diff --git a/autotests/integration/shell_client_rules_test.cpp b/autotests/integration/shell_client_rules_test.cpp index 4c2ea11ef..9be41966d 100644 --- a/autotests/integration/shell_client_rules_test.cpp +++ b/autotests/integration/shell_client_rules_test.cpp @@ -1,3935 +1,4545 @@ /******************************************************************** KWin - the KDE window manager This file is part of the KDE project. Copyright (C) 2017 Martin Flöser Copyright (C) 2019 Vlad Zagorodniy 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 "cursor.h" #include "platform.h" #include "rules.h" #include "screens.h" #include "shell_client.h" #include "virtualdesktops.h" #include "wayland_server.h" #include "workspace.h" #include #include #include using namespace KWin; using namespace KWayland::Client; static const QString s_socketName = QStringLiteral("wayland_test_kwin_shell_client_rules-0"); class TestShellClientRules : public QObject { Q_OBJECT private Q_SLOTS: void initTestCase(); void init(); void cleanup(); void testPositionDontAffect_data(); void testPositionDontAffect(); void testPositionApply_data(); void testPositionApply(); void testPositionRemember_data(); void testPositionRemember(); void testPositionForce_data(); void testPositionForce(); void testPositionApplyNow_data(); void testPositionApplyNow(); void testPositionForceTemporarily_data(); void testPositionForceTemporarily(); void testSizeDontAffect_data(); void testSizeDontAffect(); void testSizeApply_data(); void testSizeApply(); void testSizeRemember_data(); void testSizeRemember(); void testSizeForce_data(); void testSizeForce(); void testSizeApplyNow_data(); void testSizeApplyNow(); void testSizeForceTemporarily_data(); void testSizeForceTemporarily(); + void testMaximizeDontAffect_data(); + void testMaximizeDontAffect(); + void testMaximizeApply_data(); + void testMaximizeApply(); + void testMaximizeRemember_data(); + void testMaximizeRemember(); + void testMaximizeForce_data(); + void testMaximizeForce(); + void testMaximizeApplyNow_data(); + void testMaximizeApplyNow(); + void testMaximizeForceTemporarily_data(); + void testMaximizeForceTemporarily(); + void testDesktopDontAffect_data(); void testDesktopDontAffect(); void testDesktopApply_data(); void testDesktopApply(); void testDesktopRemember_data(); void testDesktopRemember(); void testDesktopForce_data(); void testDesktopForce(); void testDesktopApplyNow_data(); void testDesktopApplyNow(); void testDesktopForceTemporarily_data(); void testDesktopForceTemporarily(); void testMinimizeDontAffect_data(); void testMinimizeDontAffect(); void testMinimizeApply_data(); void testMinimizeApply(); void testMinimizeRemember_data(); void testMinimizeRemember(); void testMinimizeForce_data(); void testMinimizeForce(); void testMinimizeApplyNow_data(); void testMinimizeApplyNow(); void testMinimizeForceTemporarily_data(); void testMinimizeForceTemporarily(); void testSkipTaskbarDontAffect_data(); void testSkipTaskbarDontAffect(); void testSkipTaskbarApply_data(); void testSkipTaskbarApply(); void testSkipTaskbarRemember_data(); void testSkipTaskbarRemember(); void testSkipTaskbarForce_data(); void testSkipTaskbarForce(); void testSkipTaskbarApplyNow_data(); void testSkipTaskbarApplyNow(); void testSkipTaskbarForceTemporarily_data(); void testSkipTaskbarForceTemporarily(); void testSkipPagerDontAffect_data(); void testSkipPagerDontAffect(); void testSkipPagerApply_data(); void testSkipPagerApply(); void testSkipPagerRemember_data(); void testSkipPagerRemember(); void testSkipPagerForce_data(); void testSkipPagerForce(); void testSkipPagerApplyNow_data(); void testSkipPagerApplyNow(); void testSkipPagerForceTemporarily_data(); void testSkipPagerForceTemporarily(); void testSkipSwitcherDontAffect_data(); void testSkipSwitcherDontAffect(); void testSkipSwitcherApply_data(); void testSkipSwitcherApply(); void testSkipSwitcherRemember_data(); void testSkipSwitcherRemember(); void testSkipSwitcherForce_data(); void testSkipSwitcherForce(); void testSkipSwitcherApplyNow_data(); void testSkipSwitcherApplyNow(); void testSkipSwitcherForceTemporarily_data(); void testSkipSwitcherForceTemporarily(); void testKeepAboveDontAffect_data(); void testKeepAboveDontAffect(); void testKeepAboveApply_data(); void testKeepAboveApply(); void testKeepAboveRemember_data(); void testKeepAboveRemember(); void testKeepAboveForce_data(); void testKeepAboveForce(); void testKeepAboveApplyNow_data(); void testKeepAboveApplyNow(); void testKeepAboveForceTemporarily_data(); void testKeepAboveForceTemporarily(); void testKeepBelowDontAffect_data(); void testKeepBelowDontAffect(); void testKeepBelowApply_data(); void testKeepBelowApply(); void testKeepBelowRemember_data(); void testKeepBelowRemember(); void testKeepBelowForce_data(); void testKeepBelowForce(); void testKeepBelowApplyNow_data(); void testKeepBelowApplyNow(); void testKeepBelowForceTemporarily_data(); void testKeepBelowForceTemporarily(); void testShortcutDontAffect_data(); void testShortcutDontAffect(); void testShortcutApply_data(); void testShortcutApply(); void testShortcutRemember_data(); void testShortcutRemember(); void testShortcutForce_data(); void testShortcutForce(); void testShortcutApplyNow_data(); void testShortcutApplyNow(); void testShortcutForceTemporarily_data(); void testShortcutForceTemporarily(); void testDesktopFileDontAffect_data(); void testDesktopFileDontAffect(); void testDesktopFileApply_data(); void testDesktopFileApply(); void testDesktopFileRemember_data(); void testDesktopFileRemember(); void testDesktopFileForce_data(); void testDesktopFileForce(); void testDesktopFileApplyNow_data(); void testDesktopFileApplyNow(); void testDesktopFileForceTemporarily_data(); void testDesktopFileForceTemporarily(); void testActiveOpacityDontAffect_data(); void testActiveOpacityDontAffect(); void testActiveOpacityForce_data(); void testActiveOpacityForce(); void testActiveOpacityForceTemporarily_data(); void testActiveOpacityForceTemporarily(); void testInactiveOpacityDontAffect_data(); void testInactiveOpacityDontAffect(); void testInactiveOpacityForce_data(); void testInactiveOpacityForce(); void testInactiveOpacityForceTemporarily_data(); void testInactiveOpacityForceTemporarily(); void testMatchAfterNameChange(); }; void TestShellClientRules::initTestCase() { qRegisterMetaType(); qRegisterMetaType(); QSignalSpy workspaceCreatedSpy(kwinApp(), &Application::workspaceCreated); QVERIFY(workspaceCreatedSpy.isValid()); kwinApp()->platform()->setInitialWindowSize(QSize(1280, 1024)); QMetaObject::invokeMethod(kwinApp()->platform(), "setVirtualOutputs", Qt::DirectConnection, Q_ARG(int, 2)); QVERIFY(waylandServer()->init(s_socketName.toLocal8Bit())); kwinApp()->start(); QVERIFY(workspaceCreatedSpy.wait()); QCOMPARE(screens()->count(), 2); QCOMPARE(screens()->geometry(0), QRect(0, 0, 1280, 1024)); QCOMPARE(screens()->geometry(1), QRect(1280, 0, 1280, 1024)); waylandServer()->initWorkspace(); } void TestShellClientRules::init() { VirtualDesktopManager::self()->setCurrent(VirtualDesktopManager::self()->desktops().first()); QVERIFY(Test::setupWaylandConnection(Test::AdditionalWaylandInterface::Decoration)); screens()->setCurrent(0); } void TestShellClientRules::cleanup() { Test::destroyWaylandConnection(); // Unreference the previous config. RuleBook::self()->setConfig({}); workspace()->slotReconfigure(); // Restore virtual desktops to the initial state. VirtualDesktopManager::self()->setCount(1); QCOMPARE(VirtualDesktopManager::self()->count(), 1u); } #define TEST_DATA(name) \ void TestShellClientRules::name##_data() \ { \ QTest::addColumn("type"); \ QTest::newRow("XdgShellV5") << Test::ShellSurfaceType::XdgShellV5; \ QTest::newRow("XdgShellV6") << Test::ShellSurfaceType::XdgShellV6; \ QTest::newRow("XdgWmBase") << Test::ShellSurfaceType::XdgShellStable; \ } std::tuple createWindow(Test::ShellSurfaceType type, const QByteArray &appId) { // Create an xdg surface. Surface *surface = Test::createSurface(); XdgShellSurface *shellSurface = Test::createXdgShellSurface(type, surface, surface, Test::CreationSetup::CreateOnly); // Assign the desired app id. shellSurface->setAppId(appId); // Wait for the initial configure event. QSignalSpy configureRequestedSpy(shellSurface, &XdgShellSurface::configureRequested); surface->commit(Surface::CommitFlag::None); configureRequestedSpy.wait(); // Draw content of the surface. shellSurface->ackConfigure(configureRequestedSpy.last().at(2).value()); ShellClient *client = Test::renderAndWaitForShown(surface, QSize(100, 50), Qt::blue); return {client, surface, shellSurface}; } TEST_DATA(testPositionDontAffect) void TestShellClientRules::testPositionDontAffect() { // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("position", QPoint(42, 42)); group.writeEntry("positionrule", int(Rules::DontAffect)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // Create the test client. QFETCH(Test::ShellSurfaceType, type); ShellClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); QVERIFY(client->isActive()); // The position of the client should not be affected by the rule. The default // placement policy will put the client in the top-left corner of the screen. QVERIFY(client->isMovable()); QVERIFY(client->isMovableAcrossScreens()); QCOMPARE(client->pos(), QPoint(0, 0)); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testPositionApply) void TestShellClientRules::testPositionApply() { // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("position", QPoint(42, 42)); group.writeEntry("positionrule", int(Rules::Apply)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // Create the test client. QFETCH(Test::ShellSurfaceType, type); ShellClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); QVERIFY(client->isActive()); // The client should be moved to the position specified by the rule. QVERIFY(client->isMovable()); QVERIFY(client->isMovableAcrossScreens()); QCOMPARE(client->pos(), QPoint(42, 42)); // One should still be able to move the client around. QSignalSpy clientStartMoveResizedSpy(client, &AbstractClient::clientStartUserMovedResized); QVERIFY(clientStartMoveResizedSpy.isValid()); QSignalSpy clientStepUserMovedResizedSpy(client, &AbstractClient::clientStepUserMovedResized); QVERIFY(clientStepUserMovedResizedSpy.isValid()); QSignalSpy clientFinishUserMovedResizedSpy(client, &AbstractClient::clientFinishUserMovedResized); QVERIFY(clientFinishUserMovedResizedSpy.isValid()); QCOMPARE(workspace()->moveResizeClient(), nullptr); QVERIFY(!client->isMove()); QVERIFY(!client->isResize()); workspace()->slotWindowMove(); QCOMPARE(workspace()->moveResizeClient(), client); QCOMPARE(clientStartMoveResizedSpy.count(), 1); QVERIFY(client->isMove()); QVERIFY(!client->isResize()); const QPoint cursorPos = KWin::Cursor::pos(); client->keyPressEvent(Qt::Key_Right); client->updateMoveResize(KWin::Cursor::pos()); QCOMPARE(KWin::Cursor::pos(), cursorPos + QPoint(8, 0)); QCOMPARE(clientStepUserMovedResizedSpy.count(), 1); QCOMPARE(client->pos(), QPoint(50, 42)); client->keyPressEvent(Qt::Key_Enter); QCOMPARE(clientFinishUserMovedResizedSpy.count(), 1); QCOMPARE(workspace()->moveResizeClient(), nullptr); QVERIFY(!client->isMove()); QVERIFY(!client->isResize()); QCOMPARE(client->pos(), QPoint(50, 42)); // The rule should be applied again if the client appears after it's been closed. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); QVERIFY(client->isActive()); QVERIFY(client->isMovable()); QVERIFY(client->isMovableAcrossScreens()); QCOMPARE(client->pos(), QPoint(42, 42)); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testPositionRemember) void TestShellClientRules::testPositionRemember() { // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("position", QPoint(42, 42)); group.writeEntry("positionrule", int(Rules::Remember)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // Create the test client. QFETCH(Test::ShellSurfaceType, type); ShellClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); QVERIFY(client->isActive()); // The client should be moved to the position specified by the rule. QVERIFY(client->isMovable()); QVERIFY(client->isMovableAcrossScreens()); QCOMPARE(client->pos(), QPoint(42, 42)); // One should still be able to move the client around. QSignalSpy clientStartMoveResizedSpy(client, &AbstractClient::clientStartUserMovedResized); QVERIFY(clientStartMoveResizedSpy.isValid()); QSignalSpy clientStepUserMovedResizedSpy(client, &AbstractClient::clientStepUserMovedResized); QVERIFY(clientStepUserMovedResizedSpy.isValid()); QSignalSpy clientFinishUserMovedResizedSpy(client, &AbstractClient::clientFinishUserMovedResized); QVERIFY(clientFinishUserMovedResizedSpy.isValid()); QCOMPARE(workspace()->moveResizeClient(), nullptr); QVERIFY(!client->isMove()); QVERIFY(!client->isResize()); workspace()->slotWindowMove(); QCOMPARE(workspace()->moveResizeClient(), client); QCOMPARE(clientStartMoveResizedSpy.count(), 1); QVERIFY(client->isMove()); QVERIFY(!client->isResize()); const QPoint cursorPos = KWin::Cursor::pos(); client->keyPressEvent(Qt::Key_Right); client->updateMoveResize(KWin::Cursor::pos()); QCOMPARE(KWin::Cursor::pos(), cursorPos + QPoint(8, 0)); QCOMPARE(clientStepUserMovedResizedSpy.count(), 1); QCOMPARE(client->pos(), QPoint(50, 42)); client->keyPressEvent(Qt::Key_Enter); QCOMPARE(clientFinishUserMovedResizedSpy.count(), 1); QCOMPARE(workspace()->moveResizeClient(), nullptr); QVERIFY(!client->isMove()); QVERIFY(!client->isResize()); QCOMPARE(client->pos(), QPoint(50, 42)); // The client should be placed at the last know position if we reopen it. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); QVERIFY(client->isActive()); QVERIFY(client->isMovable()); QVERIFY(client->isMovableAcrossScreens()); QCOMPARE(client->pos(), QPoint(50, 42)); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testPositionForce) void TestShellClientRules::testPositionForce() { // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("position", QPoint(42, 42)); group.writeEntry("positionrule", int(Rules::Force)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // Create the test client. QFETCH(Test::ShellSurfaceType, type); ShellClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); QVERIFY(client->isActive()); // The client should be moved to the position specified by the rule. QVERIFY(!client->isMovable()); QVERIFY(!client->isMovableAcrossScreens()); QCOMPARE(client->pos(), QPoint(42, 42)); // User should not be able to move the client. QSignalSpy clientStartMoveResizedSpy(client, &AbstractClient::clientStartUserMovedResized); QVERIFY(clientStartMoveResizedSpy.isValid()); QCOMPARE(workspace()->moveResizeClient(), nullptr); QVERIFY(!client->isMove()); QVERIFY(!client->isResize()); workspace()->slotWindowMove(); QCOMPARE(workspace()->moveResizeClient(), nullptr); QCOMPARE(clientStartMoveResizedSpy.count(), 0); QVERIFY(!client->isMove()); QVERIFY(!client->isResize()); // The position should still be forced if we reopen the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); QVERIFY(client->isActive()); QVERIFY(!client->isMovable()); QVERIFY(!client->isMovableAcrossScreens()); QCOMPARE(client->pos(), QPoint(42, 42)); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testPositionApplyNow) void TestShellClientRules::testPositionApplyNow() { // Create the test client. QFETCH(Test::ShellSurfaceType, type); ShellClient *client; Surface *surface; QObject *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); QVERIFY(client->isActive()); // The position of the client isn't set by any rule, thus the default placement // policy will try to put the client in the top-left corner of the screen. QVERIFY(client->isMovable()); QVERIFY(client->isMovableAcrossScreens()); QCOMPARE(client->pos(), QPoint(0, 0)); // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("position", QPoint(42, 42)); group.writeEntry("positionrule", int(Rules::ApplyNow)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); // The client should be moved to the position specified by the rule. QSignalSpy geometryChangedSpy(client, &AbstractClient::geometryChanged); QVERIFY(geometryChangedSpy.isValid()); workspace()->slotReconfigure(); QCOMPARE(geometryChangedSpy.count(), 1); QCOMPARE(client->pos(), QPoint(42, 42)); // We still have to be able to move the client around. QVERIFY(client->isMovable()); QVERIFY(client->isMovableAcrossScreens()); QSignalSpy clientStartMoveResizedSpy(client, &AbstractClient::clientStartUserMovedResized); QVERIFY(clientStartMoveResizedSpy.isValid()); QSignalSpy clientStepUserMovedResizedSpy(client, &AbstractClient::clientStepUserMovedResized); QVERIFY(clientStepUserMovedResizedSpy.isValid()); QSignalSpy clientFinishUserMovedResizedSpy(client, &AbstractClient::clientFinishUserMovedResized); QVERIFY(clientFinishUserMovedResizedSpy.isValid()); QCOMPARE(workspace()->moveResizeClient(), nullptr); QVERIFY(!client->isMove()); QVERIFY(!client->isResize()); workspace()->slotWindowMove(); QCOMPARE(workspace()->moveResizeClient(), client); QCOMPARE(clientStartMoveResizedSpy.count(), 1); QVERIFY(client->isMove()); QVERIFY(!client->isResize()); const QPoint cursorPos = KWin::Cursor::pos(); client->keyPressEvent(Qt::Key_Right); client->updateMoveResize(KWin::Cursor::pos()); QCOMPARE(KWin::Cursor::pos(), cursorPos + QPoint(8, 0)); QCOMPARE(clientStepUserMovedResizedSpy.count(), 1); QCOMPARE(client->pos(), QPoint(50, 42)); client->keyPressEvent(Qt::Key_Enter); QCOMPARE(clientFinishUserMovedResizedSpy.count(), 1); QCOMPARE(workspace()->moveResizeClient(), nullptr); QVERIFY(!client->isMove()); QVERIFY(!client->isResize()); QCOMPARE(client->pos(), QPoint(50, 42)); // The rule should not be applied again. client->evaluateWindowRules(); QCOMPARE(client->pos(), QPoint(50, 42)); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testPositionForceTemporarily) void TestShellClientRules::testPositionForceTemporarily() { // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("position", QPoint(42, 42)); group.writeEntry("positionrule", int(Rules::ForceTemporarily)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // Create the test client. QFETCH(Test::ShellSurfaceType, type); ShellClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); QVERIFY(client->isActive()); // The client should be moved to the position specified by the rule. QVERIFY(!client->isMovable()); QVERIFY(!client->isMovableAcrossScreens()); QCOMPARE(client->pos(), QPoint(42, 42)); // User should not be able to move the client. QSignalSpy clientStartMoveResizedSpy(client, &AbstractClient::clientStartUserMovedResized); QVERIFY(clientStartMoveResizedSpy.isValid()); QCOMPARE(workspace()->moveResizeClient(), nullptr); QVERIFY(!client->isMove()); QVERIFY(!client->isResize()); workspace()->slotWindowMove(); QCOMPARE(workspace()->moveResizeClient(), nullptr); QCOMPARE(clientStartMoveResizedSpy.count(), 0); QVERIFY(!client->isMove()); QVERIFY(!client->isResize()); // The rule should be discarded if we close the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); QVERIFY(client->isActive()); QVERIFY(client->isMovable()); QVERIFY(client->isMovableAcrossScreens()); QCOMPARE(client->pos(), QPoint(0, 0)); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testSizeDontAffect) void TestShellClientRules::testSizeDontAffect() { // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("size", QSize(480, 640)); group.writeEntry("sizerule", int(Rules::DontAffect)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // Create the test client. QFETCH(Test::ShellSurfaceType, type); QScopedPointer surface; surface.reset(Test::createSurface()); QScopedPointer shellSurface; shellSurface.reset(createXdgShellSurface(type, surface.data(), surface.data(), Test::CreationSetup::CreateOnly)); QScopedPointer configureRequestedSpy; configureRequestedSpy.reset(new QSignalSpy(shellSurface.data(), &XdgShellSurface::configureRequested)); shellSurface->setAppId("org.kde.foo"); surface->commit(Surface::CommitFlag::None); // The window size shouldn't be enforced by the rule. QVERIFY(configureRequestedSpy->wait()); QCOMPARE(configureRequestedSpy->count(), 1); QCOMPARE(configureRequestedSpy->last().first().toSize(), QSize(0, 0)); // Map the client. shellSurface->ackConfigure(configureRequestedSpy->last().at(2).value()); ShellClient *client = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); QVERIFY(client); QVERIFY(client->isActive()); QVERIFY(client->isResizable()); QCOMPARE(client->size(), QSize(100, 50)); // We should receive a configure event when the client becomes active. QVERIFY(configureRequestedSpy->wait()); QCOMPARE(configureRequestedSpy->count(), 2); // Destroy the client. shellSurface.reset(); surface.reset(); QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testSizeApply) void TestShellClientRules::testSizeApply() { // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("size", QSize(480, 640)); group.writeEntry("sizerule", int(Rules::Apply)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // Create the test client. QFETCH(Test::ShellSurfaceType, type); QScopedPointer surface; surface.reset(Test::createSurface()); QScopedPointer shellSurface; shellSurface.reset(createXdgShellSurface(type, surface.data(), surface.data(), Test::CreationSetup::CreateOnly)); QScopedPointer configureRequestedSpy; configureRequestedSpy.reset(new QSignalSpy(shellSurface.data(), &XdgShellSurface::configureRequested)); shellSurface->setAppId("org.kde.foo"); surface->commit(Surface::CommitFlag::None); // The initial configure event should contain size hint set by the rule. XdgShellSurface::States states; QVERIFY(configureRequestedSpy->wait()); QCOMPARE(configureRequestedSpy->count(), 1); QCOMPARE(configureRequestedSpy->last().at(0).toSize(), QSize(480, 640)); states = configureRequestedSpy->last().at(1).value(); QVERIFY(!states.testFlag(XdgShellSurface::State::Activated)); QVERIFY(!states.testFlag(XdgShellSurface::State::Resizing)); // Map the client. shellSurface->ackConfigure(configureRequestedSpy->last().at(2).value()); ShellClient *client = Test::renderAndWaitForShown(surface.data(), QSize(480, 640), Qt::blue); QVERIFY(client); QVERIFY(client->isActive()); QVERIFY(client->isResizable()); QCOMPARE(client->size(), QSize(480, 640)); // We should receive a configure event when the client becomes active. QVERIFY(configureRequestedSpy->wait()); QCOMPARE(configureRequestedSpy->count(), 2); states = configureRequestedSpy->last().at(1).value(); QVERIFY(states.testFlag(XdgShellSurface::State::Activated)); QVERIFY(!states.testFlag(XdgShellSurface::State::Resizing)); // One still should be able to resize the client. QSignalSpy geometryChangedSpy(client, &AbstractClient::geometryChanged); QVERIFY(geometryChangedSpy.isValid()); QSignalSpy clientStartMoveResizedSpy(client, &AbstractClient::clientStartUserMovedResized); QVERIFY(clientStartMoveResizedSpy.isValid()); QSignalSpy clientStepUserMovedResizedSpy(client, &AbstractClient::clientStepUserMovedResized); QVERIFY(clientStepUserMovedResizedSpy.isValid()); QSignalSpy clientFinishUserMovedResizedSpy(client, &AbstractClient::clientFinishUserMovedResized); QVERIFY(clientFinishUserMovedResizedSpy.isValid()); QSignalSpy surfaceSizeChangedSpy(shellSurface.data(), &XdgShellSurface::sizeChanged); QVERIFY(surfaceSizeChangedSpy.isValid()); QCOMPARE(workspace()->moveResizeClient(), nullptr); QVERIFY(!client->isMove()); QVERIFY(!client->isResize()); workspace()->slotWindowResize(); QCOMPARE(workspace()->moveResizeClient(), client); QCOMPARE(clientStartMoveResizedSpy.count(), 1); QVERIFY(!client->isMove()); QVERIFY(client->isResize()); QVERIFY(configureRequestedSpy->wait()); QCOMPARE(configureRequestedSpy->count(), 3); states = configureRequestedSpy->last().at(1).value(); QVERIFY(states.testFlag(XdgShellSurface::State::Activated)); QVERIFY(states.testFlag(XdgShellSurface::State::Resizing)); shellSurface->ackConfigure(configureRequestedSpy->last().at(2).value()); const QPoint cursorPos = KWin::Cursor::pos(); client->keyPressEvent(Qt::Key_Right); client->updateMoveResize(KWin::Cursor::pos()); QCOMPARE(KWin::Cursor::pos(), cursorPos + QPoint(8, 0)); QVERIFY(configureRequestedSpy->wait()); QCOMPARE(configureRequestedSpy->count(), 4); states = configureRequestedSpy->last().at(1).value(); QVERIFY(states.testFlag(XdgShellSurface::State::Activated)); QVERIFY(states.testFlag(XdgShellSurface::State::Resizing)); QCOMPARE(surfaceSizeChangedSpy.count(), 1); QCOMPARE(surfaceSizeChangedSpy.last().first().toSize(), QSize(488, 640)); QCOMPARE(clientStepUserMovedResizedSpy.count(), 0); shellSurface->ackConfigure(configureRequestedSpy->last().at(2).value()); Test::render(surface.data(), QSize(488, 640), Qt::blue); QVERIFY(geometryChangedSpy.wait()); QCOMPARE(client->size(), QSize(488, 640)); QCOMPARE(clientStepUserMovedResizedSpy.count(), 1); client->keyPressEvent(Qt::Key_Enter); QCOMPARE(clientFinishUserMovedResizedSpy.count(), 1); QCOMPARE(workspace()->moveResizeClient(), nullptr); 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. shellSurface.reset(); surface.reset(); QVERIFY(Test::waitForWindowDestroyed(client)); surface.reset(Test::createSurface()); shellSurface.reset(createXdgShellSurface(type, surface.data(), surface.data(), Test::CreationSetup::CreateOnly)); configureRequestedSpy.reset(new QSignalSpy(shellSurface.data(), &XdgShellSurface::configureRequested)); shellSurface->setAppId("org.kde.foo"); surface->commit(Surface::CommitFlag::None); QVERIFY(configureRequestedSpy->wait()); QCOMPARE(configureRequestedSpy->count(), 1); QCOMPARE(configureRequestedSpy->last().first().toSize(), QSize(480, 640)); shellSurface->ackConfigure(configureRequestedSpy->last().at(2).value()); client = Test::renderAndWaitForShown(surface.data(), QSize(480, 640), Qt::blue); QVERIFY(client); QVERIFY(client->isActive()); QVERIFY(client->isResizable()); QCOMPARE(client->size(), QSize(480, 640)); QVERIFY(configureRequestedSpy->wait()); QCOMPARE(configureRequestedSpy->count(), 2); // Destroy the client. shellSurface.reset(); surface.reset(); QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testSizeRemember) void TestShellClientRules::testSizeRemember() { // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("size", QSize(480, 640)); group.writeEntry("sizerule", int(Rules::Remember)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // Create the test client. QFETCH(Test::ShellSurfaceType, type); QScopedPointer surface; surface.reset(Test::createSurface()); QScopedPointer shellSurface; shellSurface.reset(createXdgShellSurface(type, surface.data(), surface.data(), Test::CreationSetup::CreateOnly)); QScopedPointer configureRequestedSpy; configureRequestedSpy.reset(new QSignalSpy(shellSurface.data(), &XdgShellSurface::configureRequested)); shellSurface->setAppId("org.kde.foo"); surface->commit(Surface::CommitFlag::None); // The initial configure event should contain size hint set by the rule. XdgShellSurface::States states; QVERIFY(configureRequestedSpy->wait()); QCOMPARE(configureRequestedSpy->count(), 1); QCOMPARE(configureRequestedSpy->last().first().toSize(), QSize(480, 640)); states = configureRequestedSpy->last().at(1).value(); QVERIFY(!states.testFlag(XdgShellSurface::State::Activated)); QVERIFY(!states.testFlag(XdgShellSurface::State::Resizing)); // Map the client. shellSurface->ackConfigure(configureRequestedSpy->last().at(2).value()); ShellClient *client = Test::renderAndWaitForShown(surface.data(), QSize(480, 640), Qt::blue); QVERIFY(client); QVERIFY(client->isActive()); QVERIFY(client->isResizable()); QCOMPARE(client->size(), QSize(480, 640)); // We should receive a configure event when the client becomes active. QVERIFY(configureRequestedSpy->wait()); QCOMPARE(configureRequestedSpy->count(), 2); states = configureRequestedSpy->last().at(1).value(); QVERIFY(states.testFlag(XdgShellSurface::State::Activated)); QVERIFY(!states.testFlag(XdgShellSurface::State::Resizing)); // One should still be able to resize the client. QSignalSpy geometryChangedSpy(client, &AbstractClient::geometryChanged); QVERIFY(geometryChangedSpy.isValid()); QSignalSpy clientStartMoveResizedSpy(client, &AbstractClient::clientStartUserMovedResized); QVERIFY(clientStartMoveResizedSpy.isValid()); QSignalSpy clientStepUserMovedResizedSpy(client, &AbstractClient::clientStepUserMovedResized); QVERIFY(clientStepUserMovedResizedSpy.isValid()); QSignalSpy clientFinishUserMovedResizedSpy(client, &AbstractClient::clientFinishUserMovedResized); QVERIFY(clientFinishUserMovedResizedSpy.isValid()); QSignalSpy surfaceSizeChangedSpy(shellSurface.data(), &XdgShellSurface::sizeChanged); QVERIFY(surfaceSizeChangedSpy.isValid()); QCOMPARE(workspace()->moveResizeClient(), nullptr); QVERIFY(!client->isMove()); QVERIFY(!client->isResize()); workspace()->slotWindowResize(); QCOMPARE(workspace()->moveResizeClient(), client); QCOMPARE(clientStartMoveResizedSpy.count(), 1); QVERIFY(!client->isMove()); QVERIFY(client->isResize()); QVERIFY(configureRequestedSpy->wait()); QCOMPARE(configureRequestedSpy->count(), 3); states = configureRequestedSpy->last().at(1).value(); QVERIFY(states.testFlag(XdgShellSurface::State::Activated)); QVERIFY(states.testFlag(XdgShellSurface::State::Resizing)); shellSurface->ackConfigure(configureRequestedSpy->last().at(2).value()); const QPoint cursorPos = KWin::Cursor::pos(); client->keyPressEvent(Qt::Key_Right); client->updateMoveResize(KWin::Cursor::pos()); QCOMPARE(KWin::Cursor::pos(), cursorPos + QPoint(8, 0)); QVERIFY(configureRequestedSpy->wait()); QCOMPARE(configureRequestedSpy->count(), 4); states = configureRequestedSpy->last().at(1).value(); QVERIFY(states.testFlag(XdgShellSurface::State::Activated)); QVERIFY(states.testFlag(XdgShellSurface::State::Resizing)); QCOMPARE(surfaceSizeChangedSpy.count(), 1); QCOMPARE(surfaceSizeChangedSpy.last().first().toSize(), QSize(488, 640)); QCOMPARE(clientStepUserMovedResizedSpy.count(), 0); shellSurface->ackConfigure(configureRequestedSpy->last().at(2).value()); Test::render(surface.data(), QSize(488, 640), Qt::blue); QVERIFY(geometryChangedSpy.wait()); QCOMPARE(client->size(), QSize(488, 640)); QCOMPARE(clientStepUserMovedResizedSpy.count(), 1); client->keyPressEvent(Qt::Key_Enter); QCOMPARE(clientFinishUserMovedResizedSpy.count(), 1); QCOMPARE(workspace()->moveResizeClient(), nullptr); 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. shellSurface.reset(); surface.reset(); QVERIFY(Test::waitForWindowDestroyed(client)); surface.reset(Test::createSurface()); shellSurface.reset(createXdgShellSurface(type, surface.data(), surface.data(), Test::CreationSetup::CreateOnly)); configureRequestedSpy.reset(new QSignalSpy(shellSurface.data(), &XdgShellSurface::configureRequested)); shellSurface->setAppId("org.kde.foo"); surface->commit(Surface::CommitFlag::None); QVERIFY(configureRequestedSpy->wait()); QCOMPARE(configureRequestedSpy->count(), 1); QCOMPARE(configureRequestedSpy->last().first().toSize(), QSize(488, 640)); shellSurface->ackConfigure(configureRequestedSpy->last().at(2).value()); client = Test::renderAndWaitForShown(surface.data(), QSize(488, 640), Qt::blue); QVERIFY(client); QVERIFY(client->isActive()); QVERIFY(client->isResizable()); QCOMPARE(client->size(), QSize(488, 640)); QVERIFY(configureRequestedSpy->wait()); QCOMPARE(configureRequestedSpy->count(), 2); // Destroy the client. shellSurface.reset(); surface.reset(); QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testSizeForce) void TestShellClientRules::testSizeForce() { // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("size", QSize(480, 640)); group.writeEntry("sizerule", int(Rules::Force)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // Create the test client. QFETCH(Test::ShellSurfaceType, type); QScopedPointer surface; surface.reset(Test::createSurface()); QScopedPointer shellSurface; shellSurface.reset(createXdgShellSurface(type, surface.data(), surface.data(), Test::CreationSetup::CreateOnly)); QScopedPointer configureRequestedSpy; configureRequestedSpy.reset(new QSignalSpy(shellSurface.data(), &XdgShellSurface::configureRequested)); shellSurface->setAppId("org.kde.foo"); surface->commit(Surface::CommitFlag::None); // The initial configure event should contain size hint set by the rule. QVERIFY(configureRequestedSpy->wait()); QCOMPARE(configureRequestedSpy->count(), 1); QCOMPARE(configureRequestedSpy->last().first().toSize(), QSize(480, 640)); // Map the client. shellSurface->ackConfigure(configureRequestedSpy->last().at(2).value()); ShellClient *client = Test::renderAndWaitForShown(surface.data(), QSize(480, 640), Qt::blue); QVERIFY(client); QVERIFY(client->isActive()); QVERIFY(!client->isResizable()); QCOMPARE(client->size(), QSize(480, 640)); // We should receive a configure event when the client becomes active. QVERIFY(configureRequestedSpy->wait()); QCOMPARE(configureRequestedSpy->count(), 2); // Any attempt to resize the client should not succeed. QSignalSpy clientStartMoveResizedSpy(client, &AbstractClient::clientStartUserMovedResized); QVERIFY(clientStartMoveResizedSpy.isValid()); QCOMPARE(workspace()->moveResizeClient(), nullptr); QVERIFY(!client->isMove()); QVERIFY(!client->isResize()); workspace()->slotWindowResize(); QCOMPARE(workspace()->moveResizeClient(), nullptr); QCOMPARE(clientStartMoveResizedSpy.count(), 0); QVERIFY(!client->isMove()); QVERIFY(!client->isResize()); QVERIFY(!configureRequestedSpy->wait(100)); // If the client appears again, the size should still be forced. shellSurface.reset(); surface.reset(); QVERIFY(Test::waitForWindowDestroyed(client)); surface.reset(Test::createSurface()); shellSurface.reset(createXdgShellSurface(type, surface.data(), surface.data(), Test::CreationSetup::CreateOnly)); configureRequestedSpy.reset(new QSignalSpy(shellSurface.data(), &XdgShellSurface::configureRequested)); shellSurface->setAppId("org.kde.foo"); surface->commit(Surface::CommitFlag::None); QVERIFY(configureRequestedSpy->wait()); QCOMPARE(configureRequestedSpy->count(), 1); QCOMPARE(configureRequestedSpy->last().first().toSize(), QSize(480, 640)); shellSurface->ackConfigure(configureRequestedSpy->last().at(2).value()); client = Test::renderAndWaitForShown(surface.data(), QSize(480, 640), Qt::blue); QVERIFY(client); QVERIFY(client->isActive()); QVERIFY(!client->isResizable()); QCOMPARE(client->size(), QSize(480, 640)); QVERIFY(configureRequestedSpy->wait()); QCOMPARE(configureRequestedSpy->count(), 2); // Destroy the client. shellSurface.reset(); surface.reset(); QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testSizeApplyNow) void TestShellClientRules::testSizeApplyNow() { // Create the test client. QFETCH(Test::ShellSurfaceType, type); QScopedPointer surface; surface.reset(Test::createSurface()); QScopedPointer shellSurface; shellSurface.reset(createXdgShellSurface(type, surface.data(), surface.data(), Test::CreationSetup::CreateOnly)); QScopedPointer configureRequestedSpy; configureRequestedSpy.reset(new QSignalSpy(shellSurface.data(), &XdgShellSurface::configureRequested)); shellSurface->setAppId("org.kde.foo"); surface->commit(Surface::CommitFlag::None); // The expected surface dimensions should be set by the rule. QVERIFY(configureRequestedSpy->wait()); QCOMPARE(configureRequestedSpy->count(), 1); QCOMPARE(configureRequestedSpy->last().first().toSize(), QSize(0, 0)); // Map the client. shellSurface->ackConfigure(configureRequestedSpy->last().at(2).value()); ShellClient *client = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); QVERIFY(client); QVERIFY(client->isActive()); QVERIFY(client->isResizable()); QCOMPARE(client->size(), QSize(100, 50)); // We should receive a configure event when the client becomes active. QVERIFY(configureRequestedSpy->wait()); QCOMPARE(configureRequestedSpy->count(), 2); // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("size", QSize(480, 640)); group.writeEntry("sizerule", int(Rules::ApplyNow)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // The compositor should send a configure event with a new size. QVERIFY(configureRequestedSpy->wait()); QCOMPARE(configureRequestedSpy->count(), 3); QCOMPARE(configureRequestedSpy->last().first().toSize(), QSize(480, 640)); // Draw the surface with the new size. QSignalSpy geometryChangedSpy(client, &AbstractClient::geometryChanged); QVERIFY(geometryChangedSpy.isValid()); shellSurface->ackConfigure(configureRequestedSpy->last().at(2).value()); Test::render(surface.data(), QSize(480, 640), Qt::blue); QVERIFY(geometryChangedSpy.wait()); QCOMPARE(client->size(), QSize(480, 640)); QVERIFY(!configureRequestedSpy->wait(100)); // The rule should not be applied again. client->evaluateWindowRules(); QVERIFY(!configureRequestedSpy->wait(100)); // Destroy the client. shellSurface.reset(); surface.reset(); QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testSizeForceTemporarily) void TestShellClientRules::testSizeForceTemporarily() { // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("size", QSize(480, 640)); group.writeEntry("sizerule", int(Rules::ForceTemporarily)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // Create the test client. QFETCH(Test::ShellSurfaceType, type); QScopedPointer surface; surface.reset(Test::createSurface()); QScopedPointer shellSurface; shellSurface.reset(createXdgShellSurface(type, surface.data(), surface.data(), Test::CreationSetup::CreateOnly)); QScopedPointer configureRequestedSpy; configureRequestedSpy.reset(new QSignalSpy(shellSurface.data(), &XdgShellSurface::configureRequested)); shellSurface->setAppId("org.kde.foo"); surface->commit(Surface::CommitFlag::None); // The initial configure event should contain size hint set by the rule. QVERIFY(configureRequestedSpy->wait()); QCOMPARE(configureRequestedSpy->count(), 1); QCOMPARE(configureRequestedSpy->last().first().toSize(), QSize(480, 640)); // Map the client. shellSurface->ackConfigure(configureRequestedSpy->last().at(2).value()); ShellClient *client = Test::renderAndWaitForShown(surface.data(), QSize(480, 640), Qt::blue); QVERIFY(client); QVERIFY(client->isActive()); QVERIFY(!client->isResizable()); QCOMPARE(client->size(), QSize(480, 640)); // We should receive a configure event when the client becomes active. QVERIFY(configureRequestedSpy->wait()); QCOMPARE(configureRequestedSpy->count(), 2); // Any attempt to resize the client should not succeed. QSignalSpy clientStartMoveResizedSpy(client, &AbstractClient::clientStartUserMovedResized); QVERIFY(clientStartMoveResizedSpy.isValid()); QCOMPARE(workspace()->moveResizeClient(), nullptr); QVERIFY(!client->isMove()); QVERIFY(!client->isResize()); workspace()->slotWindowResize(); QCOMPARE(workspace()->moveResizeClient(), nullptr); QCOMPARE(clientStartMoveResizedSpy.count(), 0); QVERIFY(!client->isMove()); QVERIFY(!client->isResize()); QVERIFY(!configureRequestedSpy->wait(100)); // The rule should be discarded when the client is closed. shellSurface.reset(); surface.reset(); QVERIFY(Test::waitForWindowDestroyed(client)); surface.reset(Test::createSurface()); shellSurface.reset(createXdgShellSurface(type, surface.data(), surface.data(), Test::CreationSetup::CreateOnly)); configureRequestedSpy.reset(new QSignalSpy(shellSurface.data(), &XdgShellSurface::configureRequested)); shellSurface->setAppId("org.kde.foo"); surface->commit(Surface::CommitFlag::None); QVERIFY(configureRequestedSpy->wait()); QCOMPARE(configureRequestedSpy->count(), 1); QCOMPARE(configureRequestedSpy->last().first().toSize(), QSize(0, 0)); shellSurface->ackConfigure(configureRequestedSpy->last().at(2).value()); client = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); QVERIFY(client); QVERIFY(client->isActive()); QVERIFY(client->isResizable()); QCOMPARE(client->size(), QSize(100, 50)); QVERIFY(configureRequestedSpy->wait()); QCOMPARE(configureRequestedSpy->count(), 2); // Destroy the client. shellSurface.reset(); surface.reset(); QVERIFY(Test::waitForWindowDestroyed(client)); } +TEST_DATA(testMaximizeDontAffect) + +void TestShellClientRules::testMaximizeDontAffect() +{ + // Initialize RuleBook with the test rule. + auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); + config->group("General").writeEntry("count", 1); + KConfigGroup group = config->group("1"); + group.writeEntry("maximizehoriz", true); + group.writeEntry("maximizehorizrule", int(Rules::DontAffect)); + group.writeEntry("maximizevert", true); + group.writeEntry("maximizevertrule", int(Rules::DontAffect)); + group.writeEntry("wmclass", "org.kde.foo"); + group.writeEntry("wmclasscomplete", false); + group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); + group.sync(); + RuleBook::self()->setConfig(config); + workspace()->slotReconfigure(); + + // Create the test client. + QFETCH(Test::ShellSurfaceType, type); + QScopedPointer surface; + surface.reset(Test::createSurface()); + QScopedPointer shellSurface; + shellSurface.reset(createXdgShellSurface(type, surface.data(), surface.data(), Test::CreationSetup::CreateOnly)); + QScopedPointer configureRequestedSpy; + configureRequestedSpy.reset(new QSignalSpy(shellSurface.data(), &XdgShellSurface::configureRequested)); + shellSurface->setAppId("org.kde.foo"); + surface->commit(Surface::CommitFlag::None); + + // Wait for the initial configure event. + XdgShellSurface::States states; + QVERIFY(configureRequestedSpy->wait()); + QCOMPARE(configureRequestedSpy->count(), 1); + QCOMPARE(configureRequestedSpy->last().at(0).toSize(), QSize(0, 0)); + states = configureRequestedSpy->last().at(1).value(); + QVERIFY(!states.testFlag(XdgShellSurface::State::Activated)); + QVERIFY(!states.testFlag(XdgShellSurface::State::Maximized)); + + // Map the client. + shellSurface->ackConfigure(configureRequestedSpy->last().at(2).value()); + ShellClient *client = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); + QVERIFY(client); + QVERIFY(client->isActive()); + QVERIFY(client->isMaximizable()); + QCOMPARE(client->maximizeMode(), MaximizeMode::MaximizeRestore); + QCOMPARE(client->requestedMaximizeMode(), MaximizeMode::MaximizeRestore); + QCOMPARE(client->size(), QSize(100, 50)); + + // We should receive a configure event when the client becomes active. + QVERIFY(configureRequestedSpy->wait()); + QCOMPARE(configureRequestedSpy->count(), 2); + states = configureRequestedSpy->last().at(1).value(); + QVERIFY(states.testFlag(XdgShellSurface::State::Activated)); + QVERIFY(!states.testFlag(XdgShellSurface::State::Maximized)); + + // Destroy the client. + shellSurface.reset(); + surface.reset(); + QVERIFY(Test::waitForWindowDestroyed(client)); +} + +TEST_DATA(testMaximizeApply) + +void TestShellClientRules::testMaximizeApply() +{ + // Initialize RuleBook with the test rule. + auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); + config->group("General").writeEntry("count", 1); + KConfigGroup group = config->group("1"); + group.writeEntry("maximizehoriz", true); + group.writeEntry("maximizehorizrule", int(Rules::Apply)); + group.writeEntry("maximizevert", true); + group.writeEntry("maximizevertrule", int(Rules::Apply)); + group.writeEntry("wmclass", "org.kde.foo"); + group.writeEntry("wmclasscomplete", false); + group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); + group.sync(); + RuleBook::self()->setConfig(config); + workspace()->slotReconfigure(); + + // Create the test client. + QFETCH(Test::ShellSurfaceType, type); + QScopedPointer surface; + surface.reset(Test::createSurface()); + QScopedPointer shellSurface; + shellSurface.reset(createXdgShellSurface(type, surface.data(), surface.data(), Test::CreationSetup::CreateOnly)); + QScopedPointer configureRequestedSpy; + configureRequestedSpy.reset(new QSignalSpy(shellSurface.data(), &XdgShellSurface::configureRequested)); + shellSurface->setAppId("org.kde.foo"); + surface->commit(Surface::CommitFlag::None); + + // Wait for the initial configure event. + XdgShellSurface::States states; + QVERIFY(configureRequestedSpy->wait()); + QCOMPARE(configureRequestedSpy->count(), 1); + QCOMPARE(configureRequestedSpy->last().at(0).toSize(), QSize(1280, 1024)); + states = configureRequestedSpy->last().at(1).value(); + QVERIFY(!states.testFlag(XdgShellSurface::State::Activated)); + QVERIFY(states.testFlag(XdgShellSurface::State::Maximized)); + + // Map the client. + shellSurface->ackConfigure(configureRequestedSpy->last().at(2).value()); + ShellClient *client = Test::renderAndWaitForShown(surface.data(), QSize(1280, 1024), Qt::blue); + QVERIFY(client); + QVERIFY(client->isActive()); + QVERIFY(client->isMaximizable()); + QCOMPARE(client->maximizeMode(), MaximizeMode::MaximizeFull); + QCOMPARE(client->requestedMaximizeMode(), MaximizeMode::MaximizeFull); + QCOMPARE(client->size(), QSize(1280, 1024)); + + // We should receive a configure event when the client becomes active. + QVERIFY(configureRequestedSpy->wait()); + QCOMPARE(configureRequestedSpy->count(), 2); + states = configureRequestedSpy->last().at(1).value(); + QVERIFY(states.testFlag(XdgShellSurface::State::Activated)); + QVERIFY(states.testFlag(XdgShellSurface::State::Maximized)); + + // One should still be able to change the maximized state of the client. + workspace()->slotWindowMaximize(); + QVERIFY(configureRequestedSpy->wait()); + QCOMPARE(configureRequestedSpy->count(), 3); + QCOMPARE(configureRequestedSpy->last().at(0).toSize(), QSize(0, 0)); + states = configureRequestedSpy->last().at(1).value(); + QVERIFY(states.testFlag(XdgShellSurface::State::Activated)); + QVERIFY(!states.testFlag(XdgShellSurface::State::Maximized)); + + QSignalSpy geometryChangedSpy(client, &AbstractClient::geometryChanged); + QVERIFY(geometryChangedSpy.isValid()); + shellSurface->ackConfigure(configureRequestedSpy->last().at(2).value()); + Test::render(surface.data(), QSize(100, 50), Qt::blue); + QVERIFY(geometryChangedSpy.wait()); + QCOMPARE(client->size(), QSize(100, 50)); + QCOMPARE(client->maximizeMode(), MaximizeMode::MaximizeRestore); + QCOMPARE(client->requestedMaximizeMode(), MaximizeMode::MaximizeRestore); + + // If we create the client again, it should be initially maximized. + shellSurface.reset(); + surface.reset(); + QVERIFY(Test::waitForWindowDestroyed(client)); + surface.reset(Test::createSurface()); + shellSurface.reset(createXdgShellSurface(type, surface.data(), surface.data(), Test::CreationSetup::CreateOnly)); + configureRequestedSpy.reset(new QSignalSpy(shellSurface.data(), &XdgShellSurface::configureRequested)); + shellSurface->setAppId("org.kde.foo"); + surface->commit(Surface::CommitFlag::None); + + QVERIFY(configureRequestedSpy->wait()); + QCOMPARE(configureRequestedSpy->count(), 1); + QCOMPARE(configureRequestedSpy->last().at(0).toSize(), QSize(1280, 1024)); + states = configureRequestedSpy->last().at(1).value(); + QVERIFY(!states.testFlag(XdgShellSurface::State::Activated)); + QVERIFY(states.testFlag(XdgShellSurface::State::Maximized)); + + shellSurface->ackConfigure(configureRequestedSpy->last().at(2).value()); + client = Test::renderAndWaitForShown(surface.data(), QSize(1280, 1024), Qt::blue); + QVERIFY(client); + QVERIFY(client->isActive()); + QVERIFY(client->isMaximizable()); + QCOMPARE(client->maximizeMode(), MaximizeMode::MaximizeFull); + QCOMPARE(client->requestedMaximizeMode(), MaximizeMode::MaximizeFull); + QCOMPARE(client->size(), QSize(1280, 1024)); + + QVERIFY(configureRequestedSpy->wait()); + QCOMPARE(configureRequestedSpy->count(), 2); + states = configureRequestedSpy->last().at(1).value(); + QVERIFY(states.testFlag(XdgShellSurface::State::Activated)); + QVERIFY(states.testFlag(XdgShellSurface::State::Maximized)); + + // Destroy the client. + shellSurface.reset(); + surface.reset(); + QVERIFY(Test::waitForWindowDestroyed(client)); +} + +TEST_DATA(testMaximizeRemember) + +void TestShellClientRules::testMaximizeRemember() +{ + // Initialize RuleBook with the test rule. + auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); + config->group("General").writeEntry("count", 1); + KConfigGroup group = config->group("1"); + group.writeEntry("maximizehoriz", true); + group.writeEntry("maximizehorizrule", int(Rules::Remember)); + group.writeEntry("maximizevert", true); + group.writeEntry("maximizevertrule", int(Rules::Remember)); + group.writeEntry("wmclass", "org.kde.foo"); + group.writeEntry("wmclasscomplete", false); + group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); + group.sync(); + RuleBook::self()->setConfig(config); + workspace()->slotReconfigure(); + + // Create the test client. + QFETCH(Test::ShellSurfaceType, type); + QScopedPointer surface; + surface.reset(Test::createSurface()); + QScopedPointer shellSurface; + shellSurface.reset(createXdgShellSurface(type, surface.data(), surface.data(), Test::CreationSetup::CreateOnly)); + QScopedPointer configureRequestedSpy; + configureRequestedSpy.reset(new QSignalSpy(shellSurface.data(), &XdgShellSurface::configureRequested)); + shellSurface->setAppId("org.kde.foo"); + surface->commit(Surface::CommitFlag::None); + + // Wait for the initial configure event. + XdgShellSurface::States states; + QVERIFY(configureRequestedSpy->wait()); + QCOMPARE(configureRequestedSpy->count(), 1); + QCOMPARE(configureRequestedSpy->last().at(0).toSize(), QSize(1280, 1024)); + states = configureRequestedSpy->last().at(1).value(); + QVERIFY(!states.testFlag(XdgShellSurface::State::Activated)); + QVERIFY(states.testFlag(XdgShellSurface::State::Maximized)); + + // Map the client. + shellSurface->ackConfigure(configureRequestedSpy->last().at(2).value()); + ShellClient *client = Test::renderAndWaitForShown(surface.data(), QSize(1280, 1024), Qt::blue); + QVERIFY(client); + QVERIFY(client->isActive()); + QVERIFY(client->isMaximizable()); + QCOMPARE(client->maximizeMode(), MaximizeMode::MaximizeFull); + QCOMPARE(client->requestedMaximizeMode(), MaximizeMode::MaximizeFull); + QCOMPARE(client->size(), QSize(1280, 1024)); + + // We should receive a configure event when the client becomes active. + QVERIFY(configureRequestedSpy->wait()); + QCOMPARE(configureRequestedSpy->count(), 2); + states = configureRequestedSpy->last().at(1).value(); + QVERIFY(states.testFlag(XdgShellSurface::State::Activated)); + QVERIFY(states.testFlag(XdgShellSurface::State::Maximized)); + + // One should still be able to change the maximized state of the client. + workspace()->slotWindowMaximize(); + QVERIFY(configureRequestedSpy->wait()); + QCOMPARE(configureRequestedSpy->count(), 3); + QCOMPARE(configureRequestedSpy->last().at(0).toSize(), QSize(0, 0)); + states = configureRequestedSpy->last().at(1).value(); + QVERIFY(states.testFlag(XdgShellSurface::State::Activated)); + QVERIFY(!states.testFlag(XdgShellSurface::State::Maximized)); + + QSignalSpy geometryChangedSpy(client, &AbstractClient::geometryChanged); + QVERIFY(geometryChangedSpy.isValid()); + shellSurface->ackConfigure(configureRequestedSpy->last().at(2).value()); + Test::render(surface.data(), QSize(100, 50), Qt::blue); + QVERIFY(geometryChangedSpy.wait()); + QCOMPARE(client->size(), QSize(100, 50)); + QCOMPARE(client->maximizeMode(), MaximizeMode::MaximizeRestore); + QCOMPARE(client->requestedMaximizeMode(), MaximizeMode::MaximizeRestore); + + // If we create the client again, it should not be maximized (because last time it wasn't). + shellSurface.reset(); + surface.reset(); + QVERIFY(Test::waitForWindowDestroyed(client)); + surface.reset(Test::createSurface()); + shellSurface.reset(createXdgShellSurface(type, surface.data(), surface.data(), Test::CreationSetup::CreateOnly)); + configureRequestedSpy.reset(new QSignalSpy(shellSurface.data(), &XdgShellSurface::configureRequested)); + shellSurface->setAppId("org.kde.foo"); + surface->commit(Surface::CommitFlag::None); + + QVERIFY(configureRequestedSpy->wait()); + QCOMPARE(configureRequestedSpy->count(), 1); + QCOMPARE(configureRequestedSpy->last().at(0).toSize(), QSize(0, 0)); + states = configureRequestedSpy->last().at(1).value(); + QVERIFY(!states.testFlag(XdgShellSurface::State::Activated)); + QVERIFY(!states.testFlag(XdgShellSurface::State::Maximized)); + + shellSurface->ackConfigure(configureRequestedSpy->last().at(2).value()); + client = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); + QVERIFY(client); + QVERIFY(client->isActive()); + QVERIFY(client->isMaximizable()); + QCOMPARE(client->maximizeMode(), MaximizeMode::MaximizeRestore); + QCOMPARE(client->requestedMaximizeMode(), MaximizeMode::MaximizeRestore); + QCOMPARE(client->size(), QSize(100, 50)); + + QVERIFY(configureRequestedSpy->wait()); + QCOMPARE(configureRequestedSpy->count(), 2); + states = configureRequestedSpy->last().at(1).value(); + QVERIFY(states.testFlag(XdgShellSurface::State::Activated)); + QVERIFY(!states.testFlag(XdgShellSurface::State::Maximized)); + + // Destroy the client. + shellSurface.reset(); + surface.reset(); + QVERIFY(Test::waitForWindowDestroyed(client)); +} + +TEST_DATA(testMaximizeForce) + +void TestShellClientRules::testMaximizeForce() +{ + // Initialize RuleBook with the test rule. + auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); + config->group("General").writeEntry("count", 1); + KConfigGroup group = config->group("1"); + group.writeEntry("maximizehoriz", true); + group.writeEntry("maximizehorizrule", int(Rules::Force)); + group.writeEntry("maximizevert", true); + group.writeEntry("maximizevertrule", int(Rules::Force)); + group.writeEntry("wmclass", "org.kde.foo"); + group.writeEntry("wmclasscomplete", false); + group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); + group.sync(); + RuleBook::self()->setConfig(config); + workspace()->slotReconfigure(); + + // Create the test client. + QFETCH(Test::ShellSurfaceType, type); + QScopedPointer surface; + surface.reset(Test::createSurface()); + QScopedPointer shellSurface; + shellSurface.reset(createXdgShellSurface(type, surface.data(), surface.data(), Test::CreationSetup::CreateOnly)); + QScopedPointer configureRequestedSpy; + configureRequestedSpy.reset(new QSignalSpy(shellSurface.data(), &XdgShellSurface::configureRequested)); + shellSurface->setAppId("org.kde.foo"); + surface->commit(Surface::CommitFlag::None); + + // Wait for the initial configure event. + XdgShellSurface::States states; + QVERIFY(configureRequestedSpy->wait()); + QCOMPARE(configureRequestedSpy->count(), 1); + QCOMPARE(configureRequestedSpy->last().at(0).toSize(), QSize(1280, 1024)); + states = configureRequestedSpy->last().at(1).value(); + QVERIFY(!states.testFlag(XdgShellSurface::State::Activated)); + QVERIFY(states.testFlag(XdgShellSurface::State::Maximized)); + + // Map the client. + shellSurface->ackConfigure(configureRequestedSpy->last().at(2).value()); + ShellClient *client = Test::renderAndWaitForShown(surface.data(), QSize(1280, 1024), Qt::blue); + QVERIFY(client); + QVERIFY(client->isActive()); + QVERIFY(!client->isMaximizable()); + QCOMPARE(client->maximizeMode(), MaximizeMode::MaximizeFull); + QCOMPARE(client->requestedMaximizeMode(), MaximizeMode::MaximizeFull); + QCOMPARE(client->size(), QSize(1280, 1024)); + + // We should receive a configure event when the client becomes active. + QVERIFY(configureRequestedSpy->wait()); + QCOMPARE(configureRequestedSpy->count(), 2); + states = configureRequestedSpy->last().at(1).value(); + QVERIFY(states.testFlag(XdgShellSurface::State::Activated)); + QVERIFY(states.testFlag(XdgShellSurface::State::Maximized)); + + // Any attempt to change the maximized state should not succeed. + const QRect oldGeometry = client->geometry(); + workspace()->slotWindowMaximize(); + QVERIFY(!configureRequestedSpy->wait(100)); + QCOMPARE(client->maximizeMode(), MaximizeMode::MaximizeFull); + QCOMPARE(client->requestedMaximizeMode(), MaximizeMode::MaximizeFull); + QCOMPARE(client->geometry(), oldGeometry); + + // If we create the client again, the maximized state should still be forced. + shellSurface.reset(); + surface.reset(); + QVERIFY(Test::waitForWindowDestroyed(client)); + surface.reset(Test::createSurface()); + shellSurface.reset(createXdgShellSurface(type, surface.data(), surface.data(), Test::CreationSetup::CreateOnly)); + configureRequestedSpy.reset(new QSignalSpy(shellSurface.data(), &XdgShellSurface::configureRequested)); + shellSurface->setAppId("org.kde.foo"); + surface->commit(Surface::CommitFlag::None); + + QVERIFY(configureRequestedSpy->wait()); + QCOMPARE(configureRequestedSpy->count(), 1); + QCOMPARE(configureRequestedSpy->last().at(0).toSize(), QSize(1280, 1024)); + states = configureRequestedSpy->last().at(1).value(); + QVERIFY(!states.testFlag(XdgShellSurface::State::Activated)); + QVERIFY(states.testFlag(XdgShellSurface::State::Maximized)); + + shellSurface->ackConfigure(configureRequestedSpy->last().at(2).value()); + client = Test::renderAndWaitForShown(surface.data(), QSize(1280, 1024), Qt::blue); + QVERIFY(client); + QVERIFY(client->isActive()); + QVERIFY(!client->isMaximizable()); + QCOMPARE(client->maximizeMode(), MaximizeMode::MaximizeFull); + QCOMPARE(client->requestedMaximizeMode(), MaximizeMode::MaximizeFull); + QCOMPARE(client->size(), QSize(1280, 1024)); + + QVERIFY(configureRequestedSpy->wait()); + QCOMPARE(configureRequestedSpy->count(), 2); + states = configureRequestedSpy->last().at(1).value(); + QVERIFY(states.testFlag(XdgShellSurface::State::Activated)); + QVERIFY(states.testFlag(XdgShellSurface::State::Maximized)); + + // Destroy the client. + shellSurface.reset(); + surface.reset(); + QVERIFY(Test::waitForWindowDestroyed(client)); +} + +TEST_DATA(testMaximizeApplyNow) + +void TestShellClientRules::testMaximizeApplyNow() +{ + // Create the test client. + QFETCH(Test::ShellSurfaceType, type); + QScopedPointer surface; + surface.reset(Test::createSurface()); + QScopedPointer shellSurface; + shellSurface.reset(createXdgShellSurface(type, surface.data(), surface.data(), Test::CreationSetup::CreateOnly)); + QScopedPointer configureRequestedSpy; + configureRequestedSpy.reset(new QSignalSpy(shellSurface.data(), &XdgShellSurface::configureRequested)); + shellSurface->setAppId("org.kde.foo"); + surface->commit(Surface::CommitFlag::None); + + // Wait for the initial configure event. + XdgShellSurface::States states; + QVERIFY(configureRequestedSpy->wait()); + QCOMPARE(configureRequestedSpy->count(), 1); + QCOMPARE(configureRequestedSpy->last().at(0).toSize(), QSize(0, 0)); + states = configureRequestedSpy->last().at(1).value(); + QVERIFY(!states.testFlag(XdgShellSurface::State::Activated)); + QVERIFY(!states.testFlag(XdgShellSurface::State::Maximized)); + + // Map the client. + shellSurface->ackConfigure(configureRequestedSpy->last().at(2).value()); + ShellClient *client = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); + QVERIFY(client); + QVERIFY(client->isActive()); + QVERIFY(client->isMaximizable()); + QCOMPARE(client->maximizeMode(), MaximizeMode::MaximizeRestore); + QCOMPARE(client->requestedMaximizeMode(), MaximizeMode::MaximizeRestore); + QCOMPARE(client->size(), QSize(100, 50)); + + // We should receive a configure event when the client becomes active. + QVERIFY(configureRequestedSpy->wait()); + QCOMPARE(configureRequestedSpy->count(), 2); + states = configureRequestedSpy->last().at(1).value(); + QVERIFY(states.testFlag(XdgShellSurface::State::Activated)); + QVERIFY(!states.testFlag(XdgShellSurface::State::Maximized)); + + // Initialize RuleBook with the test rule. + auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); + config->group("General").writeEntry("count", 1); + KConfigGroup group = config->group("1"); + group.writeEntry("maximizehoriz", true); + group.writeEntry("maximizehorizrule", int(Rules::ApplyNow)); + group.writeEntry("maximizevert", true); + group.writeEntry("maximizevertrule", int(Rules::ApplyNow)); + group.writeEntry("wmclass", "org.kde.foo"); + group.writeEntry("wmclasscomplete", false); + group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); + group.sync(); + RuleBook::self()->setConfig(config); + workspace()->slotReconfigure(); + + // We should receive a configure event with a new surface size. + QVERIFY(configureRequestedSpy->wait()); + QCOMPARE(configureRequestedSpy->count(), 3); + QCOMPARE(configureRequestedSpy->last().at(0).toSize(), QSize(1280, 1024)); + states = configureRequestedSpy->last().at(1).value(); + QVERIFY(states.testFlag(XdgShellSurface::State::Activated)); + QVERIFY(states.testFlag(XdgShellSurface::State::Maximized)); + + // Draw contents of the maximized client. + QSignalSpy geometryChangedSpy(client, &AbstractClient::geometryChanged); + QVERIFY(geometryChangedSpy.isValid()); + shellSurface->ackConfigure(configureRequestedSpy->last().at(2).value()); + Test::render(surface.data(), QSize(1280, 1024), Qt::blue); + QVERIFY(geometryChangedSpy.wait()); + QCOMPARE(client->size(), QSize(1280, 1024)); + QCOMPARE(client->maximizeMode(), MaximizeMode::MaximizeFull); + QCOMPARE(client->requestedMaximizeMode(), MaximizeMode::MaximizeFull); + + // The client still has to be maximizeable. + QVERIFY(client->isMaximizable()); + + // Restore the client. + workspace()->slotWindowMaximize(); + QVERIFY(configureRequestedSpy->wait()); + QCOMPARE(configureRequestedSpy->count(), 4); + QCOMPARE(configureRequestedSpy->last().at(0).toSize(), QSize(100, 50)); + states = configureRequestedSpy->last().at(1).value(); + QVERIFY(states.testFlag(XdgShellSurface::State::Activated)); + QVERIFY(!states.testFlag(XdgShellSurface::State::Maximized)); + + shellSurface->ackConfigure(configureRequestedSpy->last().at(2).value()); + Test::render(surface.data(), QSize(100, 50), Qt::blue); + QVERIFY(geometryChangedSpy.wait()); + QCOMPARE(client->size(), QSize(100, 50)); + QCOMPARE(client->maximizeMode(), MaximizeMode::MaximizeRestore); + QCOMPARE(client->requestedMaximizeMode(), MaximizeMode::MaximizeRestore); + + // The rule should be discarded after it's been applied. + const QRect oldGeometry = client->geometry(); + client->evaluateWindowRules(); + QVERIFY(!configureRequestedSpy->wait(100)); + QCOMPARE(client->maximizeMode(), MaximizeMode::MaximizeRestore); + QCOMPARE(client->requestedMaximizeMode(), MaximizeMode::MaximizeRestore); + QCOMPARE(client->geometry(), oldGeometry); + + // Destroy the client. + shellSurface.reset(); + surface.reset(); + QVERIFY(Test::waitForWindowDestroyed(client)); +} + +TEST_DATA(testMaximizeForceTemporarily) + +void TestShellClientRules::testMaximizeForceTemporarily() +{ + // Initialize RuleBook with the test rule. + auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); + config->group("General").writeEntry("count", 1); + KConfigGroup group = config->group("1"); + group.writeEntry("maximizehoriz", true); + group.writeEntry("maximizehorizrule", int(Rules::ForceTemporarily)); + group.writeEntry("maximizevert", true); + group.writeEntry("maximizevertrule", int(Rules::ForceTemporarily)); + group.writeEntry("wmclass", "org.kde.foo"); + group.writeEntry("wmclasscomplete", false); + group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); + group.sync(); + RuleBook::self()->setConfig(config); + workspace()->slotReconfigure(); + + // Create the test client. + QFETCH(Test::ShellSurfaceType, type); + QScopedPointer surface; + surface.reset(Test::createSurface()); + QScopedPointer shellSurface; + shellSurface.reset(createXdgShellSurface(type, surface.data(), surface.data(), Test::CreationSetup::CreateOnly)); + QScopedPointer configureRequestedSpy; + configureRequestedSpy.reset(new QSignalSpy(shellSurface.data(), &XdgShellSurface::configureRequested)); + shellSurface->setAppId("org.kde.foo"); + surface->commit(Surface::CommitFlag::None); + + // Wait for the initial configure event. + XdgShellSurface::States states; + QVERIFY(configureRequestedSpy->wait()); + QCOMPARE(configureRequestedSpy->count(), 1); + QCOMPARE(configureRequestedSpy->last().at(0).toSize(), QSize(1280, 1024)); + states = configureRequestedSpy->last().at(1).value(); + QVERIFY(!states.testFlag(XdgShellSurface::State::Activated)); + QVERIFY(states.testFlag(XdgShellSurface::State::Maximized)); + + // Map the client. + shellSurface->ackConfigure(configureRequestedSpy->last().at(2).value()); + ShellClient *client = Test::renderAndWaitForShown(surface.data(), QSize(1280, 1024), Qt::blue); + QVERIFY(client); + QVERIFY(client->isActive()); + QVERIFY(!client->isMaximizable()); + QCOMPARE(client->maximizeMode(), MaximizeMode::MaximizeFull); + QCOMPARE(client->requestedMaximizeMode(), MaximizeMode::MaximizeFull); + QCOMPARE(client->size(), QSize(1280, 1024)); + + // We should receive a configure event when the client becomes active. + QVERIFY(configureRequestedSpy->wait()); + QCOMPARE(configureRequestedSpy->count(), 2); + states = configureRequestedSpy->last().at(1).value(); + QVERIFY(states.testFlag(XdgShellSurface::State::Activated)); + QVERIFY(states.testFlag(XdgShellSurface::State::Maximized)); + + // Any attempt to change the maximized state should not succeed. + const QRect oldGeometry = client->geometry(); + workspace()->slotWindowMaximize(); + QVERIFY(!configureRequestedSpy->wait(100)); + QCOMPARE(client->maximizeMode(), MaximizeMode::MaximizeFull); + QCOMPARE(client->requestedMaximizeMode(), MaximizeMode::MaximizeFull); + QCOMPARE(client->geometry(), oldGeometry); + + // The rule should be discarded if we close the client. + shellSurface.reset(); + surface.reset(); + QVERIFY(Test::waitForWindowDestroyed(client)); + surface.reset(Test::createSurface()); + shellSurface.reset(createXdgShellSurface(type, surface.data(), surface.data(), Test::CreationSetup::CreateOnly)); + configureRequestedSpy.reset(new QSignalSpy(shellSurface.data(), &XdgShellSurface::configureRequested)); + shellSurface->setAppId("org.kde.foo"); + surface->commit(Surface::CommitFlag::None); + + QVERIFY(configureRequestedSpy->wait()); + QCOMPARE(configureRequestedSpy->count(), 1); + QCOMPARE(configureRequestedSpy->last().at(0).toSize(), QSize(0, 0)); + states = configureRequestedSpy->last().at(1).value(); + QVERIFY(!states.testFlag(XdgShellSurface::State::Activated)); + QVERIFY(!states.testFlag(XdgShellSurface::State::Maximized)); + + shellSurface->ackConfigure(configureRequestedSpy->last().at(2).value()); + client = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); + QVERIFY(client); + QVERIFY(client->isActive()); + QVERIFY(client->isMaximizable()); + QCOMPARE(client->maximizeMode(), MaximizeMode::MaximizeRestore); + QCOMPARE(client->requestedMaximizeMode(), MaximizeMode::MaximizeRestore); + QCOMPARE(client->size(), QSize(100, 50)); + + QVERIFY(configureRequestedSpy->wait()); + QCOMPARE(configureRequestedSpy->count(), 2); + states = configureRequestedSpy->last().at(1).value(); + QVERIFY(states.testFlag(XdgShellSurface::State::Activated)); + QVERIFY(!states.testFlag(XdgShellSurface::State::Maximized)); + + // Destroy the client. + shellSurface.reset(); + surface.reset(); + QVERIFY(Test::waitForWindowDestroyed(client)); +} + TEST_DATA(testDesktopDontAffect) void TestShellClientRules::testDesktopDontAffect() { // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("desktop", 2); group.writeEntry("desktoprule", int(Rules::DontAffect)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // We need at least two virtual desktop for this test. VirtualDesktopManager::self()->setCount(2); QCOMPARE(VirtualDesktopManager::self()->count(), 2u); VirtualDesktopManager::self()->setCurrent(1); QCOMPARE(VirtualDesktopManager::self()->current(), 1); // Create the test client. QFETCH(Test::ShellSurfaceType, type); ShellClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); // The client should appear on the current virtual desktop. QCOMPARE(client->desktop(), 1); QCOMPARE(VirtualDesktopManager::self()->current(), 1); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testDesktopApply) void TestShellClientRules::testDesktopApply() { // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("desktop", 2); group.writeEntry("desktoprule", int(Rules::Apply)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // We need at least two virtual desktop for this test. VirtualDesktopManager::self()->setCount(2); QCOMPARE(VirtualDesktopManager::self()->count(), 2u); VirtualDesktopManager::self()->setCurrent(1); QCOMPARE(VirtualDesktopManager::self()->current(), 1); // Create the test client. QFETCH(Test::ShellSurfaceType, type); ShellClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); // The client should appear on the second virtual desktop. QCOMPARE(client->desktop(), 2); QCOMPARE(VirtualDesktopManager::self()->current(), 2); // We still should be able to move the client between desktops. workspace()->sendClientToDesktop(client, 1, true); QCOMPARE(client->desktop(), 1); QCOMPARE(VirtualDesktopManager::self()->current(), 2); // If we re-open the client, it should appear on the second virtual desktop again. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); VirtualDesktopManager::self()->setCurrent(1); QCOMPARE(VirtualDesktopManager::self()->current(), 1); std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); QCOMPARE(client->desktop(), 2); QCOMPARE(VirtualDesktopManager::self()->current(), 2); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testDesktopRemember) void TestShellClientRules::testDesktopRemember() { // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("desktop", 2); group.writeEntry("desktoprule", int(Rules::Remember)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // We need at least two virtual desktop for this test. VirtualDesktopManager::self()->setCount(2); QCOMPARE(VirtualDesktopManager::self()->count(), 2u); VirtualDesktopManager::self()->setCurrent(1); QCOMPARE(VirtualDesktopManager::self()->current(), 1); // Create the test client. QFETCH(Test::ShellSurfaceType, type); ShellClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); QCOMPARE(client->desktop(), 2); QCOMPARE(VirtualDesktopManager::self()->current(), 2); // Move the client to the first virtual desktop. workspace()->sendClientToDesktop(client, 1, true); QCOMPARE(client->desktop(), 1); QCOMPARE(VirtualDesktopManager::self()->current(), 2); // If we create the client again, it should appear on the first virtual desktop. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); QCOMPARE(client->desktop(), 1); QCOMPARE(VirtualDesktopManager::self()->current(), 1); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testDesktopForce) void TestShellClientRules::testDesktopForce() { // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("desktop", 2); group.writeEntry("desktoprule", int(Rules::Force)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // We need at least two virtual desktop for this test. VirtualDesktopManager::self()->setCount(2); QCOMPARE(VirtualDesktopManager::self()->count(), 2u); VirtualDesktopManager::self()->setCurrent(1); QCOMPARE(VirtualDesktopManager::self()->current(), 1); // Create the test client. QFETCH(Test::ShellSurfaceType, type); ShellClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); // The client should appear on the second virtual desktop. QCOMPARE(client->desktop(), 2); QCOMPARE(VirtualDesktopManager::self()->current(), 2); // Any attempt to move the client to another virtual desktop should fail. workspace()->sendClientToDesktop(client, 1, true); QCOMPARE(client->desktop(), 2); QCOMPARE(VirtualDesktopManager::self()->current(), 2); // If we re-open the client, it should appear on the second virtual desktop again. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); VirtualDesktopManager::self()->setCurrent(1); QCOMPARE(VirtualDesktopManager::self()->current(), 1); std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); QCOMPARE(client->desktop(), 2); QCOMPARE(VirtualDesktopManager::self()->current(), 2); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testDesktopApplyNow) void TestShellClientRules::testDesktopApplyNow() { // We need at least two virtual desktop for this test. VirtualDesktopManager::self()->setCount(2); QCOMPARE(VirtualDesktopManager::self()->count(), 2u); VirtualDesktopManager::self()->setCurrent(1); QCOMPARE(VirtualDesktopManager::self()->current(), 1); // Create the test client. QFETCH(Test::ShellSurfaceType, type); ShellClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); QCOMPARE(client->desktop(), 1); QCOMPARE(VirtualDesktopManager::self()->current(), 1); // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("desktop", 2); group.writeEntry("desktoprule", int(Rules::ApplyNow)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // The client should have been moved to the second virtual desktop. QCOMPARE(client->desktop(), 2); QCOMPARE(VirtualDesktopManager::self()->current(), 1); // One should still be able to move the client between desktops. workspace()->sendClientToDesktop(client, 1, true); QCOMPARE(client->desktop(), 1); QCOMPARE(VirtualDesktopManager::self()->current(), 1); // The rule should not be applied again. client->evaluateWindowRules(); QCOMPARE(client->desktop(), 1); QCOMPARE(VirtualDesktopManager::self()->current(), 1); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testDesktopForceTemporarily) void TestShellClientRules::testDesktopForceTemporarily() { // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("desktop", 2); group.writeEntry("desktoprule", int(Rules::ForceTemporarily)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // We need at least two virtual desktop for this test. VirtualDesktopManager::self()->setCount(2); QCOMPARE(VirtualDesktopManager::self()->count(), 2u); VirtualDesktopManager::self()->setCurrent(1); QCOMPARE(VirtualDesktopManager::self()->current(), 1); // Create the test client. QFETCH(Test::ShellSurfaceType, type); ShellClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); // The client should appear on the second virtual desktop. QCOMPARE(client->desktop(), 2); QCOMPARE(VirtualDesktopManager::self()->current(), 2); // Any attempt to move the client to another virtual desktop should fail. workspace()->sendClientToDesktop(client, 1, true); QCOMPARE(client->desktop(), 2); QCOMPARE(VirtualDesktopManager::self()->current(), 2); // The rule should be discarded when the client is withdrawn. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); VirtualDesktopManager::self()->setCurrent(1); QCOMPARE(VirtualDesktopManager::self()->current(), 1); std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); QCOMPARE(client->desktop(), 1); QCOMPARE(VirtualDesktopManager::self()->current(), 1); // One should be able to move the client between desktops. workspace()->sendClientToDesktop(client, 2, true); QCOMPARE(client->desktop(), 2); QCOMPARE(VirtualDesktopManager::self()->current(), 1); workspace()->sendClientToDesktop(client, 1, true); QCOMPARE(client->desktop(), 1); QCOMPARE(VirtualDesktopManager::self()->current(), 1); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testMinimizeDontAffect) void TestShellClientRules::testMinimizeDontAffect() { // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("minimize", true); group.writeEntry("minimizerule", int(Rules::DontAffect)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // Create the test client. QFETCH(Test::ShellSurfaceType, type); ShellClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); QVERIFY(client->isMinimizable()); // The client should not be minimized. QVERIFY(!client->isMinimized()); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testMinimizeApply) void TestShellClientRules::testMinimizeApply() { // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("minimize", true); group.writeEntry("minimizerule", int(Rules::Apply)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // Create the test client. QFETCH(Test::ShellSurfaceType, type); ShellClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); QVERIFY(client->isMinimizable()); // The client should be minimized. QVERIFY(client->isMinimized()); // We should still be able to unminimize the client. client->unminimize(); QVERIFY(!client->isMinimized()); // If we re-open the client, it should be minimized back again. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); QVERIFY(client->isMinimizable()); QVERIFY(client->isMinimized()); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testMinimizeRemember) void TestShellClientRules::testMinimizeRemember() { // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("minimize", false); group.writeEntry("minimizerule", int(Rules::Remember)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // Create the test client. QFETCH(Test::ShellSurfaceType, type); ShellClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); QVERIFY(client->isMinimizable()); QVERIFY(!client->isMinimized()); // Minimize the client. client->minimize(); QVERIFY(client->isMinimized()); // If we open the client again, it should be minimized. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); QVERIFY(client->isMinimizable()); QVERIFY(client->isMinimized()); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testMinimizeForce) void TestShellClientRules::testMinimizeForce() { // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("minimize", false); group.writeEntry("minimizerule", int(Rules::Force)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // Create the test client. QFETCH(Test::ShellSurfaceType, type); ShellClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); QVERIFY(!client->isMinimizable()); QVERIFY(!client->isMinimized()); // Any attempt to minimize the client should fail. client->minimize(); QVERIFY(!client->isMinimized()); // If we re-open the client, the minimized state should still be forced. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); QVERIFY(!client->isMinimizable()); QVERIFY(!client->isMinimized()); client->minimize(); QVERIFY(!client->isMinimized()); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testMinimizeApplyNow) void TestShellClientRules::testMinimizeApplyNow() { // Create the test client. QFETCH(Test::ShellSurfaceType, type); ShellClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); QVERIFY(client->isMinimizable()); QVERIFY(!client->isMinimized()); // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("minimize", true); group.writeEntry("minimizerule", int(Rules::ApplyNow)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // The client should be minimized now. QVERIFY(client->isMinimizable()); QVERIFY(client->isMinimized()); // One is still able to unminimize the client. client->unminimize(); QVERIFY(!client->isMinimized()); // The rule should not be applied again. client->evaluateWindowRules(); QVERIFY(client->isMinimizable()); QVERIFY(!client->isMinimized()); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testMinimizeForceTemporarily) void TestShellClientRules::testMinimizeForceTemporarily() { // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("minimize", false); group.writeEntry("minimizerule", int(Rules::ForceTemporarily)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // Create the test client. QFETCH(Test::ShellSurfaceType, type); ShellClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); QVERIFY(!client->isMinimizable()); QVERIFY(!client->isMinimized()); // Any attempt to minimize the client should fail until the client is closed. client->minimize(); QVERIFY(!client->isMinimized()); // The rule should be discarded when the client is closed. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); QVERIFY(client->isMinimizable()); QVERIFY(!client->isMinimized()); client->minimize(); QVERIFY(client->isMinimized()); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testSkipTaskbarDontAffect) void TestShellClientRules::testSkipTaskbarDontAffect() { // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("skiptaskbar", true); group.writeEntry("skiptaskbarrule", int(Rules::DontAffect)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // Create the test client. QFETCH(Test::ShellSurfaceType, type); ShellClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); // The client should not be affected by the rule. QVERIFY(!client->skipTaskbar()); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testSkipTaskbarApply) void TestShellClientRules::testSkipTaskbarApply() { // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("skiptaskbar", true); group.writeEntry("skiptaskbarrule", int(Rules::Apply)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // Create the test client. QFETCH(Test::ShellSurfaceType, type); ShellClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); // The client should not be included on a taskbar. QVERIFY(client->skipTaskbar()); // Though one can change that. client->setOriginalSkipTaskbar(false); QVERIFY(!client->skipTaskbar()); // Reopen the client, the rule should be applied again. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); QVERIFY(client->skipTaskbar()); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testSkipTaskbarRemember) void TestShellClientRules::testSkipTaskbarRemember() { // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("skiptaskbar", true); group.writeEntry("skiptaskbarrule", int(Rules::Remember)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // Create the test client. QFETCH(Test::ShellSurfaceType, type); ShellClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); // The client should not be included on a taskbar. QVERIFY(client->skipTaskbar()); // Change the skip-taskbar state. client->setOriginalSkipTaskbar(false); QVERIFY(!client->skipTaskbar()); // Reopen the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); // The client should be included on a taskbar. QVERIFY(!client->skipTaskbar()); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testSkipTaskbarForce) void TestShellClientRules::testSkipTaskbarForce() { // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("skiptaskbar", true); group.writeEntry("skiptaskbarrule", int(Rules::Force)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // Create the test client. QFETCH(Test::ShellSurfaceType, type); ShellClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); // The client should not be included on a taskbar. QVERIFY(client->skipTaskbar()); // Any attempt to change the skip-taskbar state should not succeed. client->setOriginalSkipTaskbar(false); QVERIFY(client->skipTaskbar()); // Reopen the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); // The skip-taskbar state should be still forced. QVERIFY(client->skipTaskbar()); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testSkipTaskbarApplyNow) void TestShellClientRules::testSkipTaskbarApplyNow() { // Create the test client. QFETCH(Test::ShellSurfaceType, type); ShellClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); QVERIFY(!client->skipTaskbar()); // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("skiptaskbar", true); group.writeEntry("skiptaskbarrule", int(Rules::ApplyNow)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // The client should not be on a taskbar now. QVERIFY(client->skipTaskbar()); // Also, one change the skip-taskbar state. client->setOriginalSkipTaskbar(false); QVERIFY(!client->skipTaskbar()); // The rule should not be applied again. client->evaluateWindowRules(); QVERIFY(!client->skipTaskbar()); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testSkipTaskbarForceTemporarily) void TestShellClientRules::testSkipTaskbarForceTemporarily() { // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("skiptaskbar", true); group.writeEntry("skiptaskbarrule", int(Rules::ForceTemporarily)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // Create the test client. QFETCH(Test::ShellSurfaceType, type); ShellClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); // The client should not be included on a taskbar. QVERIFY(client->skipTaskbar()); // Any attempt to change the skip-taskbar state should not succeed. client->setOriginalSkipTaskbar(false); QVERIFY(client->skipTaskbar()); // The rule should be discarded when the client is closed. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); QVERIFY(!client->skipTaskbar()); // The skip-taskbar state is no longer forced. client->setOriginalSkipTaskbar(true); QVERIFY(client->skipTaskbar()); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testSkipPagerDontAffect) void TestShellClientRules::testSkipPagerDontAffect() { // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("skippager", true); group.writeEntry("skippagerrule", int(Rules::DontAffect)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // Create the test client. QFETCH(Test::ShellSurfaceType, type); ShellClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); // The client should not be affected by the rule. QVERIFY(!client->skipPager()); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testSkipPagerApply) void TestShellClientRules::testSkipPagerApply() { // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("skippager", true); group.writeEntry("skippagerrule", int(Rules::Apply)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // Create the test client. QFETCH(Test::ShellSurfaceType, type); ShellClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); // The client should not be included on a pager. QVERIFY(client->skipPager()); // Though one can change that. client->setSkipPager(false); QVERIFY(!client->skipPager()); // Reopen the client, the rule should be applied again. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); QVERIFY(client->skipPager()); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testSkipPagerRemember) void TestShellClientRules::testSkipPagerRemember() { // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("skippager", true); group.writeEntry("skippagerrule", int(Rules::Remember)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // Create the test client. QFETCH(Test::ShellSurfaceType, type); ShellClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); // The client should not be included on a pager. QVERIFY(client->skipPager()); // Change the skip-pager state. client->setSkipPager(false); QVERIFY(!client->skipPager()); // Reopen the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); // The client should be included on a pager. QVERIFY(!client->skipPager()); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testSkipPagerForce) void TestShellClientRules::testSkipPagerForce() { // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("skippager", true); group.writeEntry("skippagerrule", int(Rules::Force)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // Create the test client. QFETCH(Test::ShellSurfaceType, type); ShellClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); // The client should not be included on a pager. QVERIFY(client->skipPager()); // Any attempt to change the skip-pager state should not succeed. client->setSkipPager(false); QVERIFY(client->skipPager()); // Reopen the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); // The skip-pager state should be still forced. QVERIFY(client->skipPager()); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testSkipPagerApplyNow) void TestShellClientRules::testSkipPagerApplyNow() { // Create the test client. QFETCH(Test::ShellSurfaceType, type); ShellClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); QVERIFY(!client->skipPager()); // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("skippager", true); group.writeEntry("skippagerrule", int(Rules::ApplyNow)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // The client should not be on a pager now. QVERIFY(client->skipPager()); // Also, one change the skip-pager state. client->setSkipPager(false); QVERIFY(!client->skipPager()); // The rule should not be applied again. client->evaluateWindowRules(); QVERIFY(!client->skipPager()); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testSkipPagerForceTemporarily) void TestShellClientRules::testSkipPagerForceTemporarily() { // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("skippager", true); group.writeEntry("skippagerrule", int(Rules::ForceTemporarily)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // Create the test client. QFETCH(Test::ShellSurfaceType, type); ShellClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); // The client should not be included on a pager. QVERIFY(client->skipPager()); // Any attempt to change the skip-pager state should not succeed. client->setSkipPager(false); QVERIFY(client->skipPager()); // The rule should be discarded when the client is closed. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); QVERIFY(!client->skipPager()); // The skip-pager state is no longer forced. client->setSkipPager(true); QVERIFY(client->skipPager()); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testSkipSwitcherDontAffect) void TestShellClientRules::testSkipSwitcherDontAffect() { // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("skipswitcher", true); group.writeEntry("skipswitcherrule", int(Rules::DontAffect)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // Create the test client. QFETCH(Test::ShellSurfaceType, type); ShellClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); // The client should not be affected by the rule. QVERIFY(!client->skipSwitcher()); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testSkipSwitcherApply) void TestShellClientRules::testSkipSwitcherApply() { // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("skipswitcher", true); group.writeEntry("skipswitcherrule", int(Rules::Apply)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // Create the test client. QFETCH(Test::ShellSurfaceType, type); ShellClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); // The client should be excluded from window switching effects. QVERIFY(client->skipSwitcher()); // Though one can change that. client->setSkipSwitcher(false); QVERIFY(!client->skipSwitcher()); // Reopen the client, the rule should be applied again. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); QVERIFY(client->skipSwitcher()); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testSkipSwitcherRemember) void TestShellClientRules::testSkipSwitcherRemember() { // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("skipswitcher", true); group.writeEntry("skipswitcherrule", int(Rules::Remember)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // Create the test client. QFETCH(Test::ShellSurfaceType, type); ShellClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); // The client should be excluded from window switching effects. QVERIFY(client->skipSwitcher()); // Change the skip-switcher state. client->setSkipSwitcher(false); QVERIFY(!client->skipSwitcher()); // Reopen the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); // The client should be included in window switching effects. QVERIFY(!client->skipSwitcher()); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testSkipSwitcherForce) void TestShellClientRules::testSkipSwitcherForce() { // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("skipswitcher", true); group.writeEntry("skipswitcherrule", int(Rules::Force)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // Create the test client. QFETCH(Test::ShellSurfaceType, type); ShellClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); // The client should be excluded from window switching effects. QVERIFY(client->skipSwitcher()); // Any attempt to change the skip-switcher state should not succeed. client->setSkipSwitcher(false); QVERIFY(client->skipSwitcher()); // Reopen the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); // The skip-switcher state should be still forced. QVERIFY(client->skipSwitcher()); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testSkipSwitcherApplyNow) void TestShellClientRules::testSkipSwitcherApplyNow() { // Create the test client. QFETCH(Test::ShellSurfaceType, type); ShellClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); QVERIFY(!client->skipSwitcher()); // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("skipswitcher", true); group.writeEntry("skipswitcherrule", int(Rules::ApplyNow)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // The client should be excluded from window switching effects now. QVERIFY(client->skipSwitcher()); // Also, one change the skip-switcher state. client->setSkipSwitcher(false); QVERIFY(!client->skipSwitcher()); // The rule should not be applied again. client->evaluateWindowRules(); QVERIFY(!client->skipSwitcher()); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testSkipSwitcherForceTemporarily) void TestShellClientRules::testSkipSwitcherForceTemporarily() { // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("skipswitcher", true); group.writeEntry("skipswitcherrule", int(Rules::ForceTemporarily)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // Create the test client. QFETCH(Test::ShellSurfaceType, type); ShellClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); // The client should be excluded from window switching effects. QVERIFY(client->skipSwitcher()); // Any attempt to change the skip-switcher state should not succeed. client->setSkipSwitcher(false); QVERIFY(client->skipSwitcher()); // The rule should be discarded when the client is closed. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); QVERIFY(!client->skipSwitcher()); // The skip-switcher state is no longer forced. client->setSkipSwitcher(true); QVERIFY(client->skipSwitcher()); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testKeepAboveDontAffect) void TestShellClientRules::testKeepAboveDontAffect() { // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("above", true); group.writeEntry("aboverule", int(Rules::DontAffect)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // Create the test client. QFETCH(Test::ShellSurfaceType, type); ShellClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); // The keep-above state of the client should not be affected by the rule. QVERIFY(!client->keepAbove()); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testKeepAboveApply) void TestShellClientRules::testKeepAboveApply() { // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("above", true); group.writeEntry("aboverule", int(Rules::Apply)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // Create the test client. QFETCH(Test::ShellSurfaceType, type); ShellClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); // Initially, the client should be kept above. QVERIFY(client->keepAbove()); // One should also be able to alter the keep-above state. client->setKeepAbove(false); QVERIFY(!client->keepAbove()); // If one re-opens the client, it should be kept above back again. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); QVERIFY(client->keepAbove()); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testKeepAboveRemember) void TestShellClientRules::testKeepAboveRemember() { // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("above", true); group.writeEntry("aboverule", int(Rules::Remember)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // Create the test client. QFETCH(Test::ShellSurfaceType, type); ShellClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); // Initially, the client should be kept above. QVERIFY(client->keepAbove()); // Unset the keep-above state. client->setKeepAbove(false); QVERIFY(!client->keepAbove()); delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); // Re-open the client, it should not be kept above. std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); QVERIFY(!client->keepAbove()); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testKeepAboveForce) void TestShellClientRules::testKeepAboveForce() { // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("above", true); group.writeEntry("aboverule", int(Rules::Force)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // Create the test client. QFETCH(Test::ShellSurfaceType, type); ShellClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); // Initially, the client should be kept above. QVERIFY(client->keepAbove()); // Any attemt to unset the keep-above should not succeed. client->setKeepAbove(false); QVERIFY(client->keepAbove()); // If we re-open the client, it should still be kept above. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); QVERIFY(client->keepAbove()); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testKeepAboveApplyNow) void TestShellClientRules::testKeepAboveApplyNow() { // Create the test client. QFETCH(Test::ShellSurfaceType, type); ShellClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); QVERIFY(!client->keepAbove()); // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("above", true); group.writeEntry("aboverule", int(Rules::ApplyNow)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // The client should now be kept above other clients. QVERIFY(client->keepAbove()); // One is still able to change the keep-above state of the client. client->setKeepAbove(false); QVERIFY(!client->keepAbove()); // The rule should not be applied again. client->evaluateWindowRules(); QVERIFY(!client->keepAbove()); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testKeepAboveForceTemporarily) void TestShellClientRules::testKeepAboveForceTemporarily() { // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("above", true); group.writeEntry("aboverule", int(Rules::ForceTemporarily)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // Create the test client. QFETCH(Test::ShellSurfaceType, type); ShellClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); // Initially, the client should be kept above. QVERIFY(client->keepAbove()); // Any attempt to alter the keep-above state should not succeed. client->setKeepAbove(false); QVERIFY(client->keepAbove()); // The rule should be discarded when the client is closed. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); QVERIFY(!client->keepAbove()); // The keep-above state is no longer forced. client->setKeepAbove(true); QVERIFY(client->keepAbove()); client->setKeepAbove(false); QVERIFY(!client->keepAbove()); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testKeepBelowDontAffect) void TestShellClientRules::testKeepBelowDontAffect() { // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("below", true); group.writeEntry("belowrule", int(Rules::DontAffect)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // Create the test client. QFETCH(Test::ShellSurfaceType, type); ShellClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); // The keep-below state of the client should not be affected by the rule. QVERIFY(!client->keepBelow()); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testKeepBelowApply) void TestShellClientRules::testKeepBelowApply() { // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("below", true); group.writeEntry("belowrule", int(Rules::Apply)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // Create the test client. QFETCH(Test::ShellSurfaceType, type); ShellClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); // Initially, the client should be kept below. QVERIFY(client->keepBelow()); // One should also be able to alter the keep-below state. client->setKeepBelow(false); QVERIFY(!client->keepBelow()); // If one re-opens the client, it should be kept above back again. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); QVERIFY(client->keepBelow()); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testKeepBelowRemember) void TestShellClientRules::testKeepBelowRemember() { // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("below", true); group.writeEntry("belowrule", int(Rules::Remember)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // Create the test client. QFETCH(Test::ShellSurfaceType, type); ShellClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); // Initially, the client should be kept below. QVERIFY(client->keepBelow()); // Unset the keep-below state. client->setKeepBelow(false); QVERIFY(!client->keepBelow()); delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); // Re-open the client, it should not be kept below. std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); QVERIFY(!client->keepBelow()); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testKeepBelowForce) void TestShellClientRules::testKeepBelowForce() { // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("below", true); group.writeEntry("belowrule", int(Rules::Force)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // Create the test client. QFETCH(Test::ShellSurfaceType, type); ShellClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); // Initially, the client should be kept below. QVERIFY(client->keepBelow()); // Any attemt to unset the keep-below should not succeed. client->setKeepBelow(false); QVERIFY(client->keepBelow()); // If we re-open the client, it should still be kept below. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); QVERIFY(client->keepBelow()); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testKeepBelowApplyNow) void TestShellClientRules::testKeepBelowApplyNow() { // Create the test client. QFETCH(Test::ShellSurfaceType, type); ShellClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); QVERIFY(!client->keepBelow()); // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("below", true); group.writeEntry("belowrule", int(Rules::ApplyNow)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // The client should now be kept below other clients. QVERIFY(client->keepBelow()); // One is still able to change the keep-below state of the client. client->setKeepBelow(false); QVERIFY(!client->keepBelow()); // The rule should not be applied again. client->evaluateWindowRules(); QVERIFY(!client->keepBelow()); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testKeepBelowForceTemporarily) void TestShellClientRules::testKeepBelowForceTemporarily() { // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("below", true); group.writeEntry("belowrule", int(Rules::ForceTemporarily)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // Create the test client. QFETCH(Test::ShellSurfaceType, type); ShellClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); // Initially, the client should be kept below. QVERIFY(client->keepBelow()); // Any attempt to alter the keep-below state should not succeed. client->setKeepBelow(false); QVERIFY(client->keepBelow()); // The rule should be discarded when the client is closed. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); QVERIFY(!client->keepBelow()); // The keep-below state is no longer forced. client->setKeepBelow(true); QVERIFY(client->keepBelow()); client->setKeepBelow(false); QVERIFY(!client->keepBelow()); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testShortcutDontAffect) void TestShellClientRules::testShortcutDontAffect() { // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("shortcut", "Ctrl+Alt+1"); group.writeEntry("shortcutrule", int(Rules::DontAffect)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // Create the test client. QFETCH(Test::ShellSurfaceType, type); ShellClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); QCOMPARE(client->shortcut(), QKeySequence()); client->minimize(); QVERIFY(client->isMinimized()); // If we press the window shortcut, nothing should happen. QSignalSpy clientUnminimizedSpy(client, &AbstractClient::clientUnminimized); QVERIFY(clientUnminimizedSpy.isValid()); quint32 timestamp = 1; kwinApp()->platform()->keyboardKeyPressed(KEY_LEFTCTRL, timestamp++); kwinApp()->platform()->keyboardKeyPressed(KEY_LEFTALT, timestamp++); kwinApp()->platform()->keyboardKeyPressed(KEY_1, timestamp++); kwinApp()->platform()->keyboardKeyReleased(KEY_1, timestamp++); kwinApp()->platform()->keyboardKeyReleased(KEY_LEFTALT, timestamp++); kwinApp()->platform()->keyboardKeyReleased(KEY_LEFTCTRL, timestamp++); QVERIFY(!clientUnminimizedSpy.wait(100)); QVERIFY(client->isMinimized()); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testShortcutApply) void TestShellClientRules::testShortcutApply() { // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("shortcut", "Ctrl+Alt+1"); group.writeEntry("shortcutrule", int(Rules::Apply)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // Create the test client. QFETCH(Test::ShellSurfaceType, type); ShellClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); // If we press the window shortcut, the window should be brought back to user. QSignalSpy clientUnminimizedSpy(client, &AbstractClient::clientUnminimized); QVERIFY(clientUnminimizedSpy.isValid()); quint32 timestamp = 1; QCOMPARE(client->shortcut(), (QKeySequence{Qt::CTRL + Qt::ALT + Qt::Key_1})); client->minimize(); QVERIFY(client->isMinimized()); kwinApp()->platform()->keyboardKeyPressed(KEY_LEFTCTRL, timestamp++); kwinApp()->platform()->keyboardKeyPressed(KEY_LEFTALT, timestamp++); kwinApp()->platform()->keyboardKeyPressed(KEY_1, timestamp++); kwinApp()->platform()->keyboardKeyReleased(KEY_1, timestamp++); kwinApp()->platform()->keyboardKeyReleased(KEY_LEFTALT, timestamp++); kwinApp()->platform()->keyboardKeyReleased(KEY_LEFTCTRL, timestamp++); QVERIFY(clientUnminimizedSpy.wait()); QVERIFY(!client->isMinimized()); // One can also change the shortcut. client->setShortcut(QStringLiteral("Ctrl+Alt+2")); QCOMPARE(client->shortcut(), (QKeySequence{Qt::CTRL + Qt::ALT + Qt::Key_2})); client->minimize(); QVERIFY(client->isMinimized()); kwinApp()->platform()->keyboardKeyPressed(KEY_LEFTCTRL, timestamp++); kwinApp()->platform()->keyboardKeyPressed(KEY_LEFTALT, timestamp++); kwinApp()->platform()->keyboardKeyPressed(KEY_2, timestamp++); kwinApp()->platform()->keyboardKeyReleased(KEY_2, timestamp++); kwinApp()->platform()->keyboardKeyReleased(KEY_LEFTALT, timestamp++); kwinApp()->platform()->keyboardKeyReleased(KEY_LEFTCTRL, timestamp++); QVERIFY(clientUnminimizedSpy.wait()); QVERIFY(!client->isMinimized()); // The old shortcut should do nothing. client->minimize(); QVERIFY(client->isMinimized()); kwinApp()->platform()->keyboardKeyPressed(KEY_LEFTCTRL, timestamp++); kwinApp()->platform()->keyboardKeyPressed(KEY_LEFTALT, timestamp++); kwinApp()->platform()->keyboardKeyPressed(KEY_1, timestamp++); kwinApp()->platform()->keyboardKeyReleased(KEY_1, timestamp++); kwinApp()->platform()->keyboardKeyReleased(KEY_LEFTALT, timestamp++); kwinApp()->platform()->keyboardKeyReleased(KEY_LEFTCTRL, timestamp++); QVERIFY(!clientUnminimizedSpy.wait(100)); QVERIFY(client->isMinimized()); // Reopen the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); // The window shortcut should be set back to Ctrl+Alt+1. QCOMPARE(client->shortcut(), (QKeySequence{Qt::CTRL + Qt::ALT + Qt::Key_1})); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testShortcutRemember) void TestShellClientRules::testShortcutRemember() { QSKIP("KWin core doesn't try to save the last used window shortcut"); // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("shortcut", "Ctrl+Alt+1"); group.writeEntry("shortcutrule", int(Rules::Remember)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // Create the test client. QFETCH(Test::ShellSurfaceType, type); ShellClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); // If we press the window shortcut, the window should be brought back to user. QSignalSpy clientUnminimizedSpy(client, &AbstractClient::clientUnminimized); QVERIFY(clientUnminimizedSpy.isValid()); quint32 timestamp = 1; QCOMPARE(client->shortcut(), (QKeySequence{Qt::CTRL + Qt::ALT + Qt::Key_1})); client->minimize(); QVERIFY(client->isMinimized()); kwinApp()->platform()->keyboardKeyPressed(KEY_LEFTCTRL, timestamp++); kwinApp()->platform()->keyboardKeyPressed(KEY_LEFTALT, timestamp++); kwinApp()->platform()->keyboardKeyPressed(KEY_1, timestamp++); kwinApp()->platform()->keyboardKeyReleased(KEY_1, timestamp++); kwinApp()->platform()->keyboardKeyReleased(KEY_LEFTALT, timestamp++); kwinApp()->platform()->keyboardKeyReleased(KEY_LEFTCTRL, timestamp++); QVERIFY(clientUnminimizedSpy.wait()); QVERIFY(!client->isMinimized()); // Change the window shortcut to Ctrl+Alt+2. client->setShortcut(QStringLiteral("Ctrl+Alt+2")); QCOMPARE(client->shortcut(), (QKeySequence{Qt::CTRL + Qt::ALT + Qt::Key_2})); client->minimize(); QVERIFY(client->isMinimized()); kwinApp()->platform()->keyboardKeyPressed(KEY_LEFTCTRL, timestamp++); kwinApp()->platform()->keyboardKeyPressed(KEY_LEFTALT, timestamp++); kwinApp()->platform()->keyboardKeyPressed(KEY_2, timestamp++); kwinApp()->platform()->keyboardKeyReleased(KEY_2, timestamp++); kwinApp()->platform()->keyboardKeyReleased(KEY_LEFTALT, timestamp++); kwinApp()->platform()->keyboardKeyReleased(KEY_LEFTCTRL, timestamp++); QVERIFY(clientUnminimizedSpy.wait()); QVERIFY(!client->isMinimized()); // Reopen the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); // The window shortcut should be set to the last known value. QCOMPARE(client->shortcut(), (QKeySequence{Qt::CTRL + Qt::ALT + Qt::Key_2})); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testShortcutForce) void TestShellClientRules::testShortcutForce() { QSKIP("KWin core can't release forced window shortcuts"); // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("shortcut", "Ctrl+Alt+1"); group.writeEntry("shortcutrule", int(Rules::Force)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // Create the test client. QFETCH(Test::ShellSurfaceType, type); ShellClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); // If we press the window shortcut, the window should be brought back to user. QSignalSpy clientUnminimizedSpy(client, &AbstractClient::clientUnminimized); QVERIFY(clientUnminimizedSpy.isValid()); quint32 timestamp = 1; QCOMPARE(client->shortcut(), (QKeySequence{Qt::CTRL + Qt::ALT + Qt::Key_1})); client->minimize(); QVERIFY(client->isMinimized()); kwinApp()->platform()->keyboardKeyPressed(KEY_LEFTCTRL, timestamp++); kwinApp()->platform()->keyboardKeyPressed(KEY_LEFTALT, timestamp++); kwinApp()->platform()->keyboardKeyPressed(KEY_1, timestamp++); kwinApp()->platform()->keyboardKeyReleased(KEY_1, timestamp++); kwinApp()->platform()->keyboardKeyReleased(KEY_LEFTALT, timestamp++); kwinApp()->platform()->keyboardKeyReleased(KEY_LEFTCTRL, timestamp++); QVERIFY(clientUnminimizedSpy.wait()); QVERIFY(!client->isMinimized()); // Any attempt to change the window shortcut should not succeed. client->setShortcut(QStringLiteral("Ctrl+Alt+2")); QCOMPARE(client->shortcut(), (QKeySequence{Qt::CTRL + Qt::ALT + Qt::Key_1})); client->minimize(); QVERIFY(client->isMinimized()); kwinApp()->platform()->keyboardKeyPressed(KEY_LEFTCTRL, timestamp++); kwinApp()->platform()->keyboardKeyPressed(KEY_LEFTALT, timestamp++); kwinApp()->platform()->keyboardKeyPressed(KEY_2, timestamp++); kwinApp()->platform()->keyboardKeyReleased(KEY_2, timestamp++); kwinApp()->platform()->keyboardKeyReleased(KEY_LEFTALT, timestamp++); kwinApp()->platform()->keyboardKeyReleased(KEY_LEFTCTRL, timestamp++); QVERIFY(!clientUnminimizedSpy.wait(100)); QVERIFY(client->isMinimized()); // Reopen the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); // The window shortcut should still be forced. QCOMPARE(client->shortcut(), (QKeySequence{Qt::CTRL + Qt::ALT + Qt::Key_1})); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testShortcutApplyNow) void TestShellClientRules::testShortcutApplyNow() { // Create the test client. QFETCH(Test::ShellSurfaceType, type); ShellClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); QVERIFY(client->shortcut().isEmpty()); // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("shortcut", "Ctrl+Alt+1"); group.writeEntry("shortcutrule", int(Rules::ApplyNow)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // The client should now have a window shortcut assigned. QCOMPARE(client->shortcut(), (QKeySequence{Qt::CTRL + Qt::ALT + Qt::Key_1})); QSignalSpy clientUnminimizedSpy(client, &AbstractClient::clientUnminimized); QVERIFY(clientUnminimizedSpy.isValid()); quint32 timestamp = 1; client->minimize(); QVERIFY(client->isMinimized()); kwinApp()->platform()->keyboardKeyPressed(KEY_LEFTCTRL, timestamp++); kwinApp()->platform()->keyboardKeyPressed(KEY_LEFTALT, timestamp++); kwinApp()->platform()->keyboardKeyPressed(KEY_1, timestamp++); kwinApp()->platform()->keyboardKeyReleased(KEY_1, timestamp++); kwinApp()->platform()->keyboardKeyReleased(KEY_LEFTALT, timestamp++); kwinApp()->platform()->keyboardKeyReleased(KEY_LEFTCTRL, timestamp++); QVERIFY(clientUnminimizedSpy.wait()); QVERIFY(!client->isMinimized()); // Assign a different shortcut. client->setShortcut(QStringLiteral("Ctrl+Alt+2")); QCOMPARE(client->shortcut(), (QKeySequence{Qt::CTRL + Qt::ALT + Qt::Key_2})); client->minimize(); QVERIFY(client->isMinimized()); kwinApp()->platform()->keyboardKeyPressed(KEY_LEFTCTRL, timestamp++); kwinApp()->platform()->keyboardKeyPressed(KEY_LEFTALT, timestamp++); kwinApp()->platform()->keyboardKeyPressed(KEY_2, timestamp++); kwinApp()->platform()->keyboardKeyReleased(KEY_2, timestamp++); kwinApp()->platform()->keyboardKeyReleased(KEY_LEFTALT, timestamp++); kwinApp()->platform()->keyboardKeyReleased(KEY_LEFTCTRL, timestamp++); QVERIFY(clientUnminimizedSpy.wait()); QVERIFY(!client->isMinimized()); // The rule should not be applied again. client->evaluateWindowRules(); QCOMPARE(client->shortcut(), (QKeySequence{Qt::CTRL + Qt::ALT + Qt::Key_2})); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testShortcutForceTemporarily) void TestShellClientRules::testShortcutForceTemporarily() { QSKIP("KWin core can't release forced window shortcuts"); // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("shortcut", "Ctrl+Alt+1"); group.writeEntry("shortcutrule", int(Rules::ForceTemporarily)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // Create the test client. QFETCH(Test::ShellSurfaceType, type); ShellClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); // If we press the window shortcut, the window should be brought back to user. QSignalSpy clientUnminimizedSpy(client, &AbstractClient::clientUnminimized); QVERIFY(clientUnminimizedSpy.isValid()); quint32 timestamp = 1; QCOMPARE(client->shortcut(), (QKeySequence{Qt::CTRL + Qt::ALT + Qt::Key_1})); client->minimize(); QVERIFY(client->isMinimized()); kwinApp()->platform()->keyboardKeyPressed(KEY_LEFTCTRL, timestamp++); kwinApp()->platform()->keyboardKeyPressed(KEY_LEFTALT, timestamp++); kwinApp()->platform()->keyboardKeyPressed(KEY_1, timestamp++); kwinApp()->platform()->keyboardKeyReleased(KEY_1, timestamp++); kwinApp()->platform()->keyboardKeyReleased(KEY_LEFTALT, timestamp++); kwinApp()->platform()->keyboardKeyReleased(KEY_LEFTCTRL, timestamp++); QVERIFY(clientUnminimizedSpy.wait()); QVERIFY(!client->isMinimized()); // Any attempt to change the window shortcut should not succeed. client->setShortcut(QStringLiteral("Ctrl+Alt+2")); QCOMPARE(client->shortcut(), (QKeySequence{Qt::CTRL + Qt::ALT + Qt::Key_1})); client->minimize(); QVERIFY(client->isMinimized()); kwinApp()->platform()->keyboardKeyPressed(KEY_LEFTCTRL, timestamp++); kwinApp()->platform()->keyboardKeyPressed(KEY_LEFTALT, timestamp++); kwinApp()->platform()->keyboardKeyPressed(KEY_2, timestamp++); kwinApp()->platform()->keyboardKeyReleased(KEY_2, timestamp++); kwinApp()->platform()->keyboardKeyReleased(KEY_LEFTALT, timestamp++); kwinApp()->platform()->keyboardKeyReleased(KEY_LEFTCTRL, timestamp++); QVERIFY(!clientUnminimizedSpy.wait(100)); QVERIFY(client->isMinimized()); // The rule should be discarded when the client is closed. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); QVERIFY(client->shortcut().isEmpty()); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testDesktopFileDontAffect) void TestShellClientRules::testDesktopFileDontAffect() { // Currently, the desktop file name is derived from the app id. If the app id is // changed, then the old rules will be lost. Either setDesktopFileName should // be exposed or the desktop file name rule should be removed for wayland clients. QSKIP("Needs changes in KWin core to pass"); } TEST_DATA(testDesktopFileApply) void TestShellClientRules::testDesktopFileApply() { // Currently, the desktop file name is derived from the app id. If the app id is // changed, then the old rules will be lost. Either setDesktopFileName should // be exposed or the desktop file name rule should be removed for wayland clients. QSKIP("Needs changes in KWin core to pass"); } TEST_DATA(testDesktopFileRemember) void TestShellClientRules::testDesktopFileRemember() { // Currently, the desktop file name is derived from the app id. If the app id is // changed, then the old rules will be lost. Either setDesktopFileName should // be exposed or the desktop file name rule should be removed for wayland clients. QSKIP("Needs changes in KWin core to pass"); } TEST_DATA(testDesktopFileForce) void TestShellClientRules::testDesktopFileForce() { // Currently, the desktop file name is derived from the app id. If the app id is // changed, then the old rules will be lost. Either setDesktopFileName should // be exposed or the desktop file name rule should be removed for wayland clients. QSKIP("Needs changes in KWin core to pass"); } TEST_DATA(testDesktopFileApplyNow) void TestShellClientRules::testDesktopFileApplyNow() { // Currently, the desktop file name is derived from the app id. If the app id is // changed, then the old rules will be lost. Either setDesktopFileName should // be exposed or the desktop file name rule should be removed for wayland clients. QSKIP("Needs changes in KWin core to pass"); } TEST_DATA(testDesktopFileForceTemporarily) void TestShellClientRules::testDesktopFileForceTemporarily() { // Currently, the desktop file name is derived from the app id. If the app id is // changed, then the old rules will be lost. Either setDesktopFileName should // be exposed or the desktop file name rule should be removed for wayland clients. QSKIP("Needs changes in KWin core to pass"); } TEST_DATA(testActiveOpacityDontAffect) void TestShellClientRules::testActiveOpacityDontAffect() { // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("opacityactive", 90); group.writeEntry("opacityactiverule", int(Rules::DontAffect)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // Create the test client. QFETCH(Test::ShellSurfaceType, type); ShellClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); QVERIFY(client->isActive()); // The opacity should not be affected by the rule. QCOMPARE(client->opacity(), 1.0); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testActiveOpacityForce) void TestShellClientRules::testActiveOpacityForce() { // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("opacityactive", 90); group.writeEntry("opacityactiverule", int(Rules::Force)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // Create the test client. QFETCH(Test::ShellSurfaceType, type); ShellClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); QVERIFY(client->isActive()); QCOMPARE(client->opacity(), 0.9); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testActiveOpacityForceTemporarily) void TestShellClientRules::testActiveOpacityForceTemporarily() { // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("opacityactive", 90); group.writeEntry("opacityactiverule", int(Rules::ForceTemporarily)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // Create the test client. QFETCH(Test::ShellSurfaceType, type); ShellClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); QVERIFY(client->isActive()); QCOMPARE(client->opacity(), 0.9); // The rule should be discarded when the client is closed. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); QVERIFY(client->isActive()); QCOMPARE(client->opacity(), 1.0); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testInactiveOpacityDontAffect) void TestShellClientRules::testInactiveOpacityDontAffect() { // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("opacityinactive", 80); group.writeEntry("opacityinactiverule", int(Rules::DontAffect)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // Create the test client. QFETCH(Test::ShellSurfaceType, type); ShellClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); QVERIFY(client->isActive()); // Make the client inactive. workspace()->setActiveClient(nullptr); QVERIFY(!client->isActive()); // The opacity of the client should not be affected by the rule. QCOMPARE(client->opacity(), 1.0); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testInactiveOpacityForce) void TestShellClientRules::testInactiveOpacityForce() { // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("opacityinactive", 80); group.writeEntry("opacityinactiverule", int(Rules::Force)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // Create the test client. QFETCH(Test::ShellSurfaceType, type); ShellClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); QVERIFY(client->isActive()); QCOMPARE(client->opacity(), 1.0); // Make the client inactive. workspace()->setActiveClient(nullptr); QVERIFY(!client->isActive()); // The opacity should be forced by the rule. QCOMPARE(client->opacity(), 0.8); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testInactiveOpacityForceTemporarily) void TestShellClientRules::testInactiveOpacityForceTemporarily() { // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("opacityinactive", 80); group.writeEntry("opacityinactiverule", int(Rules::ForceTemporarily)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // Create the test client. QFETCH(Test::ShellSurfaceType, type); ShellClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); QVERIFY(client->isActive()); QCOMPARE(client->opacity(), 1.0); // Make the client inactive. workspace()->setActiveClient(nullptr); QVERIFY(!client->isActive()); // The opacity should be forced by the rule. QCOMPARE(client->opacity(), 0.8); // The rule should be discarded when the client is closed. delete shellSurface; delete surface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); QVERIFY(client->isActive()); QCOMPARE(client->opacity(), 1.0); workspace()->setActiveClient(nullptr); QVERIFY(!client->isActive()); QCOMPARE(client->opacity(), 1.0); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } void TestShellClientRules::testMatchAfterNameChange() { KSharedConfig::Ptr config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("above", true); group.writeEntry("aboverule", int(Rules::Force)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); QScopedPointer surface(Test::createSurface()); QScopedPointer shellSurface(Test::createXdgShellV6Surface(surface.data())); auto c = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); QVERIFY(c); QVERIFY(c->isActive()); QCOMPARE(c->keepAbove(), false); QSignalSpy desktopFileNameSpy(c, &AbstractClient::desktopFileNameChanged); QVERIFY(desktopFileNameSpy.isValid()); shellSurface->setAppId(QByteArrayLiteral("org.kde.foo")); QVERIFY(desktopFileNameSpy.wait()); QCOMPARE(c->keepAbove(), true); } WAYLANDTEST_MAIN(TestShellClientRules) #include "shell_client_rules_test.moc" diff --git a/shell_client.cpp b/shell_client.cpp index 3be1a7a14..5628707ea 100644 --- a/shell_client.cpp +++ b/shell_client.cpp @@ -1,1998 +1,2014 @@ /******************************************************************** KWin - the KDE window manager This file is part of the KDE project. Copyright (C) 2015 Martin Gräßlin Copyright (C) 2018 David Edmundson 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 "shell_client.h" #include "composite.h" #include "cursor.h" #include "deleted.h" #include "placement.h" #include "screenedge.h" #include "screens.h" #include "wayland_server.h" #include "workspace.h" #include "virtualdesktops.h" #include "screens.h" #include "decorations/decorationbridge.h" #include "decorations/decoratedclient.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include Q_DECLARE_METATYPE(NET::WindowType) using namespace KWayland::Server; namespace KWin { ShellClient::ShellClient(ShellSurfaceInterface *surface) : AbstractClient() , m_shellSurface(surface) , m_xdgShellSurface(nullptr) , m_xdgShellPopup(nullptr) , m_internal(surface->client() == waylandServer()->internalConnection()) { setSurface(surface->surface()); init(); m_isInitialized = true; } ShellClient::ShellClient(XdgShellSurfaceInterface *surface) : AbstractClient() , m_shellSurface(nullptr) , m_xdgShellSurface(surface) , m_xdgShellPopup(nullptr) , m_internal(surface->client() == waylandServer()->internalConnection()) { setSurface(surface->surface()); m_requestGeometryBlockCounter++; init(); connect(surface->surface(), &SurfaceInterface::committed, this, &ShellClient::finishInit); } ShellClient::ShellClient(XdgShellPopupInterface *surface) : AbstractClient() , m_shellSurface(nullptr) , m_xdgShellSurface(nullptr) , m_xdgShellPopup(surface) , m_internal(surface->client() == waylandServer()->internalConnection()) { setSurface(surface->surface()); m_requestGeometryBlockCounter++; init(); connect(surface->surface(), &SurfaceInterface::committed, this, &ShellClient::finishInit); } ShellClient::~ShellClient() = default; template void ShellClient::initSurface(T *shellSurface) { m_caption = shellSurface->title().simplified(); // delay till end of init QTimer::singleShot(0, this, &ShellClient::updateCaption); connect(shellSurface, &T::destroyed, this, &ShellClient::destroyClient); connect(shellSurface, &T::titleChanged, this, [this] (const QString &s) { const auto oldSuffix = m_captionSuffix; m_caption = s.simplified(); updateCaption(); if (m_captionSuffix == oldSuffix) { // don't emit caption change twice // it already got emitted by the changing suffix emit captionChanged(); } } ); connect(shellSurface, &T::moveRequested, this, [this] { // TODO: check the seat and serial performMouseCommand(Options::MouseMove, Cursor::pos()); } ); // determine the resource name, this is inspired from ICCCM 4.1.2.5 // the binary name of the invoked client QFileInfo info{shellSurface->client()->executablePath()}; QByteArray resourceName; if (info.exists()) { resourceName = info.fileName().toUtf8(); } setResourceClass(resourceName, shellSurface->windowClass()); setDesktopFileName(shellSurface->windowClass()); connect(shellSurface, &T::windowClassChanged, this, [this, resourceName] (const QByteArray &windowClass) { setResourceClass(resourceName, windowClass); if (m_isInitialized && supportsWindowRules()) { setupWindowRules(true); applyWindowRules(); } setDesktopFileName(windowClass); } ); connect(shellSurface, &T::resizeRequested, this, [this] (SeatInterface *seat, quint32 serial, Qt::Edges edges) { // TODO: check the seat and serial Q_UNUSED(seat) Q_UNUSED(serial) if (!isResizable() || isShade()) { return; } if (isMoveResize()) { finishMoveResize(false); } setMoveResizePointerButtonDown(true); setMoveOffset(Cursor::pos() - pos()); // map from global setInvertedMoveOffset(rect().bottomRight() - moveOffset()); setUnrestrictedMoveResize(false); auto toPosition = [edges] { Position pos = PositionCenter; if (edges.testFlag(Qt::TopEdge)) { pos = PositionTop; } else if (edges.testFlag(Qt::BottomEdge)) { pos = PositionBottom; } if (edges.testFlag(Qt::LeftEdge)) { pos = Position(pos | PositionLeft); } else if (edges.testFlag(Qt::RightEdge)) { pos = Position(pos | PositionRight); } return pos; }; setMoveResizePointerMode(toPosition()); if (!startMoveResize()) setMoveResizePointerButtonDown(false); updateCursor(); } ); connect(shellSurface, &T::maximizedChanged, this, [this] (bool maximized) { if (m_shellSurface && isFullScreen()) { // ignore for wl_shell - there it is mutual exclusive and messes with the geometry return; } + + // 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); + maximize(maximized ? MaximizeFull : MaximizeRestore); } ); // TODO: consider output! connect(shellSurface, &T::fullscreenChanged, this, &ShellClient::clientFullScreenChanged); connect(shellSurface, &T::transientForChanged, this, &ShellClient::setTransient); connect(this, &ShellClient::geometryChanged, this, &ShellClient::updateClientOutputs); connect(screens(), &Screens::changed, this, &ShellClient::updateClientOutputs); } void ShellClient::init() { connect(this, &ShellClient::desktopFileNameChanged, this, &ShellClient::updateIcon); createWindowId(); setupCompositing(); updateIcon(); SurfaceInterface *s = surface(); Q_ASSERT(s); if (s->buffer()) { setReadyForPainting(); if (shouldExposeToWindowManagement()) { setupWindowManagementInterface(); } m_unmapped = false; m_clientSize = s->size(); } else { ready_for_painting = false; } if (!m_internal) { doSetGeometry(QRect(QPoint(0, 0), m_clientSize)); } if (waylandServer()->inputMethodConnection() == s->client()) { m_windowType = NET::OnScreenDisplay; } connect(s, &SurfaceInterface::sizeChanged, this, [this] { m_clientSize = surface()->size(); doSetGeometry(QRect(geom.topLeft(), m_clientSize + QSize(borderLeft() + borderRight(), borderTop() + borderBottom()))); } ); connect(s, &SurfaceInterface::unmapped, this, &ShellClient::unmap); connect(s, &SurfaceInterface::unbound, this, &ShellClient::destroyClient); connect(s, &SurfaceInterface::destroyed, this, &ShellClient::destroyClient); if (m_shellSurface) { initSurface(m_shellSurface); auto setPopup = [this] { // TODO: verify grab serial m_hasPopupGrab = m_shellSurface->isPopup(); }; connect(m_shellSurface, &ShellSurfaceInterface::popupChanged, this, setPopup); setPopup(); } else if (m_xdgShellSurface) { initSurface(m_xdgShellSurface); auto global = static_cast(m_xdgShellSurface->global()); connect(global, &XdgShellInterface::pingDelayed, this, [this](qint32 serial) { auto it = m_pingSerials.find(serial); if (it != m_pingSerials.end()) { qCDebug(KWIN_CORE) << "First ping timeout:" << caption(); setUnresponsive(true); } }); connect(m_xdgShellSurface, &XdgShellSurfaceInterface::configureAcknowledged, this, [this](int serial) { m_lastAckedConfigureRequest = serial; }); connect(global, &XdgShellInterface::pingTimeout, this, [this](qint32 serial) { 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); } }); connect(global, &XdgShellInterface::pongReceived, this, [this](qint32 serial){ auto it = m_pingSerials.find(serial); if (it != m_pingSerials.end()) { setUnresponsive(false); m_pingSerials.erase(it); } }); connect(m_xdgShellSurface, &XdgShellSurfaceInterface::windowMenuRequested, this, [this] (SeatInterface *seat, quint32 serial, const QPoint &surfacePos) { // TODO: check serial on seat Q_UNUSED(seat) Q_UNUSED(serial) performMouseCommand(Options::MouseOperationsMenu, pos() + surfacePos); } ); connect(m_xdgShellSurface, &XdgShellSurfaceInterface::minimizeRequested, this, [this] { performMouseCommand(Options::MouseMinimize, Cursor::pos()); } ); auto configure = [this] { if (m_closing) { return; } if (m_requestGeometryBlockCounter != 0 || areGeometryUpdatesBlocked()) { return; } m_xdgShellSurface->configure(xdgSurfaceStates(), m_requestedClientSize); }; connect(this, &AbstractClient::activeChanged, this, configure); connect(this, &AbstractClient::clientStartUserMovedResized, this, configure); connect(this, &AbstractClient::clientFinishUserMovedResized, this, configure); } else if (m_xdgShellPopup) { connect(m_xdgShellPopup, &XdgShellPopupInterface::grabRequested, this, [this](SeatInterface *seat, quint32 serial) { Q_UNUSED(seat) Q_UNUSED(serial) //TODO - should check the parent had focus m_hasPopupGrab = true; }); connect(m_xdgShellPopup, &XdgShellPopupInterface::configureAcknowledged, this, [this](int serial) { m_lastAckedConfigureRequest = serial; }); connect(m_xdgShellPopup, &XdgShellPopupInterface::destroyed, this, &ShellClient::destroyClient); } // set initial desktop setDesktop(m_internal ? int(NET::OnAllDesktops) : VirtualDesktopManager::self()->current()); // setup shadow integration getShadow(); connect(s, &SurfaceInterface::shadowChanged, this, &Toplevel::getShadow); connect(waylandServer(), &WaylandServer::foreignTransientChanged, this, [this](KWayland::Server::SurfaceInterface *child) { if (child == surface()) { setTransient(); } }); setTransient(); AbstractClient::updateColorScheme(QString()); } void ShellClient::finishInit() { SurfaceInterface *s = surface(); disconnect(s, &SurfaceInterface::committed, this, &ShellClient::finishInit); updateWindowMargins(); bool needsPlacement = !isInitialPositionSet(); if (supportsWindowRules()) { setupWindowRules(false); const QRect originalGeometry = QRect(pos(), sizeForClientSize(clientSize())); const QRect ruledGeometry = rules()->checkGeometry(originalGeometry, true); if (originalGeometry != ruledGeometry) { setGeometry(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 (needsPlacement) { QRect area = workspace()->clientArea(PlacementArea, Screens::self()->current(), desktop()); placeIn(area); } m_requestGeometryBlockCounter--; if (m_requestGeometryBlockCounter == 0) { requestGeometry(m_blockedRequestGeometry); } m_isInitialized = true; } void ShellClient::destroyClient() { m_closing = true; if (isMoveResize()) { leaveMoveResize(); } Deleted *del = nullptr; if (workspace()) { del = Deleted::create(this); } emit windowClosed(this, del); // Remove Force Temporarily rules. RuleBook::self()->discardUsed(this, true); destroyWindowManagementInterface(); destroyDecoration(); if (workspace()) { 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; } } } waylandServer()->removeClient(this); if (del) { del->unrefWindow(); } m_shellSurface = nullptr; m_xdgShellSurface = nullptr; m_xdgShellPopup = nullptr; deleteClient(this); } void ShellClient::deleteClient(ShellClient *c) { delete c; } QSize ShellClient::toWindowGeometry(const QSize &size) const { QSize adjustedSize = size - QSize(borderLeft() + borderRight(), borderTop() + borderBottom()); // a client going fullscreen should have the window the contents size of the screen if (!isFullScreen() && requestedMaximizeMode() != MaximizeFull) { adjustedSize -= QSize(m_windowMargins.left() + m_windowMargins.right(), m_windowMargins.top() + m_windowMargins.bottom()); } return adjustedSize; } QStringList ShellClient::activities() const { // TODO: implement return QStringList(); } QPoint ShellClient::clientContentPos() const { return -1 * clientPos(); } QSize ShellClient::clientSize() const { return m_clientSize; } void ShellClient::debug(QDebug &stream) const { stream.nospace(); stream << "\'ShellClient:" << surface() << ";WMCLASS:" << resourceClass() << ":" << resourceName() << ";Caption:" << caption() << "\'"; } bool ShellClient::belongsToDesktop() const { const auto clients = waylandServer()->clients(); return std::any_of(clients.constBegin(), clients.constEnd(), [this](const ShellClient *client) { if (belongsToSameApplication(client, SameApplicationChecks())) { return client->isDesktop(); } return false; } ); } Layer ShellClient::layerForDock() 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(); } QRect ShellClient::transparentRect() const { // TODO: implement return QRect(); } NET::WindowType ShellClient::windowType(bool direct, int supported_types) const { // TODO: implement Q_UNUSED(direct) Q_UNUSED(supported_types) return m_windowType; } double ShellClient::opacity() const { return m_opacity; } void ShellClient::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); } void ShellClient::addDamage(const QRegion &damage) { auto s = surface(); if (s->size().isValid()) { m_clientSize = s->size(); updateWindowMargins(); updatePendingGeometry(); } markAsMapped(); setDepth((s->buffer()->hasAlphaChannel() && !isDesktop()) ? 32 : 24); repaints_region += damage.translated(clientPos()); Toplevel::addDamage(damage); } void ShellClient::markAsMapped() { if (!m_unmapped) { return; } m_unmapped = false; if (!ready_for_painting) { setReadyForPainting(); } else { addRepaintFull(); emit windowShown(this); } if (shouldExposeToWindowManagement()) { setupWindowManagementInterface(); } updateShowOnScreenEdge(); } void ShellClient::createDecoration(const QRect &oldGeom) { KDecoration2::Decoration *decoration = Decoration::DecorationBridge::self()->createDecoration(this); if (decoration) { QMetaObject::invokeMethod(decoration, "update", Qt::QueuedConnection); connect(decoration, &KDecoration2::Decoration::shadowChanged, this, &Toplevel::getShadow); connect(decoration, &KDecoration2::Decoration::bordersChanged, this, [this]() { GeometryUpdatesBlocker blocker(this); RequestGeometryBlocker requestBlocker(this); QRect oldgeom = geometry(); if (!isShade()) checkWorkspacePosition(oldgeom); emit geometryShapeChanged(this, oldgeom); } ); } setDecoration(decoration); // TODO: ensure the new geometry still fits into the client area (e.g. maximized windows) doSetGeometry(QRect(oldGeom.topLeft(), m_clientSize + (decoration ? QSize(decoration->borderLeft() + decoration->borderRight(), decoration->borderBottom() + decoration->borderTop()) : QSize()))); emit geometryShapeChanged(this, oldGeom); } void ShellClient::updateDecoration(bool check_workspace_pos, bool force) { if (!force && ((!isDecorated() && noBorder()) || (isDecorated() && !noBorder()))) return; QRect oldgeom = geometry(); 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(KWayland::Server::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_xdgShellSurface->configure(xdgSurfaceStates(), m_requestedClientSize); } } getShadow(); if (check_workspace_pos) checkWorkspacePosition(oldgeom, -2, oldClientGeom); blockGeometryUpdates(false); } void ShellClient::setGeometry(int x, int y, int w, int h, ForceGeometry_t force) { const QRect newGeometry = rules()->checkGeometry(QRect(x, y, w, h)); if (areGeometryUpdatesBlocked()) { // when the GeometryUpdateBlocker exits the current geom is passed to setGeometry // thus we need to set it here. geom = newGeometry; if (pendingGeometryUpdate() == PendingGeometryForced) {} // maximum, nothing needed else if (force == ForceGeometrySet) setPendingGeometryUpdate(PendingGeometryForced); else setPendingGeometryUpdate(PendingGeometryNormal); return; } if (pendingGeometryUpdate() != PendingGeometryNone) { // reset geometry to the one before blocking, so that we can compare properly geom = geometryBeforeUpdateBlocking(); } const QSize requestedClientSize = newGeometry.size() - QSize(borderLeft() + borderRight(), borderTop() + borderBottom()); const QSize requestedWindowGeometrySize = toWindowGeometry(newGeometry.size()); if (requestedClientSize == m_clientSize && !isWaitingForMoveResizeSync() && (m_requestedClientSize.isEmpty() || requestedWindowGeometrySize == 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); } } void ShellClient::doSetGeometry(const QRect &rect) { if (geom == rect && pendingGeometryUpdate() == PendingGeometryNone) { return; } if (!m_unmapped) { addWorkspaceRepaint(visibleRect()); } geom = rect; updateWindowRules(Rules::Position | Rules::Size); if (m_unmapped && m_geomMaximizeRestore.isEmpty() && !geom.isEmpty()) { // use first valid geometry as restore geometry m_geomMaximizeRestore = geom; } if (!m_unmapped) { addWorkspaceRepaint(visibleRect()); } if (hasStrut()) { workspace()->updateClientArea(); } const auto old = geometryBeforeUpdateBlocking(); updateGeometryBeforeUpdateBlocking(); emit geometryShapeChanged(this, old); if (isResize()) { performMoveResize(); } } QByteArray ShellClient::windowRole() const { return QByteArray(); } bool ShellClient::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; } void ShellClient::blockActivityUpdates(bool b) { Q_UNUSED(b) } void ShellClient::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 ShellClient::closeWindow() { if (m_xdgShellSurface && isCloseable()) { m_xdgShellSurface->close(); const qint32 pingSerial = static_cast(m_xdgShellSurface->global())->ping(m_xdgShellSurface); m_pingSerials.insert(pingSerial, PingReason::CloseWindow); } } AbstractClient *ShellClient::findModal(bool allow_itself) { Q_UNUSED(allow_itself) return nullptr; } bool ShellClient::isCloseable() const { if (m_windowType == NET::Desktop || m_windowType == NET::Dock) { return false; } if (m_xdgShellSurface) { return true; } return false; } bool ShellClient::isFullScreen() const { return m_fullScreen; } bool ShellClient::isMaximizable() const { if (!isResizable()) { return false; } + if (rules()->checkMaximize(MaximizeRestore) != MaximizeRestore || rules()->checkMaximize(MaximizeFull) != MaximizeFull) { + return false; + } return true; } bool ShellClient::isMinimizable() const { if (!rules()->checkMinimize(true)) { return false; } return (!m_plasmaShellSurface || m_plasmaShellSurface->role() == PlasmaShellSurfaceInterface::Role::Normal); } bool ShellClient::isMovable() 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; } bool ShellClient::isMovableAcrossScreens() 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; } bool ShellClient::isResizable() const { if (rules()->checkSize(QSize()).isValid()) { return false; } if (m_plasmaShellSurface) { return m_plasmaShellSurface->role() == PlasmaShellSurfaceInterface::Role::Normal; } if (m_xdgShellPopup) { return false; } return true; } bool ShellClient::isShown(bool shaded_is_shown) const { Q_UNUSED(shaded_is_shown) return !m_closing && !m_unmapped && !isMinimized() && !m_hidden; } void ShellClient::hideClient(bool hide) { if (m_hidden == hide) { return; } m_hidden = hide; if (hide) { addWorkspaceRepaint(visibleRect()); workspace()->clientHidden(this); emit windowHidden(this); } else { emit windowShown(this); } } static bool changeMaximizeRecursion = false; void ShellClient::changeMaximize(bool horizontal, bool vertical, bool adjust) { if (changeMaximizeRecursion) { return; } if (!isResizable()) { return; } const QRect clientArea = isElectricBorderMaximizing() ? workspace()->clientArea(MaximizeArea, Cursor::pos(), desktop()) : workspace()->clientArea(MaximizeArea, this); const MaximizeMode oldMode = m_requestedMaximizeMode; const QRect oldGeometry = geometry(); // '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); } - // TODO: add more checks as in Client - if (m_requestedMaximizeMode == oldMode) { + m_requestedMaximizeMode = rules()->checkMaximize(m_requestedMaximizeMode); + if (!adjust && m_requestedMaximizeMode == oldMode) { return; } StackingUpdatesBlocker blocker(workspace()); RequestGeometryBlocker geometryBlocker(this); // call into decoration update borders if (isDecorated() && decoration()->client() && !(options->borderlessMaximizedWindows() && m_requestedMaximizeMode == KWin::MaximizeFull)) { changeMaximizeRecursion = true; const auto c = decoration()->client().data(); 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(m_geomMaximizeRestore.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 } } - // TODO: check rules if (m_requestedMaximizeMode == MaximizeFull) { m_geomMaximizeRestore = oldGeometry; // TODO: Client has more checks if (options->electricBorderMaximize()) { updateQuickTileMode(QuickTileFlag::Maximize); } else { updateQuickTileMode(QuickTileFlag::None); } if (quickTileMode() != oldQuickTileMode) { emit quickTileModeChanged(); } setGeometry(workspace()->clientArea(MaximizeArea, this)); workspace()->raiseClient(this); } else { if (m_requestedMaximizeMode == MaximizeRestore) { updateQuickTileMode(QuickTileFlag::None); } if (quickTileMode() != oldQuickTileMode) { emit quickTileModeChanged(); } if (m_geomMaximizeRestore.isValid()) { setGeometry(m_geomMaximizeRestore); } else { setGeometry(workspace()->clientArea(PlacementArea, this)); } } } MaximizeMode ShellClient::maximizeMode() const { return m_maximizeMode; } MaximizeMode ShellClient::requestedMaximizeMode() const { return m_requestedMaximizeMode; } bool ShellClient::noBorder() const { if (m_serverDecoration) { if (m_serverDecoration->mode() == ServerSideDecorationManagerInterface::Mode::Server) { return m_userNoBorder || isFullScreen(); } } if (m_xdgDecoration && m_xdgDecoration->requestedMode() != XdgDecorationInterface::Mode::ClientSide) { return m_userNoBorder || isFullScreen(); } return true; } bool ShellClient::isFullScreenable() const { if (!rules()->checkFullScreen(true)) { return false; } return !isSpecialWindow(); } void ShellClient::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(Cursor::pos()); // may cause leave event } else { // in shell surface, maximise mode and fullscreen are exclusive // fullscreen->toplevel should restore the state we had before maximising if (m_shellSurface && m_maximizeMode == MaximizeMode::MaximizeFull) { m_geomFsRestore = m_geomMaximizeRestore; } else { m_geomFsRestore = geometry(); } } m_fullScreen = set; if (set) { untab(); workspace()->raiseClient(this); } RequestGeometryBlocker requestBlocker(this); StackingUpdatesBlocker blocker1(workspace()); GeometryUpdatesBlocker blocker2(this); workspace()->updateClientLayer(this); // active fullscreens get different layer updateDecoration(false, false); if (set) { setGeometry(workspace()->clientArea(FullScreenArea, this)); } else { if (m_geomFsRestore.isValid()) { int currentScreen = screen(); setGeometry(QRect(m_geomFsRestore.topLeft(), adjustedSize(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 setGeometry(QRect(workspace()->clientArea(PlacementArea, this).topLeft(), QSize(0, 0))); } } updateWindowRules(Rules::Fullscreen|Rules::Position|Rules::Size); emit fullScreenChanged(); } void ShellClient::setNoBorder(bool set) { if (!userCanSetNoBorder()) { return; } set = rules()->checkNoBorder(set); if (m_userNoBorder == set) { return; } m_userNoBorder = set; updateDecoration(true, false); updateWindowRules(Rules::NoBorder); } void ShellClient::setOnAllActivities(bool set) { Q_UNUSED(set) } void ShellClient::takeFocus() { if (rules()->checkAcceptFocus(wantsInput())) { if (m_xdgShellSurface) { const qint32 pingSerial = static_cast(m_xdgShellSurface->global())->ping(m_xdgShellSurface); m_pingSerials.insert(pingSerial, PingReason::FocusWindow); } setActive(true); } if (!keepAbove() && !isOnScreenDisplay() && !belongsToDesktop()) { workspace()->setShowingDesktop(false); } } void ShellClient::doSetActive() { if (!isActive()) { return; } StackingUpdatesBlocker blocker(workspace()); workspace()->focusToNull(); } bool ShellClient::userCanSetFullScreen() const { if (m_xdgShellSurface) { return true; } return false; } bool ShellClient::userCanSetNoBorder() const { if (m_serverDecoration && m_serverDecoration->mode() == ServerSideDecorationManagerInterface::Mode::Server) { return !isFullScreen() && !isShade() && !tabGroup(); } if (m_xdgDecoration && m_xdgDecoration->requestedMode() != XdgDecorationInterface::Mode::ClientSide) { return !isFullScreen() && !isShade() && !tabGroup(); } return false; } bool ShellClient::wantsInput() const { return rules()->checkAcceptFocus(acceptsFocus()); } bool ShellClient::acceptsFocus() const { if (waylandServer()->inputMethodConnection() == surface()->client()) { return false; } if (m_plasmaShellSurface) { if (m_plasmaShellSurface->role() == PlasmaShellSurfaceInterface::Role::OnScreenDisplay || m_plasmaShellSurface->role() == PlasmaShellSurfaceInterface::Role::ToolTip || m_plasmaShellSurface->role() == PlasmaShellSurfaceInterface::Role::Notification || m_plasmaShellSurface->role() == PlasmaShellSurfaceInterface::Role::CriticalNotification) { return false; } } 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_shellSurface) { if (m_shellSurface->isPopup()) { return false; } return m_shellSurface->acceptsKeyboardFocus(); } if (m_xdgShellSurface) { // TODO: proper return true; } return false; } void ShellClient::createWindowId() { if (!m_internal) { m_windowId = waylandServer()->createWindowId(surface()); } } pid_t ShellClient::pid() const { return surface()->client()->processId(); } bool ShellClient::isLockScreen() const { return surface()->client() == waylandServer()->screenLockerClientConnection(); } bool ShellClient::isInputMethod() const { return surface()->client() == waylandServer()->inputMethodConnection(); } bool ShellClient::requestGeometry(const QRect &rect) { if (m_requestGeometryBlockCounter != 0) { m_blockedRequestGeometry = rect; return false; } QSize size; if (rect.isValid()) { size = toWindowGeometry(rect.size()); } else { size = QSize(0, 0); } m_requestedClientSize = size; quint64 serialId = 0; if (m_shellSurface && !size.isEmpty()) { m_shellSurface->requestSize(size); } if (m_xdgShellSurface) { serialId = m_xdgShellSurface->configure(xdgSurfaceStates(), size); } if (m_xdgShellPopup) { auto parent = transientFor(); if (parent) { const QPoint globalClientContentPos = parent->geometry().topLeft() + parent->clientPos(); const QPoint relativeOffset = rect.topLeft() - globalClientContentPos; serialId = m_xdgShellPopup->configure(QRect(relativeOffset, size)); } } 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); } m_blockedRequestGeometry = QRect(); return true; } void ShellClient::updatePendingGeometry() { QPoint position = geom.topLeft(); 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(geometry()); } position = it->positionAfterResize; maximizeMode = it->maximizeMode; m_pendingConfigureRequests.erase(m_pendingConfigureRequests.begin(), ++it); break; } //else serialId < m_lastAckedConfigureRequest and the state is now irrelevant and can be ignored } doSetGeometry(QRect(position, m_clientSize + QSize(borderLeft() + borderRight(), borderTop() + borderBottom()))); updateMaximizeMode(maximizeMode); } void ShellClient::clientFullScreenChanged(bool fullScreen) { setFullScreen(fullScreen, false); } void ShellClient::resizeWithChecks(int w, int h, ForceGeometry_t force) { Q_UNUSED(force) QRect area = workspace()->clientArea(WorkArea, this); // don't allow growing larger than workarea if (w > area.width()) { w = area.width(); } if (h > area.height()) { h = area.height(); } if (m_shellSurface) { m_shellSurface->requestSize(QSize(w, h)); } if (m_xdgShellSurface) { m_xdgShellSurface->configure(xdgSurfaceStates(), QSize(w, h)); } } void ShellClient::unmap() { m_unmapped = true; if (isMoveResize()) { leaveMoveResize(); } m_requestedClientSize = QSize(0, 0); destroyWindowManagementInterface(); if (Workspace::self()) { addWorkspaceRepaint(visibleRect()); workspace()->clientHidden(this); } emit windowHidden(this); } void ShellClient::installPlasmaShellSurface(PlasmaShellSurfaceInterface *surface) { m_plasmaShellSurface = surface; auto updatePosition = [this, surface] { QRect rect = QRect(surface->position(), m_clientSize + QSize(borderLeft() + borderRight(), borderTop() + borderBottom())); // Shell surfaces of internal windows are sometimes desync to current value. // Make sure to not set window geometry of internal windows to invalid values (bug 386304). // This is a workaround. if (!m_internal || rect.isValid()) { doSetGeometry(rect); } }; auto updateRole = [this, surface] { NET::WindowType type = NET::Unknown; switch (surface->role()) { case PlasmaShellSurfaceInterface::Role::Desktop: type = NET::Desktop; break; case PlasmaShellSurfaceInterface::Role::Panel: type = NET::Dock; break; case PlasmaShellSurfaceInterface::Role::OnScreenDisplay: type = NET::OnScreenDisplay; break; case PlasmaShellSurfaceInterface::Role::Notification: type = NET::Notification; break; case PlasmaShellSurfaceInterface::Role::ToolTip: type = NET::Tooltip; break; case PlasmaShellSurfaceInterface::Role::CriticalNotification: type = NET::CriticalNotification; break; case PlasmaShellSurfaceInterface::Role::Normal: default: 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(); } }; 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(); } ); updatePosition(); updateRole(); updateShowOnScreenEdge(); connect(this, &ShellClient::geometryChanged, this, &ShellClient::updateShowOnScreenEdge); setSkipTaskbar(surface->skipTaskbar()); connect(surface, &PlasmaShellSurfaceInterface::skipTaskbarChanged, this, [this] { setSkipTaskbar(m_plasmaShellSurface->skipTaskbar()); }); setSkipSwitcher(surface->skipSwitcher()); connect(surface, &PlasmaShellSurfaceInterface::skipSwitcherChanged, this, [this] { setSkipSwitcher(m_plasmaShellSurface->skipSwitcher()); }); } void ShellClient::updateShowOnScreenEdge() { if (!ScreenEdges::self()) { return; } if (m_unmapped || !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 Qt::Edges edges; for (int i = 0; i < screens()->count(); i++) { const auto &screenGeo = screens()->geometry(i); if (screenGeo.x() == geom.x()) { edges |= Qt::LeftEdge; } if (screenGeo.x() + screenGeo.width() == geom.x() + geom.width()) { edges |= Qt::RightEdge; } if (screenGeo.y() == geom.y()) { edges |= Qt::TopEdge; } if (screenGeo.y() + screenGeo.height() == geom.y() + geom.height()) { 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)) { edges = edges & (~(Qt::LeftEdge | Qt::RightEdge)); } if (edges.testFlag(Qt::TopEdge) && edges.testFlag(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 = [this](Qt::Edges edges, Qt::Edge horiz, Qt::Edge vert) { if (edges.testFlag(horiz) && edges.testFlag(vert)) { if (geom.width() >= geom.height()) { return edges & ~horiz; } else { return edges & ~vert; } } return edges; }; edges = check(edges, Qt::LeftEdge, Qt::TopEdge); edges = check(edges, Qt::LeftEdge, Qt::BottomEdge); edges = check(edges, Qt::RightEdge, Qt::TopEdge); edges = check(edges, Qt::RightEdge, Qt::BottomEdge); ElectricBorder border = ElectricNone; if (edges.testFlag(Qt::LeftEdge)) { border = ElectricLeft; } if (edges.testFlag(Qt::RightEdge)) { border = ElectricRight; } if (edges.testFlag(Qt::TopEdge)) { border = ElectricTop; } if (edges.testFlag(Qt::BottomEdge)) { border = ElectricBottom; } ScreenEdges::self()->reserve(this, border); } else { ScreenEdges::self()->reserve(this, ElectricNone); } } bool ShellClient::isInitialPositionSet() const { if (m_plasmaShellSurface) { return m_plasmaShellSurface->isPositionSet(); } return false; } void ShellClient::installAppMenu(AppMenuInterface *menu) { m_appMenuInterface = menu; 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 ShellClient::installPalette(ServerSideDecorationPaletteInterface *palette) { 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 ShellClient::updateColorScheme() { if (m_paletteInterface) { AbstractClient::updateColorScheme(rules()->checkDecoColor(m_paletteInterface->palette())); } else { AbstractClient::updateColorScheme(rules()->checkDecoColor(QString())); } } void ShellClient::updateMaximizeMode(MaximizeMode maximizeMode) { 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); } bool ShellClient::hasStrut() 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; } void ShellClient::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)); } bool ShellClient::isTransient() const { return m_transient; } void ShellClient::setTransient() { SurfaceInterface *s = nullptr; if (m_shellSurface) { s = m_shellSurface->transientFor().data(); } if (m_xdgShellSurface) { if (auto transient = m_xdgShellSurface->transientFor().data()) { s = transient->surface(); } } if (m_xdgShellPopup) { s = m_xdgShellPopup->transientFor().data(); } if (!s) { s = waylandServer()->findForeignTransientForSurface(surface()); } auto t = waylandServer()->findClient(s); if (t != transientFor()) { // remove from main client if (transientFor()) transientFor()->removeTransient(this); setTransientFor(t); if (t) { t->addTransient(this); } } m_transient = (s != nullptr); } bool ShellClient::hasTransientPlacementHint() const { return isTransient() && transientFor() != nullptr && (m_shellSurface || m_xdgShellPopup); } QRect ShellClient::transientPlacement(const QRect &bounds) const { QRect anchorRect; Qt::Edges anchorEdge; Qt::Edges gravity; QPoint offset; PositionerConstraints constraintAdjustments; QSize size = geometry().size(); const QPoint parentClientPos = transientFor()->pos() + transientFor()->clientPos(); QRect popupPosition; // 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 { if (edges & Qt::LeftEdge && target.left() < bounds.left()) { return false; } if (edges & Qt::TopEdge && target.top() < bounds.top()) { return false; } if (edges & Qt::RightEdge && target.right() > bounds.right()) { //normal QRect::right issue cancels out return false; } if (edges & Qt::BottomEdge && target.bottom() > bounds.bottom()) { return false; } return true; }; if (m_shellSurface) { anchorRect = QRect(m_shellSurface->transientOffset(), QSize(1,1)); anchorEdge = Qt::TopEdge | Qt::LeftEdge; gravity = Qt::BottomEdge | Qt::RightEdge; //our single point represents the top left of the popup constraintAdjustments = (PositionerConstraint::SlideX | PositionerConstraint::SlideY); } else if (m_xdgShellPopup) { 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(); } } else { Q_UNREACHABLE(); } //initial position popupPosition = QRect(popupOffset(anchorRect, anchorEdge, gravity, size) + offset + parentClientPos, size); //if that fits, we don't need to do anything if (inBounds(popupPosition)) { return popupPosition; } //otherwise apply constraint adjustment per axis in order XDG Shell Popup states if (constraintAdjustments & PositionerConstraint::FlipX) { if (!inBounds(popupPosition, Qt::LeftEdge | Qt::RightEdge)) { //flip both edges (if either bit is set, XOR both) auto flippedAnchorEdge = anchorEdge; if (flippedAnchorEdge & (Qt::LeftEdge | Qt::RightEdge)) { flippedAnchorEdge ^= (Qt::LeftEdge | Qt::RightEdge); } auto flippedGravity = gravity; if (flippedGravity & (Qt::LeftEdge | Qt::RightEdge)) { flippedGravity ^= (Qt::LeftEdge | Qt::RightEdge); } auto flippedPopupPosition = QRect(popupOffset(anchorRect, flippedAnchorEdge, flippedGravity, size) + offset + parentClientPos, size); //if it still doesn't fit we should continue with the unflipped version if (inBounds(flippedPopupPosition, Qt::LeftEdge | Qt::RightEdge)) { popupPosition.moveLeft(flippedPopupPosition.x()); } } } if (constraintAdjustments & PositionerConstraint::SlideX) { if (!inBounds(popupPosition, Qt::LeftEdge)) { popupPosition.moveLeft(bounds.x()); } if (!inBounds(popupPosition, Qt::RightEdge)) { // moveRight suffers from the classic QRect off by one issue popupPosition.moveLeft(bounds.x() + bounds.width() - size.width()); } } if (constraintAdjustments & PositionerConstraint::ResizeX) { //TODO //but we need to sort out when this is run as resize should only happen before first configure } if (constraintAdjustments & PositionerConstraint::FlipY) { if (!inBounds(popupPosition, Qt::TopEdge | Qt::BottomEdge)) { //flip both edges (if either bit is set, XOR both) auto flippedAnchorEdge = anchorEdge; if (flippedAnchorEdge & (Qt::TopEdge | Qt::BottomEdge)) { flippedAnchorEdge ^= (Qt::TopEdge | Qt::BottomEdge); } auto flippedGravity = gravity; if (flippedGravity & (Qt::TopEdge | Qt::BottomEdge)) { flippedGravity ^= (Qt::TopEdge | Qt::BottomEdge); } auto flippedPopupPosition = QRect(popupOffset(anchorRect, flippedAnchorEdge, flippedGravity, size) + offset + parentClientPos, size); //if it still doesn't fit we should continue with the unflipped version if (inBounds(flippedPopupPosition, Qt::TopEdge | Qt::BottomEdge)) { popupPosition.moveTop(flippedPopupPosition.y()); } } } if (constraintAdjustments & PositionerConstraint::SlideY) { if (!inBounds(popupPosition, Qt::TopEdge)) { popupPosition.moveTop(bounds.y()); } if (!inBounds(popupPosition, Qt::BottomEdge)) { popupPosition.moveTop(bounds.y() + bounds.height() - size.height()); } } if (constraintAdjustments & PositionerConstraint::ResizeY) { //TODO } return popupPosition; } QPoint ShellClient::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; } bool ShellClient::isWaitingForMoveResizeSync() const { if (m_shellSurface) { return !m_pendingConfigureRequests.isEmpty(); } return false; } void ShellClient::doResizeSync() { requestGeometry(moveResizeGeometry()); } QMatrix4x4 ShellClient::inputTransformation() const { QMatrix4x4 m = Toplevel::inputTransformation(); m.translate(-borderLeft(), -borderTop()); return m; } void ShellClient::installServerSideDecoration(KWayland::Server::ServerSideDecorationInterface *deco) { 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 ShellClient 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 ShellClient::installXdgDecoration(XdgDecorationInterface *deco) { Q_ASSERT(m_xdgShellSurface); 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); }); } bool ShellClient::shouldExposeToWindowManagement() { if (m_internal) { return false; } if (isLockScreen()) { return false; } if (m_xdgShellPopup) { return false; } if (m_shellSurface) { if (m_shellSurface->isTransient() && !m_shellSurface->acceptsKeyboardFocus()) { return false; } } return true; } KWayland::Server::XdgShellSurfaceInterface::States ShellClient::xdgSurfaceStates() 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; } void ShellClient::doMinimize() { if (isMinimized()) { workspace()->clientHidden(this); } else { emit windowShown(this); } workspace()->updateMinimizedOfTransients(this); } bool ShellClient::setupCompositing() { if (m_compositingSetup) { return true; } m_compositingSetup = Toplevel::setupCompositing(); return m_compositingSetup; } void ShellClient::finishCompositing(ReleaseReason releaseReason) { m_compositingSetup = false; Toplevel::finishCompositing(releaseReason); } void ShellClient::placeIn(QRect &area) { Placement::self()->place(this, area); setGeometryRestore(geometry()); } void ShellClient::showOnScreenEdge() { if (!m_plasmaShellSurface || m_unmapped) { return; } hideClient(false); workspace()->raiseClient(this); if (m_plasmaShellSurface->panelBehavior() == PlasmaShellSurfaceInterface::PanelBehavior::AutoHide) { m_plasmaShellSurface->showAutoHidingPanel(); } } bool ShellClient::dockWantsInput() const { if (m_plasmaShellSurface) { if (m_plasmaShellSurface->role() == PlasmaShellSurfaceInterface::Role::Panel) { return m_plasmaShellSurface->panelTakesFocus(); } } return false; } void ShellClient::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); } bool ShellClient::hasPopupGrab() const { return m_hasPopupGrab; } void ShellClient::popupDone() { if (m_shellSurface) { m_shellSurface->popupDone(); } if (m_xdgShellPopup) { m_xdgShellPopup->popupDone(); } } void ShellClient::updateClientOutputs() { QVector clientOutputs; const auto outputs = waylandServer()->display()->outputs(); for (OutputInterface* output: qAsConst(outputs)) { const QRect outputGeom(output->globalPosition(), output->pixelSize() / output->scale()); if (geometry().intersects(outputGeom)) { clientOutputs << output; } } surface()->setOutputs(clientOutputs); } void ShellClient::updateWindowMargins() { QRect windowGeometry; QSize clientSize = m_clientSize; if (m_xdgShellSurface) { windowGeometry = m_xdgShellSurface->windowGeometry(); } else if (m_xdgShellPopup) { windowGeometry = m_xdgShellPopup->windowGeometry(); if (!clientSize.isValid()) { clientSize = m_xdgShellPopup->initialSize(); } } else { return; } if (windowGeometry.isEmpty() || windowGeometry.width() > clientSize.width() || windowGeometry.height() > clientSize.height()) { m_windowMargins = QMargins(); } else { m_windowMargins = QMargins(windowGeometry.left(), windowGeometry.top(), clientSize.width() - (windowGeometry.right() + 1), clientSize.height() - (windowGeometry.bottom() + 1)); } } bool ShellClient::isPopupWindow() const { if (Toplevel::isPopupWindow()) { return true; } if (m_shellSurface != nullptr) { return m_shellSurface->isPopup(); } if (m_xdgShellPopup != nullptr) { return true; } return false; } QWindow *ShellClient::internalWindow() const { return nullptr; } bool ShellClient::supportsWindowRules() const { if (m_plasmaShellSurface) { return false; } return m_xdgShellSurface; } } diff --git a/workspace.cpp b/workspace.cpp index 4aecea969..82b493f6a 100644 --- a/workspace.cpp +++ b/workspace.cpp @@ -1,1782 +1,1785 @@ /******************************************************************** KWin - the KDE window manager This file is part of the KDE project. Copyright (C) 1999, 2000 Matthias Ettrich Copyright (C) 2003 Lubos Lunak 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 . *********************************************************************/ // own #include "workspace.h" // kwin libs #include // kwin #ifdef KWIN_BUILD_ACTIVITIES #include "activities.h" #endif #include "appmenu.h" #include "atoms.h" #include "client.h" #include "composite.h" #include "cursor.h" #include "dbusinterface.h" #include "deleted.h" #include "effects.h" #include "focuschain.h" #include "group.h" #include "input.h" #include "logind.h" #include "moving_client_x11_filter.h" #include "killwindow.h" #include "netinfo.h" #include "outline.h" #include "placement.h" #include "rules.h" #include "screenedge.h" #include "screens.h" #include "platform.h" #include "scripting/scripting.h" #ifdef KWIN_BUILD_TABBOX #include "tabbox.h" #endif #include "unmanaged.h" #include "useractions.h" #include "virtualdesktops.h" #include "shell_client.h" #include "was_user_interaction_x11_filter.h" #include "wayland_server.h" #include "xcbutils.h" #include "main.h" #include "decorations/decorationbridge.h" // KDE #include #include #include #include // Qt #include namespace KWin { extern int screen_number; extern bool is_multihead; ColorMapper::ColorMapper(QObject *parent) : QObject(parent) , m_default(defaultScreen()->default_colormap) , m_installed(defaultScreen()->default_colormap) { } ColorMapper::~ColorMapper() { } void ColorMapper::update() { xcb_colormap_t cmap = m_default; if (Client *c = dynamic_cast(Workspace::self()->activeClient())) { if (c->colormap() != XCB_COLORMAP_NONE) { cmap = c->colormap(); } } if (cmap != m_installed) { xcb_install_colormap(connection(), cmap); m_installed = cmap; } } Workspace* Workspace::_self = 0; Workspace::Workspace(const QString &sessionKey) : QObject(0) , m_compositor(NULL) // Unsorted , active_popup(NULL) , active_popup_client(NULL) , m_initialDesktop(1) , active_client(0) , last_active_client(0) , most_recently_raised(0) , movingClient(0) , delayfocus_client(0) , force_restacking(false) , showing_desktop(false) , was_user_interaction(false) , session_saving(false) , block_focus(0) , m_userActionsMenu(new UserActionsMenu(this)) , client_keys_dialog(NULL) , client_keys_client(NULL) , global_shortcuts_disabled_for_client(false) , workspaceInit(true) , startup(0) , set_active_client_recursion(0) , block_stacking_updates(0) { // If KWin was already running it saved its configuration after loosing the selection -> Reread QFuture reparseConfigFuture = QtConcurrent::run(options, &Options::reparseConfiguration); ApplicationMenu::create(this); _self = this; #ifdef KWIN_BUILD_ACTIVITIES Activities *activities = nullptr; if (kwinApp()->usesKActivities()) { activities = Activities::create(this); } if (activities) { connect(activities, SIGNAL(currentChanged(QString)), SLOT(updateCurrentActivity(QString))); } #endif // PluginMgr needs access to the config file, so we need to wait for it for finishing reparseConfigFuture.waitForFinished(); options->loadConfig(); options->loadCompositingConfig(false); delayFocusTimer = 0; if (!sessionKey.isEmpty()) loadSessionInfo(sessionKey); connect(qApp, &QGuiApplication::commitDataRequest, this, &Workspace::commitData); connect(qApp, &QGuiApplication::saveStateRequest, this, &Workspace::saveState); RuleBook::create(this)->load(); ScreenEdges::create(this); // VirtualDesktopManager needs to be created prior to init shortcuts // and prior to TabBox, due to TabBox connecting to signals // actual initialization happens in init() VirtualDesktopManager::create(this); //dbus interface new VirtualDesktopManagerDBusInterface(VirtualDesktopManager::self()); #ifdef KWIN_BUILD_TABBOX // need to create the tabbox before compositing scene is setup TabBox::TabBox::create(this); #endif if (Compositor::self()) { m_compositor = Compositor::self(); } else { m_compositor = Compositor::create(this); } connect(this, &Workspace::currentDesktopChanged, m_compositor, &Compositor::addRepaintFull); connect(m_compositor, &QObject::destroyed, this, [this] { m_compositor = nullptr; }); auto decorationBridge = Decoration::DecorationBridge::create(this); decorationBridge->init(); connect(this, &Workspace::configChanged, decorationBridge, &Decoration::DecorationBridge::reconfigure); new DBusInterface(this); Outline::create(this); initShortcuts(); init(); } void Workspace::init() { KSharedConfigPtr config = kwinApp()->config(); kwinApp()->createScreens(); Screens *screens = Screens::self(); // get screen support connect(screens, SIGNAL(changed()), SLOT(desktopResized())); screens->setConfig(config); screens->reconfigure(); connect(options, SIGNAL(configChanged()), screens, SLOT(reconfigure())); ScreenEdges *screenEdges = ScreenEdges::self(); screenEdges->setConfig(config); screenEdges->init(); connect(options, SIGNAL(configChanged()), screenEdges, SLOT(reconfigure())); connect(VirtualDesktopManager::self(), SIGNAL(layoutChanged(int,int)), screenEdges, SLOT(updateLayout())); connect(this, &Workspace::clientActivated, screenEdges, &ScreenEdges::checkBlocking); FocusChain *focusChain = FocusChain::create(this); connect(this, &Workspace::clientRemoved, focusChain, &FocusChain::remove); connect(this, &Workspace::clientActivated, focusChain, &FocusChain::setActiveClient); connect(VirtualDesktopManager::self(), SIGNAL(countChanged(uint,uint)), focusChain, SLOT(resize(uint,uint))); connect(VirtualDesktopManager::self(), SIGNAL(currentChanged(uint,uint)), focusChain, SLOT(setCurrentDesktop(uint,uint))); connect(options, SIGNAL(separateScreenFocusChanged(bool)), focusChain, SLOT(setSeparateScreenFocus(bool))); focusChain->setSeparateScreenFocus(options->isSeparateScreenFocus()); // create VirtualDesktopManager and perform dependency injection VirtualDesktopManager *vds = VirtualDesktopManager::self(); connect(vds, &VirtualDesktopManager::desktopRemoved, this, [this](KWin::VirtualDesktop *desktop) { //Wayland if (kwinApp()->operationMode() == Application::OperationModeWaylandOnly || kwinApp()->operationMode() == Application::OperationModeXwayland) { for (auto it = m_allClients.constBegin(); it != m_allClients.constEnd(); ++it) { if (!(*it)->desktops().contains(desktop)) { continue; } if ((*it)->desktops().count() > 1) { (*it)->leaveDesktop(desktop); } else { sendClientToDesktop(*it, qMin(desktop->x11DesktopNumber(), VirtualDesktopManager::self()->count()), true); } } //X11 } else { for (auto it = m_allClients.constBegin(); it != m_allClients.constEnd(); ++it) { if (!(*it)->isOnAllDesktops() && ((*it)->desktop() > static_cast(VirtualDesktopManager::self()->count()))) { sendClientToDesktop(*it, VirtualDesktopManager::self()->count(), true); } } } } ); connect(vds, SIGNAL(countChanged(uint,uint)), SLOT(slotDesktopCountChanged(uint,uint))); connect(vds, SIGNAL(currentChanged(uint,uint)), SLOT(slotCurrentDesktopChanged(uint,uint))); vds->setNavigationWrappingAround(options->isRollOverDesktops()); connect(options, SIGNAL(rollOverDesktopsChanged(bool)), vds, SLOT(setNavigationWrappingAround(bool))); vds->setConfig(config); // Now we know how many desktops we'll have, thus we initialize the positioning object Placement::create(this); // positioning object needs to be created before the virtual desktops are loaded. vds->load(); vds->updateLayout(); //makes sure any autogenerated id is saved, necessary as in case of xwayland, load will be called 2 times // load is needed to be called again when starting xwayalnd to sync to RootInfo, see BUG 385260 vds->save(); if (!VirtualDesktopManager::self()->setCurrent(m_initialDesktop)) VirtualDesktopManager::self()->setCurrent(1); reconfigureTimer.setSingleShot(true); updateToolWindowsTimer.setSingleShot(true); connect(&reconfigureTimer, SIGNAL(timeout()), this, SLOT(slotReconfigure())); connect(&updateToolWindowsTimer, SIGNAL(timeout()), this, SLOT(slotUpdateToolWindows())); // TODO: do we really need to reconfigure everything when fonts change? // maybe just reconfigure the decorations? Move this into libkdecoration? QDBusConnection::sessionBus().connect(QString(), QStringLiteral("/KDEPlatformTheme"), QStringLiteral("org.kde.KDEPlatformTheme"), QStringLiteral("refreshFonts"), this, SLOT(reconfigure())); active_client = NULL; initWithX11(); Scripting::create(this); if (auto w = waylandServer()) { connect(w, &WaylandServer::shellClientAdded, this, [this] (ShellClient *c) { setupClientConnections(c); c->updateDecoration(false); updateClientLayer(c); if (!c->isInternal()) { QRect area = clientArea(PlacementArea, Screens::self()->current(), c->desktop()); bool placementDone = false; if (c->isInitialPositionSet()) { placementDone = true; } if (c->isFullScreen()) { placementDone = true; } + if (c->maximizeMode() == MaximizeMode::MaximizeFull) { + placementDone = true; + } if (c->rules()->checkPosition(invalidPoint, true) != invalidPoint) { placementDone = true; } if (!placementDone) { c->placeIn(area); } m_allClients.append(c); if (!unconstrained_stacking_order.contains(c)) unconstrained_stacking_order.append(c); // Raise if it hasn't got any stacking position yet if (!stacking_order.contains(c)) // It'll be updated later, and updateToolWindows() requires stacking_order.append(c); // c to be in stacking_order } markXStackingOrderAsDirty(); updateStackingOrder(true); updateClientArea(); if (c->wantsInput() && !c->isMinimized()) { activateClient(c); } connect(c, &ShellClient::windowShown, this, [this, c] { updateClientLayer(c); // TODO: when else should we send the client through placement? if (c->hasTransientPlacementHint()) { QRect area = clientArea(PlacementArea, Screens::self()->current(), c->desktop()); c->placeIn(area); } markXStackingOrderAsDirty(); updateStackingOrder(true); updateClientArea(); if (c->wantsInput()) { activateClient(c); } } ); connect(c, &ShellClient::windowHidden, this, [this] { markXStackingOrderAsDirty(); updateStackingOrder(true); updateClientArea(); } ); } ); connect(w, &WaylandServer::shellClientRemoved, this, [this] (ShellClient *c) { m_allClients.removeAll(c); if (c == most_recently_raised) { most_recently_raised = nullptr; } if (c == delayfocus_client) { cancelDelayFocus(); } if (c == last_active_client) { last_active_client = nullptr; } if (client_keys_client == c) { setupWindowShortcutDone(false); } if (!c->shortcut().isEmpty()) { c->setShortcut(QString()); // Remove from client_keys } clientHidden(c); emit clientRemoved(c); markXStackingOrderAsDirty(); updateStackingOrder(true); updateClientArea(); } ); } // SELI TODO: This won't work with unreasonable focus policies, // and maybe in rare cases also if the selected client doesn't // want focus workspaceInit = false; // broadcast that Workspace is ready, but first process all events. QMetaObject::invokeMethod(this, "workspaceInitialized", Qt::QueuedConnection); // TODO: ungrabXServer() } void Workspace::initWithX11() { if (!kwinApp()->x11Connection()) { connect(kwinApp(), &Application::x11ConnectionChanged, this, &Workspace::initWithX11, Qt::UniqueConnection); return; } disconnect(kwinApp(), &Application::x11ConnectionChanged, this, &Workspace::initWithX11); atoms->retrieveHelpers(); // first initialize the extensions Xcb::Extensions::self(); ColorMapper *colormaps = new ColorMapper(this); connect(this, &Workspace::clientActivated, colormaps, &ColorMapper::update); // Call this before XSelectInput() on the root window startup = new KStartupInfo( KStartupInfo::DisableKWinModule | KStartupInfo::AnnounceSilenceChanges, this); // Select windowmanager privileges selectWmInputEventMask(); // Compatibility int32_t data = 1; xcb_change_property(connection(), XCB_PROP_MODE_APPEND, rootWindow(), atoms->kwin_running, atoms->kwin_running, 32, 1, &data); if (kwinApp()->operationMode() == Application::OperationModeX11) { m_wasUserInteractionFilter.reset(new WasUserInteractionX11Filter); m_movingClientFilter.reset(new MovingClientX11Filter); } updateXTime(); // Needed for proper initialization of user_time in Client ctor const uint32_t nullFocusValues[] = {true}; m_nullFocus.reset(new Xcb::Window(QRect(-1, -1, 1, 1), XCB_WINDOW_CLASS_INPUT_ONLY, XCB_CW_OVERRIDE_REDIRECT, nullFocusValues)); m_nullFocus->map(); RootInfo *rootInfo = RootInfo::create(); const auto vds = VirtualDesktopManager::self(); vds->setRootInfo(rootInfo); // load again to sync to RootInfo, see BUG 385260 vds->load(); vds->updateRootInfo(); rootInfo->setCurrentDesktop(vds->currentDesktop()->x11DesktopNumber()); // TODO: only in X11 mode // Extra NETRootInfo instance in Client mode is needed to get the values of the properties NETRootInfo client_info(connection(), NET::ActiveWindow | NET::CurrentDesktop); if (!qApp->isSessionRestored()) { m_initialDesktop = client_info.currentDesktop(); vds->setCurrent(m_initialDesktop); } // TODO: better value rootInfo->setActiveWindow(None); focusToNull(); if (!qApp->isSessionRestored()) ++block_focus; // Because it will be set below { // Begin updates blocker block StackingUpdatesBlocker blocker(this); Xcb::Tree tree(rootWindow()); xcb_window_t *wins = xcb_query_tree_children(tree.data()); QVector windowAttributes(tree->children_len); QVector windowGeometries(tree->children_len); // Request the attributes and geometries of all toplevel windows for (int i = 0; i < tree->children_len; i++) { windowAttributes[i] = Xcb::WindowAttributes(wins[i]); windowGeometries[i] = Xcb::WindowGeometry(wins[i]); } // Get the replies for (int i = 0; i < tree->children_len; i++) { Xcb::WindowAttributes attr(windowAttributes.at(i)); if (attr.isNull()) { continue; } if (attr->override_redirect) { if (attr->map_state == XCB_MAP_STATE_VIEWABLE && attr->_class != XCB_WINDOW_CLASS_INPUT_ONLY) // ### This will request the attributes again createUnmanaged(wins[i]); } else if (attr->map_state != XCB_MAP_STATE_UNMAPPED) { if (Application::wasCrash()) { fixPositionAfterCrash(wins[i], windowGeometries.at(i).data()); } // ### This will request the attributes again createClient(wins[i], true); } } // Propagate clients, will really happen at the end of the updates blocker block updateStackingOrder(true); saveOldScreenSizes(); updateClientArea(); // NETWM spec says we have to set it to (0,0) if we don't support it NETPoint* viewports = new NETPoint[VirtualDesktopManager::self()->count()]; rootInfo->setDesktopViewport(VirtualDesktopManager::self()->count(), *viewports); delete[] viewports; QRect geom; for (int i = 0; i < screens()->count(); i++) { geom |= screens()->geometry(i); } NETSize desktop_geometry; desktop_geometry.width = geom.width(); desktop_geometry.height = geom.height(); rootInfo->setDesktopGeometry(desktop_geometry); setShowingDesktop(false); } // End updates blocker block // TODO: only on X11? AbstractClient* new_active_client = nullptr; if (!qApp->isSessionRestored()) { --block_focus; new_active_client = findClient(Predicate::WindowMatch, client_info.activeWindow()); } if (new_active_client == NULL && activeClient() == NULL && should_get_focus.count() == 0) { // No client activated in manage() if (new_active_client == NULL) new_active_client = topClientOnDesktop(VirtualDesktopManager::self()->current(), -1); if (new_active_client == NULL && !desktops.isEmpty()) new_active_client = findDesktop(true, VirtualDesktopManager::self()->current()); } if (new_active_client != NULL) activateClient(new_active_client); } Workspace::~Workspace() { blockStackingUpdates(true); // TODO: grabXServer(); // Use stacking_order, so that kwin --replace keeps stacking order const ToplevelList stack = stacking_order; // "mutex" the stackingorder, since anything trying to access it from now on will find // many dangeling pointers and crash stacking_order.clear(); for (ToplevelList::const_iterator it = stack.constBegin(), end = stack.constEnd(); it != end; ++it) { Client *c = qobject_cast(const_cast(*it)); if (!c) { continue; } // Only release the window c->releaseWindow(true); // No removeClient() is called, it does more than just removing. // However, remove from some lists to e.g. prevent performTransiencyCheck() // from crashing. clients.removeAll(c); m_allClients.removeAll(c); desktops.removeAll(c); } Client::cleanupX11(); for (UnmanagedList::iterator it = unmanaged.begin(), end = unmanaged.end(); it != end; ++it) (*it)->release(ReleaseReason::KWinShutsDown); if (auto c = kwinApp()->x11Connection()) { xcb_delete_property(c, kwinApp()->x11RootWindow(), atoms->kwin_running); } for (auto it = deleted.begin(); it != deleted.end();) { emit deletedRemoved(*it); it = deleted.erase(it); } delete RuleBook::self(); kwinApp()->config()->sync(); RootInfo::destroy(); delete startup; delete Placement::self(); delete client_keys_dialog; foreach (SessionInfo * s, session) delete s; // TODO: ungrabXServer(); Xcb::Extensions::destroy(); _self = 0; } void Workspace::setupClientConnections(AbstractClient *c) { connect(c, &Toplevel::needsRepaint, m_compositor, &Compositor::scheduleRepaint); connect(c, &AbstractClient::desktopPresenceChanged, this, &Workspace::desktopPresenceChanged); connect(c, &AbstractClient::minimizedChanged, this, std::bind(&Workspace::clientMinimizedChanged, this, c)); } Client* Workspace::createClient(xcb_window_t w, bool is_mapped) { StackingUpdatesBlocker blocker(this); Client* c = new Client(); setupClientConnections(c); connect(c, &Client::blockingCompositingChanged, m_compositor, &Compositor::updateClientCompositeBlocking); connect(c, SIGNAL(clientFullScreenSet(KWin::Client*,bool,bool)), ScreenEdges::self(), SIGNAL(checkBlocking())); if (!c->manage(w, is_mapped)) { Client::deleteClient(c); return NULL; } addClient(c); return c; } Unmanaged* Workspace::createUnmanaged(xcb_window_t w) { if (m_compositor && m_compositor->checkForOverlayWindow(w)) return NULL; Unmanaged* c = new Unmanaged(); if (!c->track(w)) { Unmanaged::deleteUnmanaged(c); return NULL; } connect(c, &Unmanaged::needsRepaint, m_compositor, &Compositor::scheduleRepaint); addUnmanaged(c); emit unmanagedAdded(c); return c; } void Workspace::addClient(Client* c) { Group* grp = findGroup(c->window()); emit clientAdded(c); if (grp != NULL) grp->gotLeader(c); if (c->isDesktop()) { desktops.append(c); if (active_client == NULL && should_get_focus.isEmpty() && c->isOnCurrentDesktop()) requestFocus(c); // TODO: Make sure desktop is active after startup if there's no other window active } else { FocusChain::self()->update(c, FocusChain::Update); clients.append(c); m_allClients.append(c); } if (!unconstrained_stacking_order.contains(c)) unconstrained_stacking_order.append(c); // Raise if it hasn't got any stacking position yet if (!stacking_order.contains(c)) // It'll be updated later, and updateToolWindows() requires stacking_order.append(c); // c to be in stacking_order markXStackingOrderAsDirty(); updateClientArea(); // This cannot be in manage(), because the client got added only now updateClientLayer(c); if (c->isDesktop()) { raiseClient(c); // If there's no active client, make this desktop the active one if (activeClient() == NULL && should_get_focus.count() == 0) activateClient(findDesktop(true, VirtualDesktopManager::self()->current())); } c->checkActiveModal(); checkTransients(c->window()); // SELI TODO: Does this really belong here? updateStackingOrder(true); // Propagate new client if (c->isUtility() || c->isMenu() || c->isToolbar()) updateToolWindows(true); #ifdef KWIN_BUILD_TABBOX if (TabBox::TabBox::self()->isDisplayed()) TabBox::TabBox::self()->reset(true); #endif } void Workspace::addUnmanaged(Unmanaged* c) { unmanaged.append(c); markXStackingOrderAsDirty(); } /** * Destroys the client \a c **/ void Workspace::removeClient(Client* c) { if (c == active_popup_client) closeActivePopup(); if (m_userActionsMenu->isMenuClient(c)) { m_userActionsMenu->close(); } c->untab(QRect(), true); if (client_keys_client == c) setupWindowShortcutDone(false); if (!c->shortcut().isEmpty()) { c->setShortcut(QString()); // Remove from client_keys clientShortcutUpdated(c); // Needed, since this is otherwise delayed by setShortcut() and wouldn't run } Q_ASSERT(clients.contains(c) || desktops.contains(c)); // TODO: if marked client is removed, notify the marked list clients.removeAll(c); m_allClients.removeAll(c); desktops.removeAll(c); markXStackingOrderAsDirty(); attention_chain.removeAll(c); Group* group = findGroup(c->window()); if (group != NULL) group->lostLeader(); if (c == most_recently_raised) most_recently_raised = 0; should_get_focus.removeAll(c); Q_ASSERT(c != active_client); if (c == last_active_client) last_active_client = 0; if (c == delayfocus_client) cancelDelayFocus(); emit clientRemoved(c); updateStackingOrder(true); #ifdef KWIN_BUILD_TABBOX TabBox::TabBox *tabBox = TabBox::TabBox::self(); if (tabBox->isDisplayed()) tabBox->reset(true); #endif updateClientArea(); } void Workspace::removeUnmanaged(Unmanaged* c) { assert(unmanaged.contains(c)); unmanaged.removeAll(c); emit unmanagedRemoved(c); markXStackingOrderAsDirty(); } void Workspace::addDeleted(Deleted* c, Toplevel *orig) { assert(!deleted.contains(c)); deleted.append(c); const int unconstraintedIndex = unconstrained_stacking_order.indexOf(orig); if (unconstraintedIndex != -1) { unconstrained_stacking_order.replace(unconstraintedIndex, c); } else { unconstrained_stacking_order.append(c); } const int index = stacking_order.indexOf(orig); if (index != -1) { stacking_order.replace(index, c); } else { stacking_order.append(c); } markXStackingOrderAsDirty(); connect(c, &Deleted::needsRepaint, m_compositor, &Compositor::scheduleRepaint); } void Workspace::removeDeleted(Deleted* c) { assert(deleted.contains(c)); emit deletedRemoved(c); deleted.removeAll(c); unconstrained_stacking_order.removeAll(c); stacking_order.removeAll(c); markXStackingOrderAsDirty(); if (c->wasClient() && m_compositor) { m_compositor->updateCompositeBlocking(); } } void Workspace::updateToolWindows(bool also_hide) { // TODO: What if Client's transiency/group changes? should this be called too? (I'm paranoid, am I not?) if (!options->isHideUtilityWindowsForInactive()) { for (ClientList::ConstIterator it = clients.constBegin(); it != clients.constEnd(); ++it) if (!(*it)->tabGroup() || (*it)->tabGroup()->current() == *it) (*it)->hideClient(false); return; } const Group* group = nullptr; auto client = active_client; // Go up in transiency hiearchy, if the top is found, only tool transients for the top mainwindow // will be shown; if a group transient is group, all tools in the group will be shown while (client != nullptr) { if (!client->isTransient()) break; if (client->groupTransient()) { group = client->group(); break; } client = client->transientFor(); } // Use stacking order only to reduce flicker, it doesn't matter if block_stacking_updates == 0, // I.e. if it's not up to date // SELI TODO: But maybe it should - what if a new client has been added that's not in stacking order yet? QVector to_show, to_hide; for (ToplevelList::ConstIterator it = stacking_order.constBegin(); it != stacking_order.constEnd(); ++it) { auto c = qobject_cast(*it); if (!c) { continue; } if (c->isUtility() || c->isMenu() || c->isToolbar()) { bool show = true; if (!c->isTransient()) { if (!c->group() || c->group()->members().count() == 1) // Has its own group, keep always visible show = true; else if (client != NULL && c->group() == client->group()) show = true; else show = false; } else { if (group != NULL && c->group() == group) show = true; else if (client != NULL && client->hasTransient(c, true)) show = true; else show = false; } if (!show && also_hide) { const auto mainclients = c->mainClients(); // Don't hide utility windows which are standalone(?) or // have e.g. kicker as mainwindow if (mainclients.isEmpty()) show = true; for (auto it2 = mainclients.constBegin(); it2 != mainclients.constEnd(); ++it2) { if ((*it2)->isSpecialWindow()) show = true; } if (!show) to_hide.append(c); } if (show) to_show.append(c); } } // First show new ones, then hide for (int i = to_show.size() - 1; i >= 0; --i) // From topmost // TODO: Since this is in stacking order, the order of taskbar entries changes :( to_show.at(i)->hideClient(false); if (also_hide) { for (auto it = to_hide.constBegin(); it != to_hide.constEnd(); ++it) // From bottommost (*it)->hideClient(true); updateToolWindowsTimer.stop(); } else // setActiveClient() is after called with NULL client, quickly followed // by setting a new client, which would result in flickering resetUpdateToolWindowsTimer(); } void Workspace::resetUpdateToolWindowsTimer() { updateToolWindowsTimer.start(200); } void Workspace::slotUpdateToolWindows() { updateToolWindows(true); } void Workspace::slotReloadConfig() { reconfigure(); } void Workspace::reconfigure() { reconfigureTimer.start(200); } /** * Reread settings **/ void Workspace::slotReconfigure() { qCDebug(KWIN_CORE) << "Workspace::slotReconfigure()"; reconfigureTimer.stop(); bool borderlessMaximizedWindows = options->borderlessMaximizedWindows(); kwinApp()->config()->reparseConfiguration(); options->updateSettings(); emit configChanged(); m_userActionsMenu->discard(); updateToolWindows(true); RuleBook::self()->load(); for (auto it = m_allClients.begin(); it != m_allClients.end(); ++it) { (*it)->setupWindowRules(true); (*it)->applyWindowRules(); RuleBook::self()->discardUsed(*it, false); } if (borderlessMaximizedWindows != options->borderlessMaximizedWindows() && !options->borderlessMaximizedWindows()) { // in case borderless maximized windows option changed and new option // is to have borders, we need to unset the borders for all maximized windows for (auto it = m_allClients.begin(); it != m_allClients.end(); ++it) { if ((*it)->maximizeMode() == MaximizeFull) (*it)->checkNoBorder(); } } } void Workspace::slotCurrentDesktopChanged(uint oldDesktop, uint newDesktop) { closeActivePopup(); ++block_focus; StackingUpdatesBlocker blocker(this); updateClientVisibilityOnDesktopChange(newDesktop); // Restore the focus on this desktop --block_focus; activateClientOnNewDesktop(newDesktop); emit currentDesktopChanged(oldDesktop, movingClient); } void Workspace::updateClientVisibilityOnDesktopChange(uint newDesktop) { for (ToplevelList::ConstIterator it = stacking_order.constBegin(); it != stacking_order.constEnd(); ++it) { Client *c = qobject_cast(*it); if (!c) { continue; } if (!c->isOnDesktop(newDesktop) && c != movingClient && c->isOnCurrentActivity()) { (c)->updateVisibility(); } } // Now propagate the change, after hiding, before showing if (rootInfo()) { rootInfo()->setCurrentDesktop(VirtualDesktopManager::self()->current()); } if (movingClient && !movingClient->isOnDesktop(newDesktop)) { movingClient->setDesktop(newDesktop); } for (int i = stacking_order.size() - 1; i >= 0 ; --i) { Client *c = qobject_cast(stacking_order.at(i)); if (!c) { continue; } if (c->isOnDesktop(newDesktop) && c->isOnCurrentActivity()) c->updateVisibility(); } if (showingDesktop()) // Do this only after desktop change to avoid flicker setShowingDesktop(false); } void Workspace::activateClientOnNewDesktop(uint desktop) { AbstractClient* c = NULL; if (options->focusPolicyIsReasonable()) { c = findClientToActivateOnDesktop(desktop); } // If "unreasonable focus policy" and active_client is on_all_desktops and // under mouse (Hence == old_active_client), conserve focus. // (Thanks to Volker Schatz ) else if (active_client && active_client->isShown(true) && active_client->isOnCurrentDesktop()) c = active_client; if (c == NULL && !desktops.isEmpty()) c = findDesktop(true, desktop); if (c != active_client) setActiveClient(NULL); if (c) requestFocus(c); else if (!desktops.isEmpty()) requestFocus(findDesktop(true, desktop)); else focusToNull(); } AbstractClient *Workspace::findClientToActivateOnDesktop(uint desktop) { if (movingClient != NULL && active_client == movingClient && FocusChain::self()->contains(active_client, desktop) && active_client->isShown(true) && active_client->isOnCurrentDesktop()) { // A requestFocus call will fail, as the client is already active return active_client; } // from actiavtion.cpp if (options->isNextFocusPrefersMouse()) { ToplevelList::const_iterator it = stackingOrder().constEnd(); while (it != stackingOrder().constBegin()) { Client *client = qobject_cast(*(--it)); if (!client) { continue; } if (!(client->isShown(false) && client->isOnDesktop(desktop) && client->isOnCurrentActivity() && client->isOnActiveScreen())) continue; if (client->geometry().contains(Cursor::pos())) { if (!client->isDesktop()) return client; break; // unconditional break - we do not pass the focus to some client below an unusable one } } } return FocusChain::self()->getForActivation(desktop); } /** * Updates the current activity when it changes * do *not* call this directly; it does not set the activity. * * Shows/Hides windows according to the stacking order **/ void Workspace::updateCurrentActivity(const QString &new_activity) { #ifdef KWIN_BUILD_ACTIVITIES if (!Activities::self()) { return; } //closeActivePopup(); ++block_focus; // TODO: Q_ASSERT( block_stacking_updates == 0 ); // Make sure stacking_order is up to date StackingUpdatesBlocker blocker(this); // Optimized Desktop switching: unmapping done from back to front // mapping done from front to back => less exposure events //Notify::raise((Notify::Event) (Notify::DesktopChange+new_desktop)); for (ToplevelList::ConstIterator it = stacking_order.constBegin(); it != stacking_order.constEnd(); ++it) { Client *c = qobject_cast(*it); if (!c) { continue; } if (!c->isOnActivity(new_activity) && c != movingClient && c->isOnCurrentDesktop()) { c->updateVisibility(); } } // Now propagate the change, after hiding, before showing //rootInfo->setCurrentDesktop( currentDesktop() ); /* TODO someday enable dragging windows to other activities if ( movingClient && !movingClient->isOnDesktop( new_desktop )) { movingClient->setDesktop( new_desktop ); */ for (int i = stacking_order.size() - 1; i >= 0 ; --i) { Client *c = qobject_cast(stacking_order.at(i)); if (!c) { continue; } if (c->isOnActivity(new_activity)) c->updateVisibility(); } //FIXME not sure if I should do this either if (showingDesktop()) // Do this only after desktop change to avoid flicker setShowingDesktop(false); // Restore the focus on this desktop --block_focus; AbstractClient* c = 0; //FIXME below here is a lot of focuschain stuff, probably all wrong now if (options->focusPolicyIsReasonable()) { // Search in focus chain c = FocusChain::self()->getForActivation(VirtualDesktopManager::self()->current()); } // If "unreasonable focus policy" and active_client is on_all_desktops and // under mouse (Hence == old_active_client), conserve focus. // (Thanks to Volker Schatz ) else if (active_client && active_client->isShown(true) && active_client->isOnCurrentDesktop() && active_client->isOnCurrentActivity()) c = active_client; if (c == NULL && !desktops.isEmpty()) c = findDesktop(true, VirtualDesktopManager::self()->current()); if (c != active_client) setActiveClient(NULL); if (c) requestFocus(c); else if (!desktops.isEmpty()) requestFocus(findDesktop(true, VirtualDesktopManager::self()->current())); else focusToNull(); // Not for the very first time, only if something changed and there are more than 1 desktops //if ( effects != NULL && old_desktop != 0 && old_desktop != new_desktop ) // static_cast( effects )->desktopChanged( old_desktop ); if (compositing() && m_compositor) m_compositor->addRepaintFull(); #else Q_UNUSED(new_activity) #endif } void Workspace::slotDesktopCountChanged(uint previousCount, uint newCount) { Q_UNUSED(previousCount) Placement::self()->reinitCascading(0); resetClientAreas(newCount); } void Workspace::resetClientAreas(uint desktopCount) { // Make it +1, so that it can be accessed as [1..numberofdesktops] workarea.clear(); workarea.resize(desktopCount + 1); restrictedmovearea.clear(); restrictedmovearea.resize(desktopCount + 1); screenarea.clear(); updateClientArea(true); } void Workspace::selectWmInputEventMask() { uint32_t presentMask = 0; Xcb::WindowAttributes attr(rootWindow()); if (!attr.isNull()) { presentMask = attr->your_event_mask; } Xcb::selectInput(rootWindow(), presentMask | XCB_EVENT_MASK_KEY_PRESS | XCB_EVENT_MASK_PROPERTY_CHANGE | XCB_EVENT_MASK_COLOR_MAP_CHANGE | XCB_EVENT_MASK_SUBSTRUCTURE_REDIRECT | XCB_EVENT_MASK_SUBSTRUCTURE_NOTIFY | XCB_EVENT_MASK_FOCUS_CHANGE | // For NotifyDetailNone XCB_EVENT_MASK_EXPOSURE ); } /** * Sends client \a c to desktop \a desk. * * Takes care of transients as well. **/ void Workspace::sendClientToDesktop(AbstractClient* c, int desk, bool dont_activate) { if ((desk < 1 && desk != NET::OnAllDesktops) || desk > static_cast(VirtualDesktopManager::self()->count())) return; int old_desktop = c->desktop(); bool was_on_desktop = c->isOnDesktop(desk) || c->isOnAllDesktops(); c->setDesktop(desk); if (c->desktop() != desk) // No change or desktop forced return; desk = c->desktop(); // Client did range checking if (c->isOnDesktop(VirtualDesktopManager::self()->current())) { if (c->wantsTabFocus() && options->focusPolicyIsReasonable() && !was_on_desktop && // for stickyness changes !dont_activate) requestFocus(c); else restackClientUnderActive(c); } else raiseClient(c); c->checkWorkspacePosition( QRect(), old_desktop ); auto transients_stacking_order = ensureStackingOrder(c->transients()); for (auto it = transients_stacking_order.constBegin(); it != transients_stacking_order.constEnd(); ++it) sendClientToDesktop(*it, desk, dont_activate); updateClientArea(); } /** * checks whether the X Window with the input focus is on our X11 screen * if the window cannot be determined or inspected, resturn depends on whether there's actually * more than one screen * * this is NOT in any way related to XRandR multiscreen * **/ extern bool is_multihead; // main.cpp bool Workspace::isOnCurrentHead() { if (!is_multihead) { return true; } Xcb::CurrentInput currentInput; if (currentInput.window() == XCB_WINDOW_NONE) { return !is_multihead; } Xcb::WindowGeometry geometry(currentInput.window()); if (geometry.isNull()) { // should not happen return !is_multihead; } return rootWindow() == geometry->root; } void Workspace::sendClientToScreen(AbstractClient* c, int screen) { c->sendToScreen(screen); } void Workspace::sendPingToWindow(xcb_window_t window, xcb_timestamp_t timestamp) { if (rootInfo()) { rootInfo()->sendPing(window, timestamp); } } /** * Delayed focus functions **/ void Workspace::delayFocus() { requestFocus(delayfocus_client); cancelDelayFocus(); } void Workspace::requestDelayFocus(AbstractClient* c) { delayfocus_client = c; delete delayFocusTimer; delayFocusTimer = new QTimer(this); connect(delayFocusTimer, SIGNAL(timeout()), this, SLOT(delayFocus())); delayFocusTimer->setSingleShot(true); delayFocusTimer->start(options->delayFocusInterval()); } void Workspace::cancelDelayFocus() { delete delayFocusTimer; delayFocusTimer = 0; } bool Workspace::checkStartupNotification(xcb_window_t w, KStartupInfoId &id, KStartupInfoData &data) { return startup->checkStartup(w, id, data) == KStartupInfo::Match; } /** * Puts the focus on a dummy window * Just using XSetInputFocus() with None would block keyboard input **/ void Workspace::focusToNull() { if (m_nullFocus) { m_nullFocus->focus(); } } void Workspace::setShowingDesktop(bool showing) { const bool changed = showing != showing_desktop; if (rootInfo() && changed) { rootInfo()->setShowingDesktop(showing); } showing_desktop = showing; AbstractClient *topDesk = nullptr; { // for the blocker RAII StackingUpdatesBlocker blocker(this); // updateLayer & lowerClient would invalidate stacking_order for (int i = stacking_order.count() - 1; i > -1; --i) { AbstractClient *c = qobject_cast(stacking_order.at(i)); if (c && c->isOnCurrentDesktop()) { if (c->isDock()) { c->updateLayer(); } else if (c->isDesktop() && c->isShown(true)) { c->updateLayer(); lowerClient(c); if (!topDesk) topDesk = c; if (auto group = c->group()) { foreach (Client *cm, group->members()) { cm->updateLayer(); } } } } } } // ~StackingUpdatesBlocker if (showing_desktop && topDesk) { requestFocus(topDesk); } else if (!showing_desktop && changed) { const auto client = FocusChain::self()->getForActivation(VirtualDesktopManager::self()->current()); if (client) { activateClient(client); } } if (changed) emit showingDesktopChanged(showing); } void Workspace::disableGlobalShortcutsForClient(bool disable) { if (global_shortcuts_disabled_for_client == disable) return; QDBusMessage message = QDBusMessage::createMethodCall(QStringLiteral("org.kde.kglobalaccel"), QStringLiteral("/kglobalaccel"), QStringLiteral("org.kde.KGlobalAccel"), QStringLiteral("blockGlobalShortcuts")); message.setArguments(QList() << disable); QDBusConnection::sessionBus().asyncCall(message); global_shortcuts_disabled_for_client = disable; // Update also Alt+LMB actions etc. for (ClientList::ConstIterator it = clients.constBegin(); it != clients.constEnd(); ++it) (*it)->updateMouseGrab(); } QString Workspace::supportInformation() const { QString support; const QString yes = QStringLiteral("yes\n"); const QString no = QStringLiteral("no\n"); support.append(ki18nc("Introductory text shown in the support information.", "KWin Support Information:\n" "The following information should be used when requesting support on e.g. https://forum.kde.org.\n" "It provides information about the currently running instance, which options are used,\n" "what OpenGL driver and which effects are running.\n" "Please post the information provided underneath this introductory text to a paste bin service\n" "like https://paste.kde.org instead of pasting into support threads.\n").toString()); support.append(QStringLiteral("\n==========================\n\n")); // all following strings are intended for support. They need to be pasted to e.g forums.kde.org // it is expected that the support will happen in English language or that the people providing // help understand English. Because of that all texts are not translated support.append(QStringLiteral("Version\n")); support.append(QStringLiteral("=======\n")); support.append(QStringLiteral("KWin version: ")); support.append(QStringLiteral(KWIN_VERSION_STRING)); support.append(QStringLiteral("\n")); support.append(QStringLiteral("Qt Version: ")); support.append(QString::fromUtf8(qVersion())); support.append(QStringLiteral("\n")); support.append(QStringLiteral("Qt compile version: %1\n").arg(QStringLiteral(QT_VERSION_STR))); support.append(QStringLiteral("XCB compile version: %1\n\n").arg(QStringLiteral(XCB_VERSION_STRING))); support.append(QStringLiteral("Operation Mode: ")); switch (kwinApp()->operationMode()) { case Application::OperationModeX11: support.append(QStringLiteral("X11 only")); break; case Application::OperationModeWaylandOnly: support.append(QStringLiteral("Wayland Only")); break; case Application::OperationModeXwayland: support.append(QStringLiteral("Xwayland")); break; } support.append(QStringLiteral("\n\n")); support.append(QStringLiteral("Build Options\n")); support.append(QStringLiteral("=============\n")); support.append(QStringLiteral("KWIN_BUILD_DECORATIONS: ")); #ifdef KWIN_BUILD_DECORATIONS support.append(yes); #else support.append(no); #endif support.append(QStringLiteral("KWIN_BUILD_TABBOX: ")); #ifdef KWIN_BUILD_TABBOX support.append(yes); #else support.append(no); #endif support.append(QStringLiteral("KWIN_BUILD_ACTIVITIES: ")); #ifdef KWIN_BUILD_ACTIVITIES support.append(yes); #else support.append(no); #endif support.append(QStringLiteral("HAVE_DRM: ")); #if HAVE_DRM support.append(yes); #else support.append(no); #endif support.append(QStringLiteral("HAVE_GBM: ")); #if HAVE_GBM support.append(yes); #else support.append(no); #endif support.append(QStringLiteral("HAVE_EGL_STREAMS: ")); #if HAVE_EGL_STREAMS support.append(yes); #else support.append(no); #endif support.append(QStringLiteral("HAVE_X11_XCB: ")); #if HAVE_X11_XCB support.append(yes); #else support.append(no); #endif support.append(QStringLiteral("HAVE_EPOXY_GLX: ")); #if HAVE_EPOXY_GLX support.append(yes); #else support.append(no); #endif support.append(QStringLiteral("HAVE_WAYLAND_EGL: ")); #if HAVE_WAYLAND_EGL support.append(yes); #else support.append(no); #endif support.append(QStringLiteral("\n")); if (auto c = kwinApp()->x11Connection()) { support.append(QStringLiteral("X11\n")); support.append(QStringLiteral("===\n")); auto x11setup = xcb_get_setup(c); support.append(QStringLiteral("Vendor: %1\n").arg(QString::fromUtf8(QByteArray::fromRawData(xcb_setup_vendor(x11setup), xcb_setup_vendor_length(x11setup))))); support.append(QStringLiteral("Vendor Release: %1\n").arg(x11setup->release_number)); support.append(QStringLiteral("Protocol Version/Revision: %1/%2\n").arg(x11setup->protocol_major_version).arg(x11setup->protocol_minor_version)); const auto extensions = Xcb::Extensions::self()->extensions(); for (const auto &e : extensions) { support.append(QStringLiteral("%1: %2; Version: 0x%3\n").arg(QString::fromUtf8(e.name)) .arg(e.present ? yes.trimmed() : no.trimmed()) .arg(QString::number(e.version, 16))); } support.append(QStringLiteral("\n")); } if (auto bridge = Decoration::DecorationBridge::self()) { support.append(QStringLiteral("Decoration\n")); support.append(QStringLiteral("==========\n")); support.append(bridge->supportInformation()); support.append(QStringLiteral("\n")); } support.append(QStringLiteral("Platform\n")); support.append(QStringLiteral("==========\n")); support.append(kwinApp()->platform()->supportInformation()); support.append(QStringLiteral("\n")); support.append(QStringLiteral("Options\n")); support.append(QStringLiteral("=======\n")); const QMetaObject *metaOptions = options->metaObject(); auto printProperty = [] (const QVariant &variant) { if (variant.type() == QVariant::Size) { const QSize &s = variant.toSize(); return QStringLiteral("%1x%2").arg(QString::number(s.width())).arg(QString::number(s.height())); } if (QLatin1String(variant.typeName()) == QLatin1String("KWin::OpenGLPlatformInterface") || QLatin1String(variant.typeName()) == QLatin1String("KWin::Options::WindowOperation")) { return QString::number(variant.toInt()); } return variant.toString(); }; for (int i=0; ipropertyCount(); ++i) { const QMetaProperty property = metaOptions->property(i); if (QLatin1String(property.name()) == QLatin1String("objectName")) { continue; } support.append(QStringLiteral("%1: %2\n").arg(property.name()).arg(printProperty(options->property(property.name())))); } support.append(QStringLiteral("\nScreen Edges\n")); support.append(QStringLiteral( "============\n")); const QMetaObject *metaScreenEdges = ScreenEdges::self()->metaObject(); for (int i=0; ipropertyCount(); ++i) { const QMetaProperty property = metaScreenEdges->property(i); if (QLatin1String(property.name()) == QLatin1String("objectName")) { continue; } support.append(QStringLiteral("%1: %2\n").arg(property.name()).arg(printProperty(ScreenEdges::self()->property(property.name())))); } support.append(QStringLiteral("\nScreens\n")); support.append(QStringLiteral( "=======\n")); support.append(QStringLiteral("Multi-Head: ")); if (is_multihead) { support.append(QStringLiteral("yes\n")); support.append(QStringLiteral("Head: %1\n").arg(screen_number)); } else { support.append(QStringLiteral("no\n")); } support.append(QStringLiteral("Active screen follows mouse: ")); if (screens()->isCurrentFollowsMouse()) support.append(QStringLiteral(" yes\n")); else support.append(QStringLiteral(" no\n")); support.append(QStringLiteral("Number of Screens: %1\n\n").arg(screens()->count())); for (int i=0; icount(); ++i) { const QRect geo = screens()->geometry(i); support.append(QStringLiteral("Screen %1:\n").arg(i)); support.append(QStringLiteral("---------\n")); support.append(QStringLiteral("Name: %1\n").arg(screens()->name(i))); support.append(QStringLiteral("Geometry: %1,%2,%3x%4\n") .arg(geo.x()) .arg(geo.y()) .arg(geo.width()) .arg(geo.height())); support.append(QStringLiteral("Scale: %1\n").arg(screens()->scale(i))); support.append(QStringLiteral("Refresh Rate: %1\n\n").arg(screens()->refreshRate(i))); } support.append(QStringLiteral("\nCompositing\n")); support.append(QStringLiteral( "===========\n")); if (effects) { support.append(QStringLiteral("Compositing is active\n")); switch (effects->compositingType()) { case OpenGL2Compositing: case OpenGLCompositing: { GLPlatform *platform = GLPlatform::instance(); if (platform->isGLES()) { support.append(QStringLiteral("Compositing Type: OpenGL ES 2.0\n")); } else { support.append(QStringLiteral("Compositing Type: OpenGL\n")); } support.append(QStringLiteral("OpenGL vendor string: ") + QString::fromUtf8(platform->glVendorString()) + QStringLiteral("\n")); support.append(QStringLiteral("OpenGL renderer string: ") + QString::fromUtf8(platform->glRendererString()) + QStringLiteral("\n")); support.append(QStringLiteral("OpenGL version string: ") + QString::fromUtf8(platform->glVersionString()) + QStringLiteral("\n")); support.append(QStringLiteral("OpenGL platform interface: ")); switch (platform->platformInterface()) { case GlxPlatformInterface: support.append(QStringLiteral("GLX")); break; case EglPlatformInterface: support.append(QStringLiteral("EGL")); break; default: support.append(QStringLiteral("UNKNOWN")); } support.append(QStringLiteral("\n")); if (platform->supports(LimitedGLSL) || platform->supports(GLSL)) support.append(QStringLiteral("OpenGL shading language version string: ") + QString::fromUtf8(platform->glShadingLanguageVersionString()) + QStringLiteral("\n")); support.append(QStringLiteral("Driver: ") + GLPlatform::driverToString(platform->driver()) + QStringLiteral("\n")); if (!platform->isMesaDriver()) support.append(QStringLiteral("Driver version: ") + GLPlatform::versionToString(platform->driverVersion()) + QStringLiteral("\n")); support.append(QStringLiteral("GPU class: ") + GLPlatform::chipClassToString(platform->chipClass()) + QStringLiteral("\n")); support.append(QStringLiteral("OpenGL version: ") + GLPlatform::versionToString(platform->glVersion()) + QStringLiteral("\n")); if (platform->supports(LimitedGLSL) || platform->supports(GLSL)) support.append(QStringLiteral("GLSL version: ") + GLPlatform::versionToString(platform->glslVersion()) + QStringLiteral("\n")); if (platform->isMesaDriver()) support.append(QStringLiteral("Mesa version: ") + GLPlatform::versionToString(platform->mesaVersion()) + QStringLiteral("\n")); if (platform->serverVersion() > 0) support.append(QStringLiteral("X server version: ") + GLPlatform::versionToString(platform->serverVersion()) + QStringLiteral("\n")); if (platform->kernelVersion() > 0) support.append(QStringLiteral("Linux kernel version: ") + GLPlatform::versionToString(platform->kernelVersion()) + QStringLiteral("\n")); support.append(QStringLiteral("Direct rendering: ")); support.append(QStringLiteral("Requires strict binding: ")); if (!platform->isLooseBinding()) { support.append(QStringLiteral("yes\n")); } else { support.append(QStringLiteral("no\n")); } support.append(QStringLiteral("GLSL shaders: ")); if (platform->supports(GLSL)) { if (platform->supports(LimitedGLSL)) { support.append(QStringLiteral(" limited\n")); } else { support.append(QStringLiteral(" yes\n")); } } else { support.append(QStringLiteral(" no\n")); } support.append(QStringLiteral("Texture NPOT support: ")); if (platform->supports(TextureNPOT)) { if (platform->supports(LimitedNPOT)) { support.append(QStringLiteral(" limited\n")); } else { support.append(QStringLiteral(" yes\n")); } } else { support.append(QStringLiteral(" no\n")); } support.append(QStringLiteral("Virtual Machine: ")); if (platform->isVirtualMachine()) { support.append(QStringLiteral(" yes\n")); } else { support.append(QStringLiteral(" no\n")); } support.append(QStringLiteral("OpenGL 2 Shaders are used\n")); support.append(QStringLiteral("Painting blocks for vertical retrace: ")); if (m_compositor->scene()->blocksForRetrace()) support.append(QStringLiteral(" yes\n")); else support.append(QStringLiteral(" no\n")); break; } case XRenderCompositing: support.append(QStringLiteral("Compositing Type: XRender\n")); break; case QPainterCompositing: support.append("Compositing Type: QPainter\n"); break; case NoCompositing: default: support.append(QStringLiteral("Something is really broken, neither OpenGL nor XRender is used")); } support.append(QStringLiteral("\nLoaded Effects:\n")); support.append(QStringLiteral( "---------------\n")); foreach (const QString &effect, static_cast(effects)->loadedEffects()) { support.append(effect + QStringLiteral("\n")); } support.append(QStringLiteral("\nCurrently Active Effects:\n")); support.append(QStringLiteral( "-------------------------\n")); foreach (const QString &effect, static_cast(effects)->activeEffects()) { support.append(effect + QStringLiteral("\n")); } support.append(QStringLiteral("\nEffect Settings:\n")); support.append(QStringLiteral( "----------------\n")); foreach (const QString &effect, static_cast(effects)->loadedEffects()) { support.append(static_cast(effects)->supportInformation(effect)); support.append(QStringLiteral("\n")); } } else { support.append(QStringLiteral("Compositing is not active\n")); } return support; } Client *Workspace::findClient(std::function func) const { if (Client *ret = Toplevel::findInList(clients, func)) { return ret; } if (Client *ret = Toplevel::findInList(desktops, func)) { return ret; } return nullptr; } AbstractClient *Workspace::findAbstractClient(std::function func) const { if (AbstractClient *ret = Toplevel::findInList(m_allClients, func)) { return ret; } if (Client *ret = Toplevel::findInList(desktops, func)) { return ret; } if (waylandServer()) { if (AbstractClient *ret = Toplevel::findInList(waylandServer()->internalClients(), func)) { return ret; } } return nullptr; } Unmanaged *Workspace::findUnmanaged(std::function func) const { return Toplevel::findInList(unmanaged, func); } Unmanaged *Workspace::findUnmanaged(xcb_window_t w) const { return findUnmanaged([w](const Unmanaged *u) { return u->window() == w; }); } Client *Workspace::findClient(Predicate predicate, xcb_window_t w) const { switch (predicate) { case Predicate::WindowMatch: return findClient([w](const Client *c) { return c->window() == w; }); case Predicate::WrapperIdMatch: return findClient([w](const Client *c) { return c->wrapperId() == w; }); case Predicate::FrameIdMatch: return findClient([w](const Client *c) { return c->frameId() == w; }); case Predicate::InputIdMatch: return findClient([w](const Client *c) { return c->inputId() == w; }); } return nullptr; } Toplevel *Workspace::findToplevel(std::function func) const { if (Client *ret = Toplevel::findInList(clients, func)) { return ret; } if (Client *ret = Toplevel::findInList(desktops, func)) { return ret; } if (Unmanaged *ret = Toplevel::findInList(unmanaged, func)) { return ret; } return nullptr; } Toplevel *Workspace::findToplevel(QWindow *w) const { if (!w) { return nullptr; } if (waylandServer()) { if (auto c = waylandServer()->findClient(w)) { return c; } } return findUnmanaged(w->winId()); } bool Workspace::hasClient(const AbstractClient *c) { if (auto cc = dynamic_cast(c)) { return hasClient(cc); } else { return findAbstractClient([c](const AbstractClient *test) { return test == c; }) != nullptr; } return false; } void Workspace::forEachAbstractClient(std::function< void (AbstractClient*) > func) { std::for_each(m_allClients.constBegin(), m_allClients.constEnd(), func); std::for_each(desktops.constBegin(), desktops.constEnd(), func); } Toplevel *Workspace::findInternal(QWindow *w) const { if (!w) { return nullptr; } if (kwinApp()->operationMode() == Application::OperationModeX11) { return findUnmanaged(w->winId()); } else { return waylandServer()->findClient(w); } } bool Workspace::compositing() const { return m_compositor && m_compositor->hasScene(); } void Workspace::markXStackingOrderAsDirty() { m_xStackingDirty = true; if (kwinApp()->x11Connection()) { m_xStackingQueryTree.reset(new Xcb::Tree(kwinApp()->x11RootWindow())); } } void Workspace::setWasUserInteraction() { if (was_user_interaction) { return; } was_user_interaction = true; // might be called from within the filter, so delay till we now the filter returned QTimer::singleShot(0, this, [this] { m_wasUserInteractionFilter.reset(); } ); } } // namespace