diff --git a/autotests/integration/internal_window.cpp b/autotests/integration/internal_window.cpp index 083045208..bacdc2030 100644 --- a/autotests/integration/internal_window.cpp +++ b/autotests/integration/internal_window.cpp @@ -1,693 +1,719 @@ /******************************************************************** KWin - the KDE window manager This file is part of the KDE project. Copyright (C) 2016 Martin Gräßlin This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . *********************************************************************/ #include "kwin_wayland_test.h" #include "platform.h" #include "cursor.h" #include "shell_client.h" #include "screens.h" #include "wayland_server.h" #include "workspace.h" #include #include #include #include #include #include +#include + #include using namespace KWayland::Client; namespace KWin { static const QString s_socketName = QStringLiteral("wayland_test_kwin_internal_window-0"); class InternalWindowTest : public QObject { Q_OBJECT private Q_SLOTS: void initTestCase(); void init(); void cleanup(); void testEnterLeave(); void testPointerPressRelease(); void testPointerAxis(); void testKeyboard_data(); void testKeyboard(); void testKeyboardShowWithoutActivating(); void testKeyboardTriggersLeave(); void testTouch(); void testOpacity(); void testMove(); void testSkipCloseAnimation_data(); void testSkipCloseAnimation(); void testModifierClickUnrestrictedMove(); void testModifierScroll(); void testPopup(); + void testScale(); }; class HelperWindow : public QRasterWindow { Q_OBJECT public: HelperWindow(); ~HelperWindow(); QPoint latestGlobalMousePos() const { return m_latestGlobalMousePos; } Qt::MouseButtons pressedButtons() const { return m_pressedButtons; } Q_SIGNALS: void entered(); void left(); void mouseMoved(const QPoint &global); void mousePressed(); void mouseReleased(); void wheel(); void keyPressed(); void keyReleased(); protected: void paintEvent(QPaintEvent *event) override; bool event(QEvent *event) override; void mouseMoveEvent(QMouseEvent *event) override; void mousePressEvent(QMouseEvent *event) override; void mouseReleaseEvent(QMouseEvent *event) override; void wheelEvent(QWheelEvent *event) override; void keyPressEvent(QKeyEvent *event) override; void keyReleaseEvent(QKeyEvent *event) override; private: QPoint m_latestGlobalMousePos; Qt::MouseButtons m_pressedButtons = Qt::MouseButtons(); }; HelperWindow::HelperWindow() : QRasterWindow(nullptr) { setFlags(Qt::FramelessWindowHint); } HelperWindow::~HelperWindow() = default; void HelperWindow::paintEvent(QPaintEvent *event) { Q_UNUSED(event) QPainter p(this); p.fillRect(0, 0, width(), height(), Qt::red); } bool HelperWindow::event(QEvent *event) { if (event->type() == QEvent::Enter) { emit entered(); } if (event->type() == QEvent::Leave) { emit left(); } return QRasterWindow::event(event); } void HelperWindow::mouseMoveEvent(QMouseEvent *event) { m_latestGlobalMousePos = event->globalPos(); emit mouseMoved(event->globalPos()); } void HelperWindow::mousePressEvent(QMouseEvent *event) { m_latestGlobalMousePos = event->globalPos(); m_pressedButtons = event->buttons(); emit mousePressed(); } void HelperWindow::mouseReleaseEvent(QMouseEvent *event) { m_latestGlobalMousePos = event->globalPos(); m_pressedButtons = event->buttons(); emit mouseReleased(); } void HelperWindow::wheelEvent(QWheelEvent *event) { Q_UNUSED(event) emit wheel(); } void HelperWindow::keyPressEvent(QKeyEvent *event) { Q_UNUSED(event) emit keyPressed(); } void HelperWindow::keyReleaseEvent(QKeyEvent *event) { Q_UNUSED(event) emit keyReleased(); } void InternalWindowTest::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()->setConfig(KSharedConfig::openConfig(QString(), KConfig::SimpleConfig)); 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 InternalWindowTest::init() { Cursor::setPos(QPoint(1280, 512)); QVERIFY(Test::setupWaylandConnection(Test::AdditionalWaylandInterface::Seat)); QVERIFY(Test::waitForWaylandKeyboard()); } void InternalWindowTest::cleanup() { Test::destroyWaylandConnection(); } void InternalWindowTest::testEnterLeave() { QSignalSpy clientAddedSpy(waylandServer(), &WaylandServer::shellClientAdded); QVERIFY(clientAddedSpy.isValid()); HelperWindow win; QVERIFY(!workspace()->findToplevel(nullptr)); QVERIFY(!workspace()->findToplevel(&win)); win.setGeometry(0, 0, 100, 100); win.show(); QVERIFY(clientAddedSpy.wait()); QCOMPARE(clientAddedSpy.count(), 1); QVERIFY(!workspace()->activeClient()); ShellClient *c = clientAddedSpy.first().first().value(); QVERIFY(c->isInternal()); QCOMPARE(c->icon().name(), QStringLiteral("wayland")); QVERIFY(!c->isDecorated()); QCOMPARE(workspace()->findToplevel(&win), c); QCOMPARE(c->geometry(), QRect(0, 0, 100, 100)); QVERIFY(c->isShown(false)); QVERIFY(workspace()->xStackingOrder().contains(c)); QSignalSpy enterSpy(&win, &HelperWindow::entered); QVERIFY(enterSpy.isValid()); QSignalSpy leaveSpy(&win, &HelperWindow::left); QVERIFY(leaveSpy.isValid()); QSignalSpy moveSpy(&win, &HelperWindow::mouseMoved); QVERIFY(moveSpy.isValid()); quint32 timestamp = 1; kwinApp()->platform()->pointerMotion(QPoint(50, 50), timestamp++); QTRY_COMPARE(moveSpy.count(), 1); kwinApp()->platform()->pointerMotion(QPoint(60, 50), timestamp++); QTRY_COMPARE(moveSpy.count(), 2); QCOMPARE(moveSpy[1].first().toPoint(), QPoint(60, 50)); kwinApp()->platform()->pointerMotion(QPoint(101, 50), timestamp++); QTRY_COMPARE(leaveSpy.count(), 1); // set a mask on the window win.setMask(QRegion(10, 20, 30, 40)); // outside the mask we should not get an enter kwinApp()->platform()->pointerMotion(QPoint(5, 5), timestamp++); QVERIFY(!enterSpy.wait(100)); QCOMPARE(enterSpy.count(), 1); // inside the mask we should still get an enter kwinApp()->platform()->pointerMotion(QPoint(25, 27), timestamp++); QTRY_COMPARE(enterSpy.count(), 2); // hide the window, which should be removed from the stacking order win.hide(); QTRY_VERIFY(!c->isShown(false)); QVERIFY(!workspace()->xStackingOrder().contains(c)); // show again win.show(); QTRY_VERIFY(c->isShown(false)); QVERIFY(workspace()->xStackingOrder().contains(c)); } void InternalWindowTest::testPointerPressRelease() { QSignalSpy clientAddedSpy(waylandServer(), &WaylandServer::shellClientAdded); QVERIFY(clientAddedSpy.isValid()); HelperWindow win; win.setGeometry(0, 0, 100, 100); win.show(); QSignalSpy pressSpy(&win, &HelperWindow::mousePressed); QVERIFY(pressSpy.isValid()); QSignalSpy releaseSpy(&win, &HelperWindow::mouseReleased); QVERIFY(releaseSpy.isValid()); QVERIFY(clientAddedSpy.wait()); QCOMPARE(clientAddedSpy.count(), 1); quint32 timestamp = 1; kwinApp()->platform()->pointerMotion(QPoint(50, 50), timestamp++); kwinApp()->platform()->pointerButtonPressed(BTN_LEFT, timestamp++); QTRY_COMPARE(pressSpy.count(), 1); kwinApp()->platform()->pointerButtonReleased(BTN_LEFT, timestamp++); QTRY_COMPARE(releaseSpy.count(), 1); } void InternalWindowTest::testPointerAxis() { QSignalSpy clientAddedSpy(waylandServer(), &WaylandServer::shellClientAdded); QVERIFY(clientAddedSpy.isValid()); HelperWindow win; win.setGeometry(0, 0, 100, 100); win.show(); QSignalSpy wheelSpy(&win, &HelperWindow::wheel); QVERIFY(wheelSpy.isValid()); QVERIFY(clientAddedSpy.wait()); QCOMPARE(clientAddedSpy.count(), 1); quint32 timestamp = 1; kwinApp()->platform()->pointerMotion(QPoint(50, 50), timestamp++); kwinApp()->platform()->pointerAxisVertical(5.0, timestamp++); QTRY_COMPARE(wheelSpy.count(), 1); kwinApp()->platform()->pointerAxisHorizontal(5.0, timestamp++); QTRY_COMPARE(wheelSpy.count(), 2); } void InternalWindowTest::testKeyboard_data() { QTest::addColumn("cursorPos"); QTest::newRow("on Window") << QPoint(50, 50); QTest::newRow("outside Window") << QPoint(250, 250); } void InternalWindowTest::testKeyboard() { QSignalSpy clientAddedSpy(waylandServer(), &WaylandServer::shellClientAdded); QVERIFY(clientAddedSpy.isValid()); HelperWindow win; win.setGeometry(0, 0, 100, 100); win.show(); QSignalSpy pressSpy(&win, &HelperWindow::keyPressed); QVERIFY(pressSpy.isValid()); QSignalSpy releaseSpy(&win, &HelperWindow::keyReleased); QVERIFY(releaseSpy.isValid()); QVERIFY(clientAddedSpy.wait()); QCOMPARE(clientAddedSpy.count(), 1); auto internalClient = clientAddedSpy.first().first().value(); QVERIFY(internalClient); QVERIFY(internalClient->isInternal()); QVERIFY(internalClient->readyForPainting()); quint32 timestamp = 1; QFETCH(QPoint, cursorPos); kwinApp()->platform()->pointerMotion(cursorPos, timestamp++); kwinApp()->platform()->keyboardKeyPressed(KEY_A, timestamp++); QTRY_COMPARE(pressSpy.count(), 1); QCOMPARE(releaseSpy.count(), 0); kwinApp()->platform()->keyboardKeyReleased(KEY_A, timestamp++); QTRY_COMPARE(releaseSpy.count(), 1); QCOMPARE(pressSpy.count(), 1); } void InternalWindowTest::testKeyboardShowWithoutActivating() { QSignalSpy clientAddedSpy(waylandServer(), &WaylandServer::shellClientAdded); QVERIFY(clientAddedSpy.isValid()); HelperWindow win; win.setProperty("_q_showWithoutActivating", true); win.setGeometry(0, 0, 100, 100); win.show(); QSignalSpy pressSpy(&win, &HelperWindow::keyPressed); QVERIFY(pressSpy.isValid()); QSignalSpy releaseSpy(&win, &HelperWindow::keyReleased); QVERIFY(releaseSpy.isValid()); QVERIFY(clientAddedSpy.wait()); QCOMPARE(clientAddedSpy.count(), 1); auto internalClient = clientAddedSpy.first().first().value(); QVERIFY(internalClient); QVERIFY(internalClient->isInternal()); QVERIFY(internalClient->readyForPainting()); quint32 timestamp = 1; const QPoint cursorPos = QPoint(50, 50); kwinApp()->platform()->pointerMotion(cursorPos, timestamp++); kwinApp()->platform()->keyboardKeyPressed(KEY_A, timestamp++); QCOMPARE(pressSpy.count(), 0); QVERIFY(!pressSpy.wait(100)); QCOMPARE(releaseSpy.count(), 0); kwinApp()->platform()->keyboardKeyReleased(KEY_A, timestamp++); QCOMPARE(releaseSpy.count(), 0); QVERIFY(!releaseSpy.wait(100)); QCOMPARE(pressSpy.count(), 0); } void InternalWindowTest::testKeyboardTriggersLeave() { // this test verifies that a leave event is sent to a client when an internal window // gets a key event QScopedPointer keyboard(Test::waylandSeat()->createKeyboard()); QVERIFY(!keyboard.isNull()); QVERIFY(keyboard->isValid()); QSignalSpy enteredSpy(keyboard.data(), &Keyboard::entered); QVERIFY(enteredSpy.isValid()); QSignalSpy leftSpy(keyboard.data(), &Keyboard::left); QVERIFY(leftSpy.isValid()); QScopedPointer surface(Test::createSurface()); QScopedPointer shellSurface(Test::createShellSurface(Test::ShellSurfaceType::WlShell, surface.data())); // now let's render auto c = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); QVERIFY(c); QVERIFY(c->isActive()); QVERIFY(!c->isInternal()); if (enteredSpy.isEmpty()) { QVERIFY(enteredSpy.wait()); } QCOMPARE(enteredSpy.count(), 1); // create internal window QSignalSpy clientAddedSpy(waylandServer(), &WaylandServer::shellClientAdded); QVERIFY(clientAddedSpy.isValid()); HelperWindow win; win.setGeometry(0, 0, 100, 100); win.show(); QSignalSpy pressSpy(&win, &HelperWindow::keyPressed); QVERIFY(pressSpy.isValid()); QSignalSpy releaseSpy(&win, &HelperWindow::keyReleased); QVERIFY(releaseSpy.isValid()); QVERIFY(clientAddedSpy.wait()); QCOMPARE(clientAddedSpy.count(), 1); auto internalClient = clientAddedSpy.first().first().value(); QVERIFY(internalClient); QVERIFY(internalClient->isInternal()); QVERIFY(internalClient->readyForPainting()); QVERIFY(leftSpy.isEmpty()); QVERIFY(!leftSpy.wait(100)); // now let's trigger a key, which should result in a leave quint32 timestamp = 1; kwinApp()->platform()->keyboardKeyPressed(KEY_A, timestamp++); QVERIFY(leftSpy.wait()); QCOMPARE(pressSpy.count(), 1); kwinApp()->platform()->keyboardKeyReleased(KEY_A, timestamp++); QTRY_COMPARE(releaseSpy.count(), 1); // after hiding the internal window, next key press should trigger an enter win.hide(); kwinApp()->platform()->keyboardKeyPressed(KEY_A, timestamp++); QVERIFY(enteredSpy.wait()); kwinApp()->platform()->keyboardKeyReleased(KEY_A, timestamp++); } void InternalWindowTest::testTouch() { // touch events for internal windows are emulated through mouse events QSignalSpy clientAddedSpy(waylandServer(), &WaylandServer::shellClientAdded); QVERIFY(clientAddedSpy.isValid()); HelperWindow win; win.setGeometry(0, 0, 100, 100); win.show(); QVERIFY(clientAddedSpy.wait()); QCOMPARE(clientAddedSpy.count(), 1); QSignalSpy pressSpy(&win, &HelperWindow::mousePressed); QVERIFY(pressSpy.isValid()); QSignalSpy releaseSpy(&win, &HelperWindow::mouseReleased); QVERIFY(releaseSpy.isValid()); QSignalSpy moveSpy(&win, &HelperWindow::mouseMoved); QVERIFY(moveSpy.isValid()); quint32 timestamp = 1; QCOMPARE(win.pressedButtons(), Qt::MouseButtons()); kwinApp()->platform()->touchDown(0, QPointF(50, 50), timestamp++); QCOMPARE(pressSpy.count(), 1); QCOMPARE(win.latestGlobalMousePos(), QPoint(50, 50)); QCOMPARE(win.pressedButtons(), Qt::MouseButtons(Qt::LeftButton)); // further touch down should not trigger kwinApp()->platform()->touchDown(1, QPointF(75, 75), timestamp++); QCOMPARE(pressSpy.count(), 1); kwinApp()->platform()->touchUp(1, timestamp++); QCOMPARE(releaseSpy.count(), 0); QCOMPARE(win.latestGlobalMousePos(), QPoint(50, 50)); QCOMPARE(win.pressedButtons(), Qt::MouseButtons(Qt::LeftButton)); // another press kwinApp()->platform()->touchDown(1, QPointF(10, 10), timestamp++); QCOMPARE(pressSpy.count(), 1); QCOMPARE(win.latestGlobalMousePos(), QPoint(50, 50)); QCOMPARE(win.pressedButtons(), Qt::MouseButtons(Qt::LeftButton)); // simulate the move QCOMPARE(moveSpy.count(), 0); kwinApp()->platform()->touchMotion(0, QPointF(80, 90), timestamp++); QCOMPARE(moveSpy.count(), 1); QCOMPARE(win.latestGlobalMousePos(), QPoint(80, 90)); QCOMPARE(win.pressedButtons(), Qt::MouseButtons(Qt::LeftButton)); // move on other ID should not do anything kwinApp()->platform()->touchMotion(1, QPointF(20, 30), timestamp++); QCOMPARE(moveSpy.count(), 1); QCOMPARE(win.latestGlobalMousePos(), QPoint(80, 90)); QCOMPARE(win.pressedButtons(), Qt::MouseButtons(Qt::LeftButton)); // now up our main point kwinApp()->platform()->touchUp(0, timestamp++); QCOMPARE(releaseSpy.count(), 1); QCOMPARE(win.latestGlobalMousePos(), QPoint(80, 90)); QCOMPARE(win.pressedButtons(), Qt::MouseButtons()); // and up the additional point kwinApp()->platform()->touchUp(1, timestamp++); QCOMPARE(releaseSpy.count(), 1); QCOMPARE(moveSpy.count(), 1); QCOMPARE(win.latestGlobalMousePos(), QPoint(80, 90)); QCOMPARE(win.pressedButtons(), Qt::MouseButtons()); } void InternalWindowTest::testOpacity() { // this test verifies that opacity is properly synced from QWindow to ShellClient QSignalSpy clientAddedSpy(waylandServer(), &WaylandServer::shellClientAdded); QVERIFY(clientAddedSpy.isValid()); HelperWindow win; win.setOpacity(0.5); win.setGeometry(0, 0, 100, 100); win.show(); QVERIFY(clientAddedSpy.wait()); QCOMPARE(clientAddedSpy.count(), 1); auto internalClient = clientAddedSpy.first().first().value(); QVERIFY(internalClient); QVERIFY(internalClient->isInternal()); QCOMPARE(internalClient->opacity(), 0.5); QSignalSpy opacityChangedSpy(internalClient, &ShellClient::opacityChanged); QVERIFY(opacityChangedSpy.isValid()); win.setOpacity(0.75); QCOMPARE(opacityChangedSpy.count(), 1); QCOMPARE(internalClient->opacity(), 0.75); } void InternalWindowTest::testMove() { QSignalSpy clientAddedSpy(waylandServer(), &WaylandServer::shellClientAdded); QVERIFY(clientAddedSpy.isValid()); HelperWindow win; win.setOpacity(0.5); win.setGeometry(0, 0, 100, 100); win.show(); QVERIFY(clientAddedSpy.wait()); QCOMPARE(clientAddedSpy.count(), 1); auto internalClient = clientAddedSpy.first().first().value(); QVERIFY(internalClient); QCOMPARE(internalClient->geometry(), QRect(0, 0, 100, 100)); // normal move should be synced internalClient->move(5, 10); QCOMPARE(internalClient->geometry(), QRect(5, 10, 100, 100)); QTRY_COMPARE(win.geometry(), QRect(5, 10, 100, 100)); // another move should also be synced internalClient->move(10, 20); QCOMPARE(internalClient->geometry(), QRect(10, 20, 100, 100)); QTRY_COMPARE(win.geometry(), QRect(10, 20, 100, 100)); // now move with a Geometry update blocker { GeometryUpdatesBlocker blocker(internalClient); internalClient->move(5, 10); // not synced! QCOMPARE(win.geometry(), QRect(10, 20, 100, 100)); } // after destroying the blocker it should be synced QTRY_COMPARE(win.geometry(), QRect(5, 10, 100, 100)); } void InternalWindowTest::testSkipCloseAnimation_data() { QTest::addColumn("initial"); QTest::newRow("set") << true; QTest::newRow("not set") << false; } void InternalWindowTest::testSkipCloseAnimation() { QSignalSpy clientAddedSpy(waylandServer(), &WaylandServer::shellClientAdded); QVERIFY(clientAddedSpy.isValid()); HelperWindow win; win.setOpacity(0.5); win.setGeometry(0, 0, 100, 100); QFETCH(bool, initial); win.setProperty("KWIN_SKIP_CLOSE_ANIMATION", initial); win.show(); QVERIFY(clientAddedSpy.wait()); QCOMPARE(clientAddedSpy.count(), 1); auto internalClient = clientAddedSpy.first().first().value(); QVERIFY(internalClient); QCOMPARE(internalClient->skipsCloseAnimation(), initial); QSignalSpy skipCloseChangedSpy(internalClient, &Toplevel::skipCloseAnimationChanged); QVERIFY(skipCloseChangedSpy.isValid()); win.setProperty("KWIN_SKIP_CLOSE_ANIMATION", !initial); QCOMPARE(skipCloseChangedSpy.count(), 1); QCOMPARE(internalClient->skipsCloseAnimation(), !initial); win.setProperty("KWIN_SKIP_CLOSE_ANIMATION", initial); QCOMPARE(skipCloseChangedSpy.count(), 2); QCOMPARE(internalClient->skipsCloseAnimation(), initial); } void InternalWindowTest::testModifierClickUnrestrictedMove() { QSignalSpy clientAddedSpy(waylandServer(), &WaylandServer::shellClientAdded); QVERIFY(clientAddedSpy.isValid()); HelperWindow win; win.setGeometry(0, 0, 100, 100); win.setFlags(win.flags() & ~Qt::FramelessWindowHint); win.show(); QVERIFY(clientAddedSpy.wait()); QCOMPARE(clientAddedSpy.count(), 1); auto internalClient = clientAddedSpy.first().first().value(); QVERIFY(internalClient); QVERIFY(internalClient->isDecorated()); KConfigGroup group = kwinApp()->config()->group("MouseBindings"); group.writeEntry("CommandAllKey", "Alt"); group.writeEntry("CommandAll1", "Move"); group.writeEntry("CommandAll2", "Move"); group.writeEntry("CommandAll3", "Move"); group.sync(); workspace()->slotReconfigure(); QCOMPARE(options->commandAllModifier(), Qt::AltModifier); QCOMPARE(options->commandAll1(), Options::MouseUnrestrictedMove); QCOMPARE(options->commandAll2(), Options::MouseUnrestrictedMove); QCOMPARE(options->commandAll3(), Options::MouseUnrestrictedMove); // move cursor on window Cursor::setPos(internalClient->geometry().center()); // simulate modifier+click quint32 timestamp = 1; kwinApp()->platform()->keyboardKeyPressed(KEY_LEFTALT, timestamp++); QVERIFY(!internalClient->isMove()); kwinApp()->platform()->pointerButtonPressed(BTN_LEFT, timestamp++); QVERIFY(internalClient->isMove()); // release modifier should not change it kwinApp()->platform()->keyboardKeyReleased(KEY_LEFTALT, timestamp++); QVERIFY(internalClient->isMove()); // but releasing the key should end move/resize kwinApp()->platform()->pointerButtonReleased(BTN_LEFT, timestamp++); QVERIFY(!internalClient->isMove()); } void InternalWindowTest::testModifierScroll() { QSignalSpy clientAddedSpy(waylandServer(), &WaylandServer::shellClientAdded); QVERIFY(clientAddedSpy.isValid()); HelperWindow win; win.setGeometry(0, 0, 100, 100); win.setFlags(win.flags() & ~Qt::FramelessWindowHint); win.show(); QVERIFY(clientAddedSpy.wait()); QCOMPARE(clientAddedSpy.count(), 1); auto internalClient = clientAddedSpy.first().first().value(); QVERIFY(internalClient); QVERIFY(internalClient->isDecorated()); KConfigGroup group = kwinApp()->config()->group("MouseBindings"); group.writeEntry("CommandAllKey", "Alt"); group.writeEntry("CommandAllWheel", "change opacity"); group.sync(); workspace()->slotReconfigure(); // move cursor on window Cursor::setPos(internalClient->geometry().center()); // set the opacity to 0.5 internalClient->setOpacity(0.5); QCOMPARE(internalClient->opacity(), 0.5); quint32 timestamp = 1; kwinApp()->platform()->keyboardKeyPressed(KEY_LEFTALT, timestamp++); kwinApp()->platform()->pointerAxisVertical(-5, timestamp++); QCOMPARE(internalClient->opacity(), 0.6); kwinApp()->platform()->pointerAxisVertical(5, timestamp++); QCOMPARE(internalClient->opacity(), 0.5); kwinApp()->platform()->keyboardKeyReleased(KEY_LEFTALT, timestamp++); } void InternalWindowTest::testPopup() { QSignalSpy clientAddedSpy(waylandServer(), &WaylandServer::shellClientAdded); QVERIFY(clientAddedSpy.isValid()); HelperWindow win; win.setGeometry(0, 0, 100, 100); win.setFlags(win.flags() | Qt::Popup); win.show(); QVERIFY(clientAddedSpy.wait()); QCOMPARE(clientAddedSpy.count(), 1); auto internalClient = clientAddedSpy.first().first().value(); QVERIFY(internalClient); QCOMPARE(internalClient->isPopupWindow(), true); } +void InternalWindowTest::testScale() +{ + QMetaObject::invokeMethod(kwinApp()->platform(), "setVirtualOutputs", Qt::DirectConnection, + Q_ARG(int, 2), + Q_ARG(QVector, QVector({QRect(0,0,1280, 1024), QRect(1280/2, 0, 1280, 1024)})), + Q_ARG(QVector, QVector({2,2}))); + + QSignalSpy clientAddedSpy(waylandServer(), &WaylandServer::shellClientAdded); + QVERIFY(clientAddedSpy.isValid()); + HelperWindow win; + win.setGeometry(0, 0, 100, 100); + win.setFlags(win.flags() | Qt::Popup); + win.show(); + QCOMPARE(win.devicePixelRatio(), 2.0); + QVERIFY(clientAddedSpy.wait()); + QCOMPARE(clientAddedSpy.count(), 1); + auto internalClient = clientAddedSpy.first().first().value(); + QCOMPARE(internalClient->surface()->scale(), 2); + + QMetaObject::invokeMethod(kwinApp()->platform(), "setVirtualOutputs", Qt::DirectConnection, Q_ARG(int, 2)); +} + + } WAYLANDTEST_MAIN(KWin::InternalWindowTest) #include "internal_window.moc" diff --git a/autotests/integration/kwin_wayland_test.h b/autotests/integration/kwin_wayland_test.h index 4c7b183c3..6d62e0a7c 100644 --- a/autotests/integration/kwin_wayland_test.h +++ b/autotests/integration/kwin_wayland_test.h @@ -1,215 +1,215 @@ /******************************************************************** KWin - the KDE window manager This file is part of the KDE project. Copyright (C) 2015 Martin Gräßlin This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . *********************************************************************/ #ifndef KWIN_WAYLAND_TEST_H #define KWIN_WAYLAND_TEST_H #include "../../main.h" // Qt #include // KWayland #include namespace KWayland { namespace Client { class AppMenuManager; class ConnectionThread; class Compositor; class IdleInhibitManager; class PlasmaShell; class PlasmaWindowManagement; class PointerConstraints; class Seat; class ServerSideDecorationManager; class ShadowManager; class Shell; class ShellSurface; class ShmPool; class Surface; class XdgDecorationManager; } } namespace KWin { class AbstractClient; class ShellClient; class WaylandTestApplication : public Application { Q_OBJECT public: WaylandTestApplication(OperationMode mode, int &argc, char **argv); virtual ~WaylandTestApplication(); protected: void performStartup() override; private: void createBackend(); void createX11Connection(); void continueStartupWithScreens(); void continueStartupWithSceen(); void continueStartupWithX(); void startXwaylandServer(); int m_xcbConnectionFd = -1; QProcess *m_xwaylandProcess = nullptr; QMetaObject::Connection m_xwaylandFailConnection; }; namespace Test { enum class AdditionalWaylandInterface { Seat = 1 << 0, Decoration = 1 << 1, PlasmaShell = 1 << 2, WindowManagement = 1 << 3, PointerConstraints = 1 << 4, IdleInhibition = 1 << 5, AppMenu = 1 << 6, ShadowManager = 1 << 7, XdgDecoration = 1 << 8, }; Q_DECLARE_FLAGS(AdditionalWaylandInterfaces, AdditionalWaylandInterface) /** * Creates a Wayland Connection in a dedicated thread and creates various * client side objects which can be used to create windows. * @returns @c true if created successfully, @c false if there was an error * @see destroyWaylandConnection **/ bool setupWaylandConnection(AdditionalWaylandInterfaces flags = AdditionalWaylandInterfaces()); /** * Destroys the Wayland Connection created with @link{setupWaylandConnection}. * This can be called from cleanup in order to ensure that no Wayland Connection * leaks into the next test method. * @see setupWaylandConnection */ void destroyWaylandConnection(); KWayland::Client::ConnectionThread *waylandConnection(); KWayland::Client::Compositor *waylandCompositor(); KWayland::Client::ShadowManager *waylandShadowManager(); KWayland::Client::Shell *waylandShell(); KWayland::Client::ShmPool *waylandShmPool(); KWayland::Client::Seat *waylandSeat(); KWayland::Client::ServerSideDecorationManager *waylandServerSideDecoration(); KWayland::Client::PlasmaShell *waylandPlasmaShell(); KWayland::Client::PlasmaWindowManagement *waylandWindowManagement(); KWayland::Client::PointerConstraints *waylandPointerConstraints(); KWayland::Client::IdleInhibitManager *waylandIdleInhibitManager(); KWayland::Client::AppMenuManager *waylandAppMenuManager(); KWayland::Client::XdgDecorationManager *xdgDecorationManager(); bool waitForWaylandPointer(); bool waitForWaylandTouch(); bool waitForWaylandKeyboard(); void flushWaylandConnection(); KWayland::Client::Surface *createSurface(QObject *parent = nullptr); enum class ShellSurfaceType { WlShell, XdgShellV5, XdgShellV6, XdgShellStable }; QObject *createShellSurface(ShellSurfaceType type, KWayland::Client::Surface *surface, QObject *parent = nullptr); KWayland::Client::ShellSurface *createShellSurface(KWayland::Client::Surface *surface, QObject *parent = nullptr); KWayland::Client::XdgShellSurface *createXdgShellV5Surface(KWayland::Client::Surface *surface, QObject *parent = nullptr); KWayland::Client::XdgShellSurface *createXdgShellV6Surface(KWayland::Client::Surface *surface, QObject *parent = nullptr); KWayland::Client::XdgShellSurface *createXdgShellStableSurface(KWayland::Client::Surface *surface, QObject *parent = nullptr); KWayland::Client::XdgShellPopup *createXdgShellStablePopup(KWayland::Client::Surface *surface, KWayland::Client::XdgShellSurface *parentSurface, const KWayland::Client::XdgPositioner &positioner, QObject *parent = nullptr); /** * Creates a shared memory buffer of @p size in @p color and attaches it to the @p surface. * The @p surface gets damaged and committed, thus it's rendered. **/ void render(KWayland::Client::Surface *surface, const QSize &size, const QColor &color, const QImage::Format &format = QImage::Format_ARGB32_Premultiplied); /** * Creates a shared memory buffer using the supplied image @p img and attaches it to the @p surface */ void render(KWayland::Client::Surface *surface, const QImage &img); /** * Waits till a new ShellClient is shown and returns the created ShellClient. * If no ShellClient gets shown during @p timeout @c null is returned. **/ ShellClient *waitForWaylandWindowShown(int timeout = 5000); /** * Combination of @link{render} and @link{waitForWaylandWindowShown}. **/ ShellClient *renderAndWaitForShown(KWayland::Client::Surface *surface, const QSize &size, const QColor &color, const QImage::Format &format = QImage::Format_ARGB32, int timeout = 5000); /** * Waits for the @p client to be destroyed. **/ bool waitForWindowDestroyed(AbstractClient *client); /** * Locks the screen and waits till the screen is locked. * @returns @c true if the screen could be locked, @c false otherwise **/ bool lockScreen(); /** * Unlocks the screen and waits till the screen is unlocked. * @returns @c true if the screen could be unlocked, @c false otherwise **/ bool unlockScreen(); } } Q_DECLARE_OPERATORS_FOR_FLAGS(KWin::Test::AdditionalWaylandInterfaces) Q_DECLARE_METATYPE(KWin::Test::ShellSurfaceType) #define WAYLANDTEST_MAIN_HELPER(TestObject, DPI, OperationMode) \ int main(int argc, char *argv[]) \ { \ setenv("QT_QPA_PLATFORM", "wayland-org.kde.kwin.qpa", true); \ setenv("QT_QPA_PLATFORM_PLUGIN_PATH", QFileInfo(QString::fromLocal8Bit(argv[0])).absolutePath().toLocal8Bit().constData(), true); \ setenv("KWIN_FORCE_OWN_QPA", "1", true); \ qunsetenv("KDE_FULL_SESSION"); \ qunsetenv("KDE_SESSION_VERSION"); \ qunsetenv("XDG_SESSION_DESKTOP"); \ qunsetenv("XDG_CURRENT_DESKTOP"); \ DPI; \ KWin::WaylandTestApplication app(OperationMode, argc, argv); \ app.setAttribute(Qt::AA_Use96Dpi, true); \ TestObject tc; \ return QTest::qExec(&tc, argc, argv); \ } #ifdef NO_XWAYLAND -#define WAYLANDTEST_MAIN(TestObject) WAYLANDTEST_MAIN_HELPER(TestObject, QCoreApplication::setAttribute(Qt::AA_DisableHighDpiScaling), KWin::Application::OperationModeWaylandOnly) +#define WAYLANDTEST_MAIN(TestObject) WAYLANDTEST_MAIN_HELPER(TestObject, QCoreApplication::setAttribute(Qt::AA_UseHighDpiPixmaps), KWin::Application::OperationModeWaylandOnly) #else -#define WAYLANDTEST_MAIN(TestObject) WAYLANDTEST_MAIN_HELPER(TestObject, QCoreApplication::setAttribute(Qt::AA_DisableHighDpiScaling), KWin::Application::OperationModeXwayland) +#define WAYLANDTEST_MAIN(TestObject) WAYLANDTEST_MAIN_HELPER(TestObject, QCoreApplication::setAttribute(Qt::AA_UseHighDpiPixmaps), KWin::Application::OperationModeXwayland) #endif #endif diff --git a/main_wayland.cpp b/main_wayland.cpp index f2cd47517..cd8a98c9f 100644 --- a/main_wayland.cpp +++ b/main_wayland.cpp @@ -1,831 +1,831 @@ /******************************************************************** KWin - the KDE window manager This file is part of the KDE project. Copyright (C) 2014 Martin Gräßlin This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . *********************************************************************/ #include "main_wayland.h" #include "composite.h" #include "virtualkeyboard.h" #include "workspace.h" #include // kwin #include "platform.h" #include "effects.h" #include "tabletmodemanager.h" #include "wayland_server.h" #include "xcbutils.h" // KWayland #include #include // KDE #include #include #include #include #include // Qt #include #include #include #include #include #include #include #include #include #include #include #include #include // system #ifdef HAVE_UNISTD_H #include #endif // HAVE_UNISTD_H #if HAVE_SYS_PRCTL_H #include #endif #if HAVE_SYS_PROCCTL_H #include #include #endif #if HAVE_LIBCAP #include #endif #include #include #include namespace KWin { static void sighandler(int) { QApplication::exit(); } void disableDrKonqi() { KCrash::setDrKonqiEnabled(false); } // run immediately, before Q_CORE_STARTUP functions // that would enable drkonqi Q_CONSTRUCTOR_FUNCTION(disableDrKonqi) static void readDisplay(int pipe); enum class RealTimeFlags { DontReset, ResetOnFork }; namespace { void gainRealTime(RealTimeFlags flags = RealTimeFlags::DontReset) { #if HAVE_SCHED_RESET_ON_FORK const int minPriority = sched_get_priority_min(SCHED_RR); struct sched_param sp; sp.sched_priority = minPriority; int policy = SCHED_RR; if (flags == RealTimeFlags::ResetOnFork) { policy |= SCHED_RESET_ON_FORK; } sched_setscheduler(0, policy, &sp); #else Q_UNUSED(flags); #endif } } //************************************ // ApplicationWayland //************************************ ApplicationWayland::ApplicationWayland(int &argc, char **argv) : Application(OperationModeWaylandOnly, argc, argv) { } ApplicationWayland::~ApplicationWayland() { setTerminating(); if (!waylandServer()) { return; } if (kwinApp()->platform()) { kwinApp()->platform()->setOutputsEnabled(false); } // need to unload all effects prior to destroying X connection as they might do X calls if (effects) { static_cast(effects)->unloadAllEffects(); } destroyWorkspace(); waylandServer()->dispatch(); disconnect(m_xwaylandFailConnection); if (x11Connection()) { Xcb::setInputFocus(XCB_INPUT_FOCUS_POINTER_ROOT); destroyAtoms(); emit x11ConnectionAboutToBeDestroyed(); xcb_disconnect(x11Connection()); setX11Connection(nullptr); } if (m_xwaylandProcess) { m_xwaylandProcess->terminate(); while (m_xwaylandProcess->state() != QProcess::NotRunning) { processEvents(QEventLoop::WaitForMoreEvents); } waylandServer()->destroyXWaylandConnection(); } if (QStyle *s = style()) { s->unpolish(this); } waylandServer()->terminateClientConnections(); destroyCompositor(); } void ApplicationWayland::performStartup() { if (m_startXWayland) { setOperationMode(OperationModeXwayland); } // first load options - done internally by a different thread createOptions(); waylandServer()->createInternalConnection(); // try creating the Wayland Backend createInput(); // now libinput thread has been created, adjust scheduler to not leak into other processes gainRealTime(RealTimeFlags::ResetOnFork); VirtualKeyboard::create(this); createBackend(); TabletModeManager::create(this); } void ApplicationWayland::createBackend() { connect(platform(), &Platform::screensQueried, this, &ApplicationWayland::continueStartupWithScreens); connect(platform(), &Platform::initFailed, this, [] () { std::cerr << "FATAL ERROR: backend failed to initialize, exiting now" << std::endl; QCoreApplication::exit(1); } ); platform()->init(); } void ApplicationWayland::continueStartupWithScreens() { disconnect(kwinApp()->platform(), &Platform::screensQueried, this, &ApplicationWayland::continueStartupWithScreens); createScreens(); if (operationMode() == OperationModeWaylandOnly) { createCompositor(); connect(Compositor::self(), &Compositor::sceneCreated, this, &ApplicationWayland::continueStartupWithSceen); return; } createCompositor(); connect(Compositor::self(), &Compositor::sceneCreated, this, &ApplicationWayland::startXwaylandServer); } void ApplicationWayland::continueStartupWithSceen() { disconnect(Compositor::self(), &Compositor::sceneCreated, this, &ApplicationWayland::continueStartupWithSceen); startSession(); createWorkspace(); notifyKSplash(); } void ApplicationWayland::continueStartupWithX() { createX11Connection(); xcb_connection_t *c = x11Connection(); if (!c) { // about to quit return; } QSocketNotifier *notifier = new QSocketNotifier(xcb_get_file_descriptor(c), QSocketNotifier::Read, this); auto processXcbEvents = [this, c] { while (auto event = xcb_poll_for_event(c)) { long result = 0; QThread::currentThread()->eventDispatcher()->filterNativeEvent(QByteArrayLiteral("xcb_generic_event_t"), event, &result); free(event); } xcb_flush(c); }; connect(notifier, &QSocketNotifier::activated, this, processXcbEvents); connect(QThread::currentThread()->eventDispatcher(), &QAbstractEventDispatcher::aboutToBlock, this, processXcbEvents); connect(QThread::currentThread()->eventDispatcher(), &QAbstractEventDispatcher::awake, this, processXcbEvents); // create selection owner for WM_S0 - magic X display number expected by XWayland KSelectionOwner owner("WM_S0", c, x11RootWindow()); owner.claim(true); createAtoms(); setupEventFilters(); // Check whether another windowmanager is running const uint32_t maskValues[] = {XCB_EVENT_MASK_SUBSTRUCTURE_REDIRECT}; ScopedCPointer redirectCheck(xcb_request_check(connection(), xcb_change_window_attributes_checked(connection(), rootWindow(), XCB_CW_EVENT_MASK, maskValues))); if (!redirectCheck.isNull()) { fputs(i18n("kwin_wayland: an X11 window manager is running on the X11 Display.\n").toLocal8Bit().constData(), stderr); ::exit(1); } m_environment.insert(QStringLiteral("DISPLAY"), QString::fromUtf8(qgetenv("DISPLAY"))); startSession(); createWorkspace(); Xcb::sync(); // Trigger possible errors, there's still a chance to abort notifyKSplash(); } void ApplicationWayland::startSession() { if (!m_inputMethodServerToStart.isEmpty()) { int socket = dup(waylandServer()->createInputMethodConnection()); if (socket >= 0) { QProcessEnvironment environment = m_environment; environment.insert(QStringLiteral("WAYLAND_SOCKET"), QByteArray::number(socket)); environment.insert(QStringLiteral("QT_QPA_PLATFORM"), QStringLiteral("wayland")); environment.remove("DISPLAY"); environment.remove("WAYLAND_DISPLAY"); QProcess *p = new Process(this); p->setProcessChannelMode(QProcess::ForwardedErrorChannel); auto finishedSignal = static_cast(&QProcess::finished); connect(p, finishedSignal, this, [this, p] { if (waylandServer()) { waylandServer()->destroyInputMethodConnection(); } p->deleteLater(); } ); p->setProcessEnvironment(environment); p->start(m_inputMethodServerToStart); p->waitForStarted(); } } // start session if (!m_sessionArgument.isEmpty()) { QProcess *p = new Process(this); p->setProcessChannelMode(QProcess::ForwardedErrorChannel); p->setProcessEnvironment(m_environment); auto finishedSignal = static_cast(&QProcess::finished); connect(p, finishedSignal, this, &ApplicationWayland::quit); p->start(m_sessionArgument); } // start the applications passed to us as command line arguments if (!m_applicationsToStart.isEmpty()) { for (const QString &application: m_applicationsToStart) { // note: this will kill the started process when we exit // this is going to happen anyway as we are the wayland and X server the app connects to QProcess *p = new Process(this); p->setProcessChannelMode(QProcess::ForwardedErrorChannel); p->setProcessEnvironment(m_environment); p->start(application); } } } void ApplicationWayland::createX11Connection() { int screenNumber = 0; xcb_connection_t *c = nullptr; if (m_xcbConnectionFd == -1) { c = xcb_connect(nullptr, &screenNumber); } else { c = xcb_connect_to_fd(m_xcbConnectionFd, nullptr); } if (int error = xcb_connection_has_error(c)) { std::cerr << "FATAL ERROR: Creating connection to XServer failed: " << error << std::endl; exit(1); return; } setX11Connection(c); // we don't support X11 multi-head in Wayland setX11ScreenNumber(screenNumber); setX11RootWindow(defaultScreen()->root); } void ApplicationWayland::startXwaylandServer() { disconnect(Compositor::self(), &Compositor::sceneCreated, this, &ApplicationWayland::startXwaylandServer); int pipeFds[2]; if (pipe(pipeFds) != 0) { std::cerr << "FATAL ERROR failed to create pipe to start Xwayland " << std::endl; exit(1); return; } int sx[2]; if (socketpair(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0, sx) < 0) { std::cerr << "FATAL ERROR: failed to open socket to open XCB connection" << std::endl; exit(1); return; } int fd = dup(sx[1]); if (fd < 0) { std::cerr << "FATAL ERROR: failed to open socket to open XCB connection" << std::endl; exit(20); return; } const int waylandSocket = waylandServer()->createXWaylandConnection(); if (waylandSocket == -1) { std::cerr << "FATAL ERROR: failed to open socket for Xwayland" << std::endl; exit(1); return; } const int wlfd = dup(waylandSocket); if (wlfd < 0) { std::cerr << "FATAL ERROR: failed to open socket for Xwayland" << std::endl; exit(20); return; } m_xcbConnectionFd = sx[0]; m_xwaylandProcess = new Process(kwinApp()); m_xwaylandProcess->setProcessChannelMode(QProcess::ForwardedErrorChannel); m_xwaylandProcess->setProgram(QStringLiteral("Xwayland")); QProcessEnvironment env = m_environment; env.insert("WAYLAND_SOCKET", QByteArray::number(wlfd)); env.insert("EGL_PLATFORM", QByteArrayLiteral("DRM")); m_xwaylandProcess->setProcessEnvironment(env); m_xwaylandProcess->setArguments({QStringLiteral("-displayfd"), QString::number(pipeFds[1]), QStringLiteral("-rootless"), QStringLiteral("-wm"), QString::number(fd)}); m_xwaylandFailConnection = connect(m_xwaylandProcess, static_cast(&QProcess::error), this, [] (QProcess::ProcessError error) { if (error == QProcess::FailedToStart) { std::cerr << "FATAL ERROR: failed to start Xwayland" << std::endl; } else { std::cerr << "FATAL ERROR: Xwayland failed, going to exit now" << std::endl; } exit(1); } ); const int xDisplayPipe = pipeFds[0]; connect(m_xwaylandProcess, &QProcess::started, this, [this, xDisplayPipe] { QFutureWatcher *watcher = new QFutureWatcher(this); QObject::connect(watcher, &QFutureWatcher::finished, this, &ApplicationWayland::continueStartupWithX, Qt::QueuedConnection); QObject::connect(watcher, &QFutureWatcher::finished, watcher, &QFutureWatcher::deleteLater, Qt::QueuedConnection); watcher->setFuture(QtConcurrent::run(readDisplay, xDisplayPipe)); } ); m_xwaylandProcess->start(); close(pipeFds[1]); } static void readDisplay(int pipe) { QFile readPipe; if (!readPipe.open(pipe, QIODevice::ReadOnly)) { std::cerr << "FATAL ERROR failed to open pipe to start X Server" << std::endl; exit(1); } QByteArray displayNumber = readPipe.readLine(); displayNumber.prepend(QByteArray(":")); displayNumber.remove(displayNumber.size() -1, 1); std::cout << "X-Server started on display " << displayNumber.constData() << std::endl; setenv("DISPLAY", displayNumber.constData(), true); // close our pipe close(pipe); } static const QString s_waylandPlugin = QStringLiteral("KWinWaylandWaylandBackend"); static const QString s_x11Plugin = QStringLiteral("KWinWaylandX11Backend"); static const QString s_fbdevPlugin = QStringLiteral("KWinWaylandFbdevBackend"); #if HAVE_DRM static const QString s_drmPlugin = QStringLiteral("KWinWaylandDrmBackend"); #endif #if HAVE_LIBHYBRIS static const QString s_hwcomposerPlugin = QStringLiteral("KWinWaylandHwcomposerBackend"); #endif static const QString s_virtualPlugin = QStringLiteral("KWinWaylandVirtualBackend"); static QString automaticBackendSelection() { if (qEnvironmentVariableIsSet("WAYLAND_DISPLAY")) { return s_waylandPlugin; } if (qEnvironmentVariableIsSet("DISPLAY")) { return s_x11Plugin; } #if HAVE_LIBHYBRIS if (qEnvironmentVariableIsSet("ANDROID_ROOT")) { return s_hwcomposerPlugin; } #endif #if HAVE_DRM return s_drmPlugin; #endif return s_fbdevPlugin; } static void disablePtrace() { #if HAVE_PR_SET_DUMPABLE // check whether we are running under a debugger const QFileInfo parent(QStringLiteral("/proc/%1/exe").arg(getppid())); if (parent.isSymLink() && (parent.symLinkTarget().endsWith(QLatin1String("/gdb")) || parent.symLinkTarget().endsWith(QLatin1String("/gdbserver")))) { // debugger, don't adjust return; } // disable ptrace in kwin_wayland prctl(PR_SET_DUMPABLE, 0); #endif #if HAVE_PROC_TRACE_CTL // FreeBSD's rudimentary procfs does not support /proc//exe // We could use the P_TRACED flag of the process to find out // if the process is being debugged ond FreeBSD. int mode = PROC_TRACE_CTL_DISABLE; procctl(P_PID, getpid(), PROC_TRACE_CTL, &mode); #endif } static void unsetDumpable(int sig) { #if HAVE_PR_SET_DUMPABLE prctl(PR_SET_DUMPABLE, 1); #endif signal(sig, SIG_IGN); raise(sig); return; } void dropNiceCapability() { #if HAVE_LIBCAP cap_t caps = cap_get_proc(); if (!caps) { return; } cap_value_t capList[] = { CAP_SYS_NICE }; if (cap_set_flag(caps, CAP_PERMITTED, 1, capList, CAP_CLEAR) == -1) { cap_free(caps); return; } if (cap_set_flag(caps, CAP_EFFECTIVE, 1, capList, CAP_CLEAR) == -1) { cap_free(caps); return; } cap_set_proc(caps); cap_free(caps); #endif } } // namespace int main(int argc, char * argv[]) { if (getuid() == 0) { std::cerr << "kwin_wayland does not support running as root." << std::endl; return 1; } KWin::disablePtrace(); KWin::Application::setupMalloc(); KWin::Application::setupLocalizedString(); KWin::gainRealTime(); KWin::dropNiceCapability(); if (signal(SIGTERM, KWin::sighandler) == SIG_IGN) signal(SIGTERM, SIG_IGN); if (signal(SIGINT, KWin::sighandler) == SIG_IGN) signal(SIGINT, SIG_IGN); if (signal(SIGHUP, KWin::sighandler) == SIG_IGN) signal(SIGHUP, SIG_IGN); signal(SIGABRT, KWin::unsetDumpable); signal(SIGSEGV, KWin::unsetDumpable); signal(SIGPIPE, SIG_IGN); // ensure that no thread takes SIGUSR sigset_t userSignals; sigemptyset(&userSignals); sigaddset(&userSignals, SIGUSR1); sigaddset(&userSignals, SIGUSR2); pthread_sigmask(SIG_BLOCK, &userSignals, nullptr); QProcessEnvironment environment = QProcessEnvironment::systemEnvironment(); // enforce our internal qpa plugin, unfortunately command line switch has precedence setenv("QT_QPA_PLATFORM", "wayland-org.kde.kwin.qpa", true); qunsetenv("QT_DEVICE_PIXEL_RATIO"); qputenv("QT_IM_MODULE", "qtvirtualkeyboard"); qputenv("QSG_RENDER_LOOP", "basic"); - QCoreApplication::setAttribute(Qt::AA_DisableHighDpiScaling); + QCoreApplication::setAttribute(Qt::AA_UseHighDpiPixmaps); KWin::ApplicationWayland a(argc, argv); a.setupTranslator(); // reset QT_QPA_PLATFORM to a sane value for any processes started from KWin setenv("QT_QPA_PLATFORM", "wayland", true); KWin::Application::createAboutData(); KQuickAddons::QtQuickSettings::init(); const auto availablePlugins = KPluginLoader::findPlugins(QStringLiteral("org.kde.kwin.waylandbackends")); auto hasPlugin = [&availablePlugins] (const QString &name) { return std::any_of(availablePlugins.begin(), availablePlugins.end(), [name] (const KPluginMetaData &plugin) { return plugin.pluginId() == name; } ); }; const bool hasSizeOption = hasPlugin(KWin::s_x11Plugin) || hasPlugin(KWin::s_virtualPlugin); const bool hasOutputCountOption = hasPlugin(KWin::s_x11Plugin); const bool hasX11Option = hasPlugin(KWin::s_x11Plugin); const bool hasVirtualOption = hasPlugin(KWin::s_virtualPlugin); const bool hasWaylandOption = hasPlugin(KWin::s_waylandPlugin); const bool hasFramebufferOption = hasPlugin(KWin::s_fbdevPlugin); #if HAVE_DRM const bool hasDrmOption = hasPlugin(KWin::s_drmPlugin); #endif #if HAVE_LIBHYBRIS const bool hasHwcomposerOption = hasPlugin(KWin::s_hwcomposerPlugin); #endif QCommandLineOption xwaylandOption(QStringLiteral("xwayland"), i18n("Start a rootless Xwayland server.")); QCommandLineOption waylandSocketOption(QStringList{QStringLiteral("s"), QStringLiteral("socket")}, i18n("Name of the Wayland socket to listen on. If not set \"wayland-0\" is used."), QStringLiteral("socket")); QCommandLineOption framebufferOption(QStringLiteral("framebuffer"), i18n("Render to framebuffer.")); QCommandLineOption framebufferDeviceOption(QStringLiteral("fb-device"), i18n("The framebuffer device to render to."), QStringLiteral("fbdev")); QCommandLineOption x11DisplayOption(QStringLiteral("x11-display"), i18n("The X11 Display to use in windowed mode on platform X11."), QStringLiteral("display")); QCommandLineOption waylandDisplayOption(QStringLiteral("wayland-display"), i18n("The Wayland Display to use in windowed mode on platform Wayland."), QStringLiteral("display")); QCommandLineOption virtualFbOption(QStringLiteral("virtual"), i18n("Render to a virtual framebuffer.")); QCommandLineOption widthOption(QStringLiteral("width"), i18n("The width for windowed mode. Default width is 1024."), QStringLiteral("width")); widthOption.setDefaultValue(QString::number(1024)); QCommandLineOption heightOption(QStringLiteral("height"), i18n("The height for windowed mode. Default height is 768."), QStringLiteral("height")); heightOption.setDefaultValue(QString::number(768)); QCommandLineOption scaleOption(QStringLiteral("scale"), i18n("The scale for windowed mode. Default value is 1."), QStringLiteral("scale")); scaleOption.setDefaultValue(QString::number(1)); QCommandLineOption outputCountOption(QStringLiteral("output-count"), i18n("The number of windows to open as outputs in windowed mode. Default value is 1"), QStringLiteral("height")); outputCountOption.setDefaultValue(QString::number(1)); QCommandLineParser parser; a.setupCommandLine(&parser); parser.addOption(xwaylandOption); parser.addOption(waylandSocketOption); if (hasX11Option) { parser.addOption(x11DisplayOption); } if (hasWaylandOption) { parser.addOption(waylandDisplayOption); } if (hasFramebufferOption) { parser.addOption(framebufferOption); parser.addOption(framebufferDeviceOption); } if (hasVirtualOption) { parser.addOption(virtualFbOption); } if (hasSizeOption) { parser.addOption(widthOption); parser.addOption(heightOption); parser.addOption(scaleOption); } if (hasOutputCountOption) { parser.addOption(outputCountOption); } #if HAVE_LIBHYBRIS QCommandLineOption hwcomposerOption(QStringLiteral("hwcomposer"), i18n("Use libhybris hwcomposer")); if (hasHwcomposerOption) { parser.addOption(hwcomposerOption); } #endif QCommandLineOption libinputOption(QStringLiteral("libinput"), i18n("Enable libinput support for input events processing. Note: never use in a nested session.")); parser.addOption(libinputOption); #if HAVE_DRM QCommandLineOption drmOption(QStringLiteral("drm"), i18n("Render through drm node.")); if (hasDrmOption) { parser.addOption(drmOption); } #endif QCommandLineOption inputMethodOption(QStringLiteral("inputmethod"), i18n("Input method that KWin starts."), QStringLiteral("path/to/imserver")); parser.addOption(inputMethodOption); QCommandLineOption listBackendsOption(QStringLiteral("list-backends"), i18n("List all available backends and quit.")); parser.addOption(listBackendsOption); QCommandLineOption screenLockerOption(QStringLiteral("lockscreen"), i18n("Starts the session in locked mode.")); parser.addOption(screenLockerOption); QCommandLineOption noScreenLockerOption(QStringLiteral("no-lockscreen"), i18n("Starts the session without lock screen support.")); parser.addOption(noScreenLockerOption); QCommandLineOption noGlobalShortcutsOption(QStringLiteral("no-global-shortcuts"), i18n("Starts the session without global shortcuts support.")); parser.addOption(noScreenLockerOption); QCommandLineOption exitWithSessionOption(QStringLiteral("exit-with-session"), i18n("Exit after the session application, which is started by KWin, closed."), QStringLiteral("/path/to/session")); parser.addOption(exitWithSessionOption); parser.addPositionalArgument(QStringLiteral("applications"), i18n("Applications to start once Wayland and Xwayland server are started"), QStringLiteral("[/path/to/application...]")); parser.process(a); a.processCommandLine(&parser); #ifdef KWIN_BUILD_ACTIVITIES a.setUseKActivities(false); #endif if (parser.isSet(listBackendsOption)) { for (const auto &plugin: availablePlugins) { std::cout << std::setw(40) << std::left << qPrintable(plugin.name()) << qPrintable(plugin.description()) << std::endl; } return 0; } if (parser.isSet(exitWithSessionOption)) { a.setSessionArgument(parser.value(exitWithSessionOption)); } KWin::Application::setUseLibinput(parser.isSet(libinputOption)); QString pluginName; QSize initialWindowSize; QByteArray deviceIdentifier; int outputCount = 1; qreal outputScale = 1; #if HAVE_DRM if (hasDrmOption && parser.isSet(drmOption)) { pluginName = KWin::s_drmPlugin; } #endif if (hasSizeOption) { bool ok = false; const int width = parser.value(widthOption).toInt(&ok); if (!ok) { std::cerr << "FATAL ERROR incorrect value for width" << std::endl; return 1; } const int height = parser.value(heightOption).toInt(&ok); if (!ok) { std::cerr << "FATAL ERROR incorrect value for height" << std::endl; return 1; } const qreal scale = parser.value(scaleOption).toDouble(&ok); if (!ok || scale < 1) { std::cerr << "FATAL ERROR incorrect value for scale" << std::endl; return 1; } outputScale = scale; initialWindowSize = QSize(width, height); } if (hasOutputCountOption) { bool ok = false; const int count = parser.value(outputCountOption).toInt(&ok); if (ok) { outputCount = qMax(1, count); } } if (hasX11Option && parser.isSet(x11DisplayOption)) { deviceIdentifier = parser.value(x11DisplayOption).toUtf8(); pluginName = KWin::s_x11Plugin; } else if (hasWaylandOption && parser.isSet(waylandDisplayOption)) { deviceIdentifier = parser.value(waylandDisplayOption).toUtf8(); pluginName = KWin::s_waylandPlugin; } if (hasFramebufferOption && parser.isSet(framebufferOption)) { pluginName = KWin::s_fbdevPlugin; deviceIdentifier = parser.value(framebufferDeviceOption).toUtf8(); } #if HAVE_LIBHYBRIS if (hasHwcomposerOption && parser.isSet(hwcomposerOption)) { pluginName = KWin::s_hwcomposerPlugin; } #endif if (hasVirtualOption && parser.isSet(virtualFbOption)) { pluginName = KWin::s_virtualPlugin; } if (pluginName.isEmpty()) { std::cerr << "No backend specified through command line argument, trying auto resolution" << std::endl; pluginName = KWin::automaticBackendSelection(); } auto pluginIt = std::find_if(availablePlugins.begin(), availablePlugins.end(), [&pluginName] (const KPluginMetaData &plugin) { return plugin.pluginId() == pluginName; } ); if (pluginIt == availablePlugins.end()) { std::cerr << "FATAL ERROR: could not find a backend" << std::endl; return 1; } // TODO: create backend without having the server running KWin::WaylandServer *server = KWin::WaylandServer::create(&a); KWin::WaylandServer::InitalizationFlags flags; if (parser.isSet(screenLockerOption)) { flags = KWin::WaylandServer::InitalizationFlag::LockScreen; } else if (parser.isSet(noScreenLockerOption)) { flags = KWin::WaylandServer::InitalizationFlag::NoLockScreenIntegration; } if (parser.isSet(noGlobalShortcutsOption)) { flags |= KWin::WaylandServer::InitalizationFlag::NoGlobalShortcuts; } if (!server->init(parser.value(waylandSocketOption).toUtf8(), flags)) { std::cerr << "FATAL ERROR: could not create Wayland server" << std::endl; return 1; } a.initPlatform(*pluginIt); if (!a.platform()) { std::cerr << "FATAL ERROR: could not instantiate a backend" << std::endl; return 1; } if (!deviceIdentifier.isEmpty()) { a.platform()->setDeviceIdentifier(deviceIdentifier); } if (initialWindowSize.isValid()) { a.platform()->setInitialWindowSize(initialWindowSize); } a.platform()->setInitialOutputScale(outputScale); a.platform()->setInitialOutputCount(outputCount); QObject::connect(&a, &KWin::Application::workspaceCreated, server, &KWin::WaylandServer::initWorkspace); environment.insert(QStringLiteral("WAYLAND_DISPLAY"), server->display()->socketName()); a.setProcessStartupEnvironment(environment); a.setStartXwayland(parser.isSet(xwaylandOption)); a.setApplicationsToStart(parser.positionalArguments()); a.setInputMethodServerToStart(parser.value(inputMethodOption)); a.start(); return a.exec(); } diff --git a/plugins/qpa/backingstore.cpp b/plugins/qpa/backingstore.cpp index 2ffe084cb..a2f4fd2ac 100644 --- a/plugins/qpa/backingstore.cpp +++ b/plugins/qpa/backingstore.cpp @@ -1,119 +1,128 @@ /******************************************************************** KWin - the KDE window manager This file is part of the KDE project. Copyright (C) 2015 Martin Gräßlin This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . *********************************************************************/ #include "window.h" #include "backingstore.h" #include "../../wayland_server.h" #include #include #include #include namespace KWin { namespace QPA { BackingStore::BackingStore(QWindow *w, KWayland::Client::ShmPool *shm) : QPlatformBackingStore(w) , m_shm(shm) , m_backBuffer(QSize(), QImage::Format_ARGB32_Premultiplied) { QObject::connect(m_shm, &KWayland::Client::ShmPool::poolResized, [this] { if (!m_buffer) { return; } auto b = m_buffer.toStrongRef(); if (!b->isUsed()){ return; } const QSize size = m_backBuffer.size(); m_backBuffer = QImage(b->address(), size.width(), size.height(), QImage::Format_ARGB32_Premultiplied); + m_backBuffer.setDevicePixelRatio(scale()); } ); } BackingStore::~BackingStore() = default; QPaintDevice *BackingStore::paintDevice() { return &m_backBuffer; } void BackingStore::resize(const QSize &size, const QRegion &staticContents) { Q_UNUSED(staticContents) - m_size = size; + m_size = size * scale(); if (!m_buffer) { return; } m_buffer.toStrongRef()->setUsed(false); m_buffer.clear(); } void BackingStore::flush(QWindow *window, const QRegion ®ion, const QPoint &offset) { Q_UNUSED(region) Q_UNUSED(offset) - auto s = static_cast(window->handle())->surface(); + + auto w = static_cast(window->handle()); + auto s = w->surface(); if (!s) { return; } s->attachBuffer(m_buffer); // TODO: proper damage region - s->damage(QRect(QPoint(0, 0), m_backBuffer.size())); + s->damage(QRect(QPoint(0, 0), m_backBuffer.size() / scale())); s->commit(KWayland::Client::Surface::CommitFlag::None); waylandServer()->internalClientConection()->flush(); waylandServer()->dispatch(); } void BackingStore::beginPaint(const QRegion&) { if (m_buffer) { auto b = m_buffer.toStrongRef(); if (b->isReleased()) { // we can re-use this buffer b->setReleased(false); return; } else { // buffer is still in use, get a new one b->setUsed(false); } } auto oldBuffer = m_buffer.toStrongRef(); m_buffer.clear(); m_buffer = m_shm->getBuffer(m_size, m_size.width() * 4); if (!m_buffer) { m_backBuffer = QImage(); return; } auto b = m_buffer.toStrongRef(); b->setUsed(true); m_backBuffer = QImage(b->address(), m_size.width(), m_size.height(), QImage::Format_ARGB32_Premultiplied); + m_backBuffer.setDevicePixelRatio(scale()); if (oldBuffer) { b->copy(oldBuffer->address()); } else { m_backBuffer.fill(Qt::transparent); } } +int BackingStore::scale() const +{ + return static_cast(window()->handle())->scale(); +} + } } diff --git a/plugins/qpa/backingstore.h b/plugins/qpa/backingstore.h index 0e00de635..53c25a778 100644 --- a/plugins/qpa/backingstore.h +++ b/plugins/qpa/backingstore.h @@ -1,60 +1,61 @@ /******************************************************************** KWin - the KDE window manager This file is part of the KDE project. Copyright (C) 2015 Martin Gräßlin This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . *********************************************************************/ #ifndef KWIN_QPA_BACKINGSTORE_H #define KWIN_QPA_BACKINGSTORE_H #include namespace KWayland { namespace Client { class Buffer; class ShmPool; } } namespace KWin { namespace QPA { class BackingStore : public QPlatformBackingStore { public: explicit BackingStore(QWindow *w, KWayland::Client::ShmPool *shm); virtual ~BackingStore(); QPaintDevice *paintDevice() override; void flush(QWindow *window, const QRegion ®ion, const QPoint &offset) override; void resize(const QSize &size, const QRegion &staticContents) override; void beginPaint(const QRegion &) override; private: + int scale() const; KWayland::Client::ShmPool *m_shm; QWeakPointer m_buffer; QImage m_backBuffer; QSize m_size; }; } } #endif diff --git a/plugins/qpa/window.cpp b/plugins/qpa/window.cpp index 37ce0e725..04deadbe8 100644 --- a/plugins/qpa/window.cpp +++ b/plugins/qpa/window.cpp @@ -1,178 +1,196 @@ /******************************************************************** KWin - the KDE window manager This file is part of the KDE project. Copyright (C) 2015 Martin Gräßlin This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . *********************************************************************/ #define WL_EGL_PLATFORM 1 #include "integration.h" #include "window.h" +#include "screens.h" #include "../../shell_client.h" #include "../../wayland_server.h" #include #include #include #include #include #include #include namespace KWin { namespace QPA { static quint32 s_windowId = 0; Window::Window(QWindow *window, KWayland::Client::Surface *surface, KWayland::Client::ShellSurface *shellSurface, const Integration *integration) : QPlatformWindow(window) , m_surface(surface) , m_shellSurface(shellSurface) , m_windowId(++s_windowId) , m_integration(integration) + , m_scale(screens()->maxScale()) { + m_surface->setScale(m_scale); + QObject::connect(m_surface, &QObject::destroyed, window, [this] { m_surface = nullptr;}); QObject::connect(m_shellSurface, &QObject::destroyed, window, [this] { m_shellSurface = nullptr;}); waylandServer()->internalClientConection()->flush(); } Window::~Window() { unmap(); if (m_eglSurface != EGL_NO_SURFACE) { eglDestroySurface(m_integration->eglDisplay(), m_eglSurface); } #if HAVE_WAYLAND_EGL if (m_eglWaylandWindow) { wl_egl_window_destroy(m_eglWaylandWindow); } #endif delete m_shellSurface; delete m_surface; } WId Window::winId() const { return m_windowId; } void Window::setVisible(bool visible) { if (!visible) { unmap(); } QPlatformWindow::setVisible(visible); } void Window::setGeometry(const QRect &rect) { const QRect &oldRect = geometry(); QPlatformWindow::setGeometry(rect); if (rect.x() != oldRect.x()) { emit window()->xChanged(rect.x()); } if (rect.y() != oldRect.y()) { emit window()->yChanged(rect.y()); } if (rect.width() != oldRect.width()) { emit window()->widthChanged(rect.width()); } if (rect.height() != oldRect.height()) { emit window()->heightChanged(rect.height()); } + + const QSize nativeSize = rect.size() * m_scale; + if (m_contentFBO) { - if (m_contentFBO->width() != geometry().width() || m_contentFBO->height() != geometry().height()) { + if (m_contentFBO->size() != nativeSize) { m_resized = true; } } #if HAVE_WAYLAND_EGL if (m_eglWaylandWindow) { - wl_egl_window_resize(m_eglWaylandWindow, geometry().width(), geometry().height(), 0, 0); + wl_egl_window_resize(m_eglWaylandWindow, nativeSize.width(), nativeSize.height(), 0, 0); } #endif QWindowSystemInterface::handleGeometryChange(window(), geometry()); } void Window::unmap() { if (m_shellClient) { m_shellClient->setInternalFramebufferObject(QSharedPointer()); } if (m_surface) { m_surface->attachBuffer(KWayland::Client::Buffer::Ptr()); m_surface->commit(KWayland::Client::Surface::CommitFlag::None); } if (waylandServer()->internalClientConection()) { waylandServer()->internalClientConection()->flush(); } } void Window::createEglSurface(EGLDisplay dpy, EGLConfig config) { #if HAVE_WAYLAND_EGL - const QSize size = window()->size(); + const QSize size = window()->size() * m_scale; m_eglWaylandWindow = wl_egl_window_create(*m_surface, size.width(), size.height()); if (!m_eglWaylandWindow) { return; } m_eglSurface = eglCreateWindowSurface(dpy, config, m_eglWaylandWindow, nullptr); #else Q_UNUSED(dpy) Q_UNUSED(config) #endif } void Window::bindContentFBO() { if (m_resized || !m_contentFBO) { createFBO(); } m_contentFBO->bind(); } QSharedPointer Window::swapFBO() { auto fbo = m_contentFBO; m_contentFBO.clear(); m_surface->commit(KWayland::Client::Surface::CommitFlag::None); return fbo; } void Window::createFBO() { const QRect &r = geometry(); if (m_contentFBO && r.size().isEmpty()) { return; } - m_contentFBO.reset(new QOpenGLFramebufferObject(r.width(), r.height(), QOpenGLFramebufferObject::CombinedDepthStencil)); + const QSize nativeSize = r.size() * m_scale; + m_contentFBO.reset(new QOpenGLFramebufferObject(nativeSize.width(), nativeSize.height(), QOpenGLFramebufferObject::CombinedDepthStencil)); if (!m_contentFBO->isValid()) { qCWarning(KWIN_QPA) << "Content FBO is not valid"; } m_resized = false; } ShellClient *Window::shellClient() { if (!m_shellClient) { waylandServer()->dispatch(); m_shellClient = waylandServer()->findClient(window()); } return m_shellClient; } +int Window::scale() const +{ + return m_scale; +} + +qreal Window::devicePixelRatio() const +{ + return m_scale; +} + } } diff --git a/plugins/qpa/window.h b/plugins/qpa/window.h index e8caedc6e..bc334c359 100644 --- a/plugins/qpa/window.h +++ b/plugins/qpa/window.h @@ -1,104 +1,108 @@ /******************************************************************** KWin - the KDE window manager This file is part of the KDE project. Copyright (C) 2015 Martin Gräßlin This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . *********************************************************************/ #ifndef KWIN_QPA_WINDOW_H #define KWIN_QPA_WINDOW_H #include #include "fixqopengl.h" #include #include // wayland #include #if HAVE_WAYLAND_EGL #include #endif class QOpenGLFramebufferObject; #if HAVE_WAYLAND_EGL struct wl_egl_window; #endif namespace KWayland { namespace Client { class Surface; class ShellSurface; } } namespace KWin { class ShellClient; namespace QPA { class Integration; class Window : public QPlatformWindow { public: explicit Window(QWindow *window, KWayland::Client::Surface *surface, KWayland::Client::ShellSurface *shellSurface, const Integration *integration); virtual ~Window(); void setVisible(bool visible) override; void setGeometry(const QRect &rect) override; WId winId() const override; KWayland::Client::Surface *surface() const { return m_surface; } EGLSurface eglSurface() const { return m_eglSurface; } void createEglSurface(EGLDisplay dpy, EGLConfig config); + int scale() const; + qreal devicePixelRatio() const override; + void bindContentFBO(); const QSharedPointer &contentFBO() const { return m_contentFBO; } QSharedPointer swapFBO(); ShellClient *shellClient(); private: void unmap(); void createFBO(); KWayland::Client::Surface *m_surface; KWayland::Client::ShellSurface *m_shellSurface; EGLSurface m_eglSurface = EGL_NO_SURFACE; QSharedPointer m_contentFBO; bool m_resized = false; ShellClient *m_shellClient = nullptr; #if HAVE_WAYLAND_EGL wl_egl_window *m_eglWaylandWindow = nullptr; #endif quint32 m_windowId; const Integration *m_integration; + int m_scale = 1; }; } } #endif diff --git a/shell_client.cpp b/shell_client.cpp index 4255d6394..810aa2fc7 100644 --- a/shell_client.cpp +++ b/shell_client.cpp @@ -1,1997 +1,1995 @@ /******************************************************************** 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 using namespace KWayland::Server; static const QByteArray s_skipClosePropertyName = QByteArrayLiteral("KWIN_SKIP_CLOSE_ANIMATION"); 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(); } ShellClient::ShellClient(XdgShellSurfaceInterface *surface) : AbstractClient() , m_shellSurface(nullptr) , m_xdgShellSurface(surface) , m_xdgShellPopup(nullptr) , m_internal(surface->client() == waylandServer()->internalConnection()) { setSurface(surface->surface()); init(); } ShellClient::ShellClient(XdgShellPopupInterface *surface) : AbstractClient() , m_shellSurface(nullptr) , m_xdgShellSurface(nullptr) , m_xdgShellPopup(surface) , m_internal(surface->client() == waylandServer()->internalConnection()) { setSurface(surface->surface()); init(); } 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()); connect(shellSurface, &T::windowClassChanged, this, [this, resourceName] (const QByteArray &windowClass) { setResourceClass(resourceName, windowClass); if (!m_internal) { 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; } 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); if (!m_internal) { setupWindowRules(false); } setDesktopFileName(rules()->checkDesktopFile(shellSurface->windowClass(), true).toUtf8()); } void ShellClient::init() { connect(this, &ShellClient::desktopFileNameChanged, this, &ShellClient::updateIcon); findInternalWindow(); 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_internalWindow) { updateInternalWindowGeometry(); updateDecoration(true); } else { 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); }; configure(); 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; }); QRect position = QRect(m_xdgShellPopup->transientOffset(), m_xdgShellPopup->initialSize()); m_xdgShellPopup->configure(position); connect(m_xdgShellPopup, &XdgShellPopupInterface::destroyed, this, &ShellClient::destroyClient); } // set initial desktop setDesktop(rules()->checkDesktop(m_internal ? int(NET::OnAllDesktops) : VirtualDesktopManager::self()->current(), true)); // TODO: merge in checks from Client::manage? if (rules()->checkMinimize(false, true)) { minimize(true); // No animation } setSkipTaskbar(rules()->checkSkipTaskbar(m_plasmaShellSurface ? m_plasmaShellSurface->skipTaskbar() : false, true)); setSkipPager(rules()->checkSkipPager(false, true)); setSkipSwitcher(rules()->checkSkipSwitcher(false, true)); setKeepAbove(rules()->checkKeepAbove(false, true)); setKeepBelow(rules()->checkKeepBelow(false, true)); setShortcut(rules()->checkShortcut(QString(), true)); // 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(); // check whether we have a ServerSideDecoration if (ServerSideDecorationInterface *deco = ServerSideDecorationInterface::get(s)) { installServerSideDecoration(deco); } AbstractClient::updateColorScheme(QString()); if (!m_internal) { discardTemporaryRules(); applyWindowRules(); // Just in case RuleBook::self()->discardUsed(this, false); // Remove ApplyNow rules updateWindowRules(Rules::All); // Was blocked while !isManaged() } } void ShellClient::destroyClient() { m_closing = true; Deleted *del = nullptr; if (workspace()) { del = Deleted::create(this); } emit windowClosed(this, del); 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; } 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() << "\'"; } 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(); updatePendingGeometry(); } markAsMapped(); setDepth((s->buffer()->hasAlphaChannel() && !isDesktop()) ? 32 : 24); repaints_region += damage.translated(clientPos()); Toplevel::addDamage(damage); } void ShellClient::setInternalFramebufferObject(const QSharedPointer &fbo) { if (fbo.isNull()) { unmap(); return; } - //Kwin currently scales internal windows to 1, so this is currently always correct - //when that changes, this needs adjusting - m_clientSize = fbo->size(); + m_clientSize = fbo->size() / surface()->scale(); markAsMapped(); doSetGeometry(QRect(geom.topLeft(), m_clientSize)); Toplevel::setInternalFramebufferObject(fbo); Toplevel::addDamage(QRegion(0, 0, width(), height())); } 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() ? XdgDecorationInterface::Mode::ServerSide: XdgDecorationInterface::Mode::ClientSide; m_xdgDecoration->configure(mode); 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) { if (areGeometryUpdatesBlocked()) { // when the GeometryUpdateBlocker exits the current geom is passed to setGeometry // thus we need to set it here. geom = QRect(x, y, w, h); 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(); } // TODO: better merge with Client's implementation const QSize requestedClientSize = QSize(w, h) - QSize(borderLeft() + borderRight(), borderTop() + borderBottom()); if (requestedClientSize == m_clientSize && !isWaitingForMoveResizeSync()) { // size didn't change, update directly doSetGeometry(QRect(x, y, w, h)); updateMaximizeMode(m_requestedMaximizeMode); } else { // size did change, Client needs to provide a new buffer requestGeometry(QRect(x, y, w, h)); } } void ShellClient::doSetGeometry(const QRect &rect) { if (geom == rect && pendingGeometryUpdate() == PendingGeometryNone) { return; } if (!m_unmapped) { addWorkspaceRepaint(visibleRect()); } geom = rect; if (m_unmapped && m_geomMaximizeRestore.isEmpty() && !geom.isEmpty()) { // use first valid geometry as restore geometry m_geomMaximizeRestore = geom; } if (!m_unmapped) { addWorkspaceRepaint(visibleRect()); } syncGeometryToInternalWindow(); if (hasStrut()) { workspace()->updateClientArea(); } const auto old = geometryBeforeUpdateBlocking(); updateGeometryBeforeUpdateBlocking(); emit geometryShapeChanged(this, old); if (isResize()) { performMoveResize(); } } void ShellClient::doMove(int x, int y) { Q_UNUSED(x) Q_UNUSED(y) syncGeometryToInternalWindow(); } void ShellClient::syncGeometryToInternalWindow() { if (!m_internalWindow) { return; } const QRect windowRect = QRect(geom.topLeft() + QPoint(borderLeft(), borderTop()), geom.size() - QSize(borderLeft() + borderRight(), borderTop() + borderBottom())); if (m_internalWindow->geometry() != windowRect) { // delay to end of cycle to prevent freeze, see BUG 384441 QTimer::singleShot(0, m_internalWindow, std::bind(static_cast(&QWindow::setGeometry), m_internalWindow, windowRect)); } } 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); } else if (m_qtExtendedSurface && isCloseable()) { m_qtExtendedSurface->close(); } else if (m_internalWindow) { m_internalWindow->hide(); } } 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; } if (m_internal) { return true; } return m_qtExtendedSurface ? true : false; } bool ShellClient::isFullScreen() const { return m_fullScreen; } bool ShellClient::isMaximizable() const { if (m_internal) { return false; } return true; } bool ShellClient::isMinimizable() const { if (m_internal) { return false; } return (!m_plasmaShellSurface || m_plasmaShellSurface->role() == PlasmaShellSurfaceInterface::Role::Normal); } bool ShellClient::isMovable() const { if (m_plasmaShellSurface) { return m_plasmaShellSurface->role() == PlasmaShellSurfaceInterface::Role::Normal; } if (m_xdgShellPopup) { return false; } return true; } bool ShellClient::isMovableAcrossScreens() const { if (m_plasmaShellSurface) { return m_plasmaShellSurface->role() == PlasmaShellSurfaceInterface::Role::Normal; } if (m_xdgShellPopup) { return false; } return true; } bool ShellClient::isResizable() const { 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(); StackingUpdatesBlocker blocker(workspace()); RequestGeometryBlocker geometryBlocker(this); // '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 // 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 (isInternal()) { return m_internalWindowFlags.testFlag(Qt::FramelessWindowHint) || m_internalWindowFlags.testFlag(Qt::Popup); } 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; } void ShellClient::setFullScreen(bool set, bool user) { if (!isFullScreen() && !set) return; if (user && !userCanSetFullScreen()) return; set = rules()->checkFullScreen(set && !isSpecialWindow()); setShade(ShadeNone); bool was_fs = isFullScreen(); if (was_fs) { 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 (was_fs == isFullScreen()) return; 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 (isFullScreen()) { setGeometry(workspace()->clientArea(FullScreenArea, this)); } else { if (!m_geomFsRestore.isNull()) { int currentScreen = screen(); setGeometry(QRect(m_geomFsRestore.topLeft(), adjustedSize(m_geomFsRestore.size()))); if( currentScreen != screen()) workspace()->sendClientToScreen( this, currentScreen ); } else { // does this ever happen? setGeometry(workspace()->clientArea(MaximizeArea, this)); } } updateWindowRules(Rules::Fullscreen|Rules::Position|Rules::Size); if (was_fs != isFullScreen()) { 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); } bool breakShowingDesktop = !keepAbove() && !isOnScreenDisplay(); if (breakShowingDesktop) { // check that it doesn't belong to the desktop const auto &clients = waylandServer()->clients(); for (auto c: clients) { if (!belongsToSameApplication(c, SameApplicationChecks())) { continue; } if (c->isDesktop()) { breakShowingDesktop = false; break; } } } if (breakShowingDesktop) 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(); } if (m_internal) { return !m_internalWindowFlags.testFlag(Qt::FramelessWindowHint) || m_internalWindowFlags.testFlag(Qt::Popup); } return false; } bool ShellClient::wantsInput() const { return rules()->checkAcceptFocus(acceptsFocus()); } bool ShellClient::acceptsFocus() const { if (isInternal()) { return false; } 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) { 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_internalWindow) { m_windowId = m_internalWindow->winId(); } else { m_windowId = waylandServer()->createWindowId(surface()); } } void ShellClient::findInternalWindow() { if (surface()->client() != waylandServer()->internalConnection()) { return; } const QWindowList windows = kwinApp()->topLevelWindows(); for (QWindow *w: windows) { auto s = KWayland::Client::Surface::fromWindow(w); if (!s) { continue; } if (s->id() != surface()->id()) { continue; } m_internalWindow = w; m_internalWindowFlags = m_internalWindow->flags(); connect(m_internalWindow, &QWindow::xChanged, this, &ShellClient::updateInternalWindowGeometry); connect(m_internalWindow, &QWindow::yChanged, this, &ShellClient::updateInternalWindowGeometry); connect(m_internalWindow, &QWindow::destroyed, this, [this] { m_internalWindow = nullptr; }); connect(m_internalWindow, &QWindow::opacityChanged, this, &ShellClient::setOpacity); // Try reading the window type from the QWindow. PlasmaCore.Dialog provides a dynamic type property // let's check whether it exists, if it does it's our window type const QVariant windowType = m_internalWindow->property("type"); if (!windowType.isNull()) { m_windowType = static_cast(windowType.toInt()); } setOpacity(m_internalWindow->opacity()); // skip close animation support setSkipCloseAnimation(m_internalWindow->property(s_skipClosePropertyName).toBool()); m_internalWindow->installEventFilter(this); return; } } void ShellClient::updateInternalWindowGeometry() { if (!m_internalWindow) { return; } doSetGeometry(QRect(m_internalWindow->geometry().topLeft() - QPoint(borderLeft(), borderTop()), m_internalWindow->geometry().size() + QSize(borderLeft() + borderRight(), borderTop() + borderBottom()))); } pid_t ShellClient::pid() const { return surface()->client()->processId(); } bool ShellClient::isInternal() const { return m_internal; } bool ShellClient::isLockScreen() const { if (m_internalWindow) { return m_internalWindow->property("org_kde_ksld_emergency").toBool(); } return surface()->client() == waylandServer()->screenLockerClientConnection(); } bool ShellClient::isInputMethod() const { if (m_internal && m_internalWindow) { return m_internalWindow->property("__kwin_input_method").toBool(); } return surface()->client() == waylandServer()->inputMethodConnection(); } void ShellClient::requestGeometry(const QRect &rect) { if (m_requestGeometryBlockCounter != 0) { m_blockedRequestGeometry = rect; return; } PendingConfigureRequest configureRequest; configureRequest.positionAfterResize = rect.topLeft(); configureRequest.maximizeMode = m_requestedMaximizeMode; const QSize size = rect.size() - QSize(borderLeft() + borderRight(), borderTop() + borderBottom()); m_requestedClientSize = size; if (m_shellSurface) { m_shellSurface->requestSize(size); } if (m_xdgShellSurface) { configureRequest.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; configureRequest.serialId = m_xdgShellPopup->configure(QRect(relativeOffset, rect.size())); } } m_pendingConfigureRequests.append(configureRequest); m_blockedRequestGeometry = QRect(); if (m_internal) { m_internalWindow->setGeometry(QRect(rect.topLeft() + QPoint(borderLeft(), borderTop()), rect.size() - QSize(borderLeft() + borderRight(), borderTop() + borderBottom()))); } } 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)); } if (m_internal) { m_internalWindow->setGeometry(QRect(pos() + QPoint(borderLeft(), borderTop()), QSize(w, h) - QSize(borderLeft() + borderRight(), borderTop() + borderBottom()))); } } void ShellClient::unmap() { m_unmapped = true; 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) 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::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) { 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::installQtExtendedSurface(QtExtendedSurfaceInterface *surface) { m_qtExtendedSurface = surface; connect(m_qtExtendedSurface.data(), &QtExtendedSurfaceInterface::raiseRequested, this, [this]() { workspace()->raiseClientRequest(this); }); connect(m_qtExtendedSurface.data(), &QtExtendedSurfaceInterface::lowerRequested, this, [this]() { workspace()->lowerClientRequest(this); }); m_qtExtendedSurface->installEventFilter(this); } 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()); } bool ShellClient::eventFilter(QObject *watched, QEvent *event) { if (watched == m_internalWindow && event->type() == QEvent::DynamicPropertyChange) { QDynamicPropertyChangeEvent *pe = static_cast(event); if (pe->propertyName() == s_skipClosePropertyName) { setSkipCloseAnimation(m_internalWindow->property(s_skipClosePropertyName).toBool()); } } return false; } 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; 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; 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(); } else { Q_UNREACHABLE(); } //initial position popupPosition = QRect(popupOffset(anchorRect, anchorEdge, gravity) + offset + parentClientPos, geometry().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) + offset + parentClientPos, geometry().size()); //if it still doesn't fit we should continue with the unflipped version if (inBounds(flippedPopupPosition, Qt::LeftEdge | Qt::RightEdge)) { popupPosition.setX(flippedPopupPosition.x()); } } } if (constraintAdjustments & PositionerConstraint::SlideX) { if (!inBounds(popupPosition, Qt::LeftEdge)) { popupPosition.setX(bounds.x()); } if (!inBounds(popupPosition, Qt::RightEdge)) { popupPosition.setX(bounds.x() + bounds.width() - geometry().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) + offset + parentClientPos, geometry().size()); //if it still doesn't fit we should continue with the unflipped version if (inBounds(flippedPopupPosition, Qt::TopEdge | Qt::BottomEdge)) { popupPosition.setY(flippedPopupPosition.y()); } } } if (constraintAdjustments & PositionerConstraint::SlideY) { if (!inBounds(popupPosition, Qt::TopEdge)) { popupPosition.setY(bounds.y()); } if (!inBounds(popupPosition, Qt::BottomEdge)) { popupPosition.setY(bounds.y() + bounds.height() - geometry().height()); } } if (constraintAdjustments & PositionerConstraint::ResizeY) { //TODO } return popupPosition; } QPoint ShellClient::popupOffset(const QRect &anchorRect, const Qt::Edges anchorEdge, const Qt::Edges gravity) const { const QSize popupSize = geometry().size(); 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 { return !m_pendingConfigureRequests.isEmpty(); } 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 (isInternal()) { 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 (isInternal()) { return; } 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); } bool ShellClient::isPopupWindow() const { if (Toplevel::isPopupWindow()) { return true; } if (isInternal()) { return m_internalWindowFlags.testFlag(Qt::Popup); } if (m_shellSurface != nullptr) { return m_shellSurface->isPopup(); } if (m_xdgShellPopup != nullptr) { return true; } return false; } }