diff --git a/abstract_client.cpp b/abstract_client.cpp index 5800f8965..3c83fc95f 100644 --- a/abstract_client.cpp +++ b/abstract_client.cpp @@ -1,3143 +1,3139 @@ /******************************************************************** KWin - the KDE window manager This file is part of the KDE project. Copyright (C) 2015 Martin Gräßlin Copyright (C) 2019 Vlad Zahorodnii This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . *********************************************************************/ #include "abstract_client.h" #include "appmenu.h" #include "decorations/decoratedclient.h" #include "decorations/decorationpalette.h" #include "decorations/decorationbridge.h" #include "cursor.h" #include "effects.h" #include "focuschain.h" #include "outline.h" #include "screens.h" #ifdef KWIN_BUILD_TABBOX #include "tabbox.h" #endif #include "screenedge.h" #include "useractions.h" #include "workspace.h" #include "wayland_server.h" #include #include #include #include #include namespace KWin { static inline int sign(int v) { return (v > 0) - (v < 0); } QHash> AbstractClient::s_palettes; std::shared_ptr AbstractClient::s_defaultPalette; AbstractClient::AbstractClient() : Toplevel() #ifdef KWIN_BUILD_TABBOX , m_tabBoxClient(QSharedPointer(new TabBox::TabBoxClientImpl(this))) #endif , m_colorScheme(QStringLiteral("kdeglobals")) { - connect(this, &AbstractClient::geometryShapeChanged, this, &AbstractClient::geometryChanged); - auto signalMaximizeChanged = static_cast(&AbstractClient::clientMaximizedStateChanged); - connect(this, signalMaximizeChanged, this, &AbstractClient::geometryChanged); - connect(this, &AbstractClient::clientStepUserMovedResized, this, &AbstractClient::geometryChanged); connect(this, &AbstractClient::clientStartUserMovedResized, this, &AbstractClient::moveResizedChanged); connect(this, &AbstractClient::clientFinishUserMovedResized, this, &AbstractClient::moveResizedChanged); connect(this, &AbstractClient::clientStartUserMovedResized, this, &AbstractClient::removeCheckScreenConnection); connect(this, &AbstractClient::clientFinishUserMovedResized, this, &AbstractClient::setupCheckScreenConnection); connect(this, &AbstractClient::paletteChanged, this, &AbstractClient::triggerDecorationRepaint); connect(Decoration::DecorationBridge::self(), &QObject::destroyed, this, &AbstractClient::destroyDecoration); // If the user manually moved the window, don't restore it after the keyboard closes connect(this, &AbstractClient::clientFinishUserMovedResized, this, [this] () { m_keyboardGeometryRestore = QRect(); }); connect(this, qOverload(&AbstractClient::clientMaximizedStateChanged), this, [this] () { m_keyboardGeometryRestore = QRect(); }); connect(this, &AbstractClient::fullScreenChanged, this, [this] () { m_keyboardGeometryRestore = QRect(); }); // replace on-screen-display on size changes - connect(this, &AbstractClient::geometryShapeChanged, this, + connect(this, &AbstractClient::frameGeometryChanged, this, [this] (Toplevel *c, const QRect &old) { Q_UNUSED(c) if (isOnScreenDisplay() && !frameGeometry().isEmpty() && old.size() != frameGeometry().size() && !isInitialPositionSet()) { GeometryUpdatesBlocker blocker(this); const QRect area = workspace()->clientArea(PlacementArea, Screens::self()->current(), desktop()); Placement::self()->place(this, area); setGeometryRestore(frameGeometry()); } } ); connect(this, &AbstractClient::paddingChanged, this, [this]() { m_visibleRectBeforeGeometryUpdate = visibleRect(); }); connect(ApplicationMenu::self(), &ApplicationMenu::applicationMenuEnabledChanged, this, [this] { emit hasApplicationMenuChanged(hasApplicationMenu()); }); } AbstractClient::~AbstractClient() { Q_ASSERT(m_blockGeometryUpdates == 0); Q_ASSERT(m_decoration.decoration == nullptr); } void AbstractClient::updateMouseGrab() { } bool AbstractClient::belongToSameApplication(const AbstractClient *c1, const AbstractClient *c2, SameApplicationChecks checks) { return c1->belongsToSameApplication(c2, checks); } bool AbstractClient::isTransient() const { return false; } void AbstractClient::setClientShown(bool shown) { Q_UNUSED(shown) } MaximizeMode AbstractClient::requestedMaximizeMode() const { return maximizeMode(); } xcb_timestamp_t AbstractClient::userTime() const { return XCB_TIME_CURRENT_TIME; } void AbstractClient::setSkipSwitcher(bool set) { set = rules()->checkSkipSwitcher(set); if (set == skipSwitcher()) return; m_skipSwitcher = set; doSetSkipSwitcher(); updateWindowRules(Rules::SkipSwitcher); emit skipSwitcherChanged(); } void AbstractClient::setSkipPager(bool b) { b = rules()->checkSkipPager(b); if (b == skipPager()) return; m_skipPager = b; doSetSkipPager(); updateWindowRules(Rules::SkipPager); emit skipPagerChanged(); } void AbstractClient::doSetSkipPager() { } void AbstractClient::setSkipTaskbar(bool b) { int was_wants_tab_focus = wantsTabFocus(); if (b == skipTaskbar()) return; m_skipTaskbar = b; doSetSkipTaskbar(); updateWindowRules(Rules::SkipTaskbar); if (was_wants_tab_focus != wantsTabFocus()) { FocusChain::self()->update(this, isActive() ? FocusChain::MakeFirst : FocusChain::Update); } emit skipTaskbarChanged(); } void AbstractClient::setOriginalSkipTaskbar(bool b) { m_originalSkipTaskbar = rules()->checkSkipTaskbar(b); setSkipTaskbar(m_originalSkipTaskbar); } void AbstractClient::doSetSkipTaskbar() { } void AbstractClient::doSetSkipSwitcher() { } void AbstractClient::setIcon(const QIcon &icon) { m_icon = icon; emit iconChanged(); } void AbstractClient::setActive(bool act) { if (m_active == act) { return; } m_active = act; const int ruledOpacity = m_active ? rules()->checkOpacityActive(qRound(opacity() * 100.0)) : rules()->checkOpacityInactive(qRound(opacity() * 100.0)); setOpacity(ruledOpacity / 100.0); workspace()->setActiveClient(act ? this : nullptr); if (!m_active) cancelAutoRaise(); if (!m_active && shadeMode() == ShadeActivated) setShade(ShadeNormal); StackingUpdatesBlocker blocker(workspace()); workspace()->updateClientLayer(this); // active windows may get different layer auto mainclients = mainClients(); for (auto it = mainclients.constBegin(); it != mainclients.constEnd(); ++it) if ((*it)->isFullScreen()) // fullscreens go high even if their transient is active workspace()->updateClientLayer(*it); doSetActive(); emit activeChanged(); updateMouseGrab(); } void AbstractClient::doSetActive() { } Layer AbstractClient::layer() const { if (m_layer == UnknownLayer) const_cast< AbstractClient* >(this)->m_layer = belongsToLayer(); return m_layer; } void AbstractClient::updateLayer() { if (layer() == belongsToLayer()) return; StackingUpdatesBlocker blocker(workspace()); invalidateLayer(); // invalidate, will be updated when doing restacking for (auto it = transients().constBegin(), end = transients().constEnd(); it != end; ++it) (*it)->updateLayer(); } void AbstractClient::invalidateLayer() { m_layer = UnknownLayer; } Layer AbstractClient::belongsToLayer() const { // NOTICE while showingDesktop, desktops move to the AboveLayer // (interchangeable w/ eg. yakuake etc. which will at first remain visible) // and the docks move into the NotificationLayer (which is between Above- and // ActiveLayer, so that active fullscreen windows will still cover everything) // Since the desktop is also activated, nothing should be in the ActiveLayer, though if (isInternal()) return UnmanagedLayer; if (isDesktop()) return workspace()->showingDesktop() ? AboveLayer : DesktopLayer; if (isSplash()) // no damn annoying splashscreens return NormalLayer; // getting in the way of everything else if (isDock()) { if (workspace()->showingDesktop()) return NotificationLayer; return layerForDock(); } if (isOnScreenDisplay()) return OnScreenDisplayLayer; if (isNotification()) return NotificationLayer; if (isCriticalNotification()) return CriticalNotificationLayer; if (workspace()->showingDesktop() && belongsToDesktop()) { return AboveLayer; } if (keepBelow()) return BelowLayer; if (isActiveFullScreen()) return ActiveLayer; if (keepAbove()) return AboveLayer; return NormalLayer; } bool AbstractClient::belongsToDesktop() const { return false; } Layer AbstractClient::layerForDock() const { // slight hack for the 'allow window to cover panel' Kicker setting // don't move keepbelow docks below normal window, but only to the same // layer, so that both may be raised to cover the other if (keepBelow()) return NormalLayer; if (keepAbove()) // slight hack for the autohiding panels return AboveLayer; return DockLayer; } void AbstractClient::setKeepAbove(bool b) { b = rules()->checkKeepAbove(b); if (b && !rules()->checkKeepBelow(false)) setKeepBelow(false); if (b == keepAbove()) { // force hint change if different if (info && bool(info->state() & NET::KeepAbove) != keepAbove()) info->setState(keepAbove() ? NET::KeepAbove : NET::States(), NET::KeepAbove); return; } m_keepAbove = b; if (info) { info->setState(keepAbove() ? NET::KeepAbove : NET::States(), NET::KeepAbove); } workspace()->updateClientLayer(this); updateWindowRules(Rules::Above); doSetKeepAbove(); emit keepAboveChanged(m_keepAbove); } void AbstractClient::doSetKeepAbove() { } void AbstractClient::setKeepBelow(bool b) { b = rules()->checkKeepBelow(b); if (b && !rules()->checkKeepAbove(false)) setKeepAbove(false); if (b == keepBelow()) { // force hint change if different if (info && bool(info->state() & NET::KeepBelow) != keepBelow()) info->setState(keepBelow() ? NET::KeepBelow : NET::States(), NET::KeepBelow); return; } m_keepBelow = b; if (info) { info->setState(keepBelow() ? NET::KeepBelow : NET::States(), NET::KeepBelow); } workspace()->updateClientLayer(this); updateWindowRules(Rules::Below); doSetKeepBelow(); emit keepBelowChanged(m_keepBelow); } void AbstractClient::doSetKeepBelow() { } void AbstractClient::startAutoRaise() { delete m_autoRaiseTimer; m_autoRaiseTimer = new QTimer(this); connect(m_autoRaiseTimer, &QTimer::timeout, this, &AbstractClient::autoRaise); m_autoRaiseTimer->setSingleShot(true); m_autoRaiseTimer->start(options->autoRaiseInterval()); } void AbstractClient::cancelAutoRaise() { delete m_autoRaiseTimer; m_autoRaiseTimer = nullptr; } void AbstractClient::autoRaise() { workspace()->raiseClient(this); cancelAutoRaise(); } bool AbstractClient::wantsTabFocus() const { return (isNormalWindow() || isDialog()) && wantsInput(); } bool AbstractClient::isSpecialWindow() const { // TODO return isDesktop() || isDock() || isSplash() || isToolbar() || isNotification() || isOnScreenDisplay() || isCriticalNotification(); } void AbstractClient::demandAttention(bool set) { if (isActive()) set = false; if (m_demandsAttention == set) return; m_demandsAttention = set; if (info) { info->setState(set ? NET::DemandsAttention : NET::States(), NET::DemandsAttention); } workspace()->clientAttentionChanged(this, set); emit demandsAttentionChanged(); } void AbstractClient::setDesktop(int desktop) { const int numberOfDesktops = VirtualDesktopManager::self()->count(); if (desktop != NET::OnAllDesktops) // Do range check desktop = qMax(1, qMin(numberOfDesktops, desktop)); desktop = qMin(numberOfDesktops, rules()->checkDesktop(desktop)); QVector desktops; if (desktop != NET::OnAllDesktops) { desktops << VirtualDesktopManager::self()->desktopForX11Id(desktop); } setDesktops(desktops); } void AbstractClient::setDesktops(QVector desktops) { //on x11 we can have only one desktop at a time if (kwinApp()->operationMode() == Application::OperationModeX11 && desktops.size() > 1) { desktops = QVector({desktops.last()}); } if (desktops == m_desktops) { return; } int was_desk = AbstractClient::desktop(); const bool wasOnCurrentDesktop = isOnCurrentDesktop() && was_desk >= 0; m_desktops = desktops; if (windowManagementInterface()) { if (m_desktops.isEmpty()) { windowManagementInterface()->setOnAllDesktops(true); } else { windowManagementInterface()->setOnAllDesktops(false); auto currentDesktops = windowManagementInterface()->plasmaVirtualDesktops(); for (auto desktop: m_desktops) { if (!currentDesktops.contains(desktop->id())) { windowManagementInterface()->addPlasmaVirtualDesktop(desktop->id()); } else { currentDesktops.removeOne(desktop->id()); } } for (auto desktopId: currentDesktops) { windowManagementInterface()->removePlasmaVirtualDesktop(desktopId); } } } if (info) { info->setDesktop(desktop()); } if ((was_desk == NET::OnAllDesktops) != (desktop() == NET::OnAllDesktops)) { // onAllDesktops changed workspace()->updateOnAllDesktopsOfTransients(this); } auto transients_stacking_order = workspace()->ensureStackingOrder(transients()); for (auto it = transients_stacking_order.constBegin(); it != transients_stacking_order.constEnd(); ++it) (*it)->setDesktops(desktops); if (isModal()) // if a modal dialog is moved, move the mainwindow with it as otherwise // the (just moved) modal dialog will confusingly return to the mainwindow with // the next desktop change { foreach (AbstractClient * c2, mainClients()) c2->setDesktops(desktops); } doSetDesktop(desktop(), was_desk); FocusChain::self()->update(this, FocusChain::MakeFirst); updateWindowRules(Rules::Desktop); emit desktopChanged(); if (wasOnCurrentDesktop != isOnCurrentDesktop()) emit desktopPresenceChanged(this, was_desk); emit x11DesktopIdsChanged(); } void AbstractClient::doSetDesktop(int desktop, int was_desk) { Q_UNUSED(desktop) Q_UNUSED(was_desk) } void AbstractClient::enterDesktop(VirtualDesktop *virtualDesktop) { if (m_desktops.contains(virtualDesktop)) { return; } auto desktops = m_desktops; desktops.append(virtualDesktop); setDesktops(desktops); } void AbstractClient::leaveDesktop(VirtualDesktop *virtualDesktop) { QVector currentDesktops; if (m_desktops.isEmpty()) { currentDesktops = VirtualDesktopManager::self()->desktops(); } else { currentDesktops = m_desktops; } if (!currentDesktops.contains(virtualDesktop)) { return; } auto desktops = currentDesktops; desktops.removeOne(virtualDesktop); setDesktops(desktops); } void AbstractClient::setOnAllDesktops(bool b) { if ((b && isOnAllDesktops()) || (!b && !isOnAllDesktops())) return; if (b) setDesktop(NET::OnAllDesktops); else setDesktop(VirtualDesktopManager::self()->current()); } QVector AbstractClient::x11DesktopIds() const { const auto desks = desktops(); QVector x11Ids; x11Ids.reserve(desks.count()); std::transform(desks.constBegin(), desks.constEnd(), std::back_inserter(x11Ids), [] (const VirtualDesktop *vd) { return vd->x11DesktopNumber(); } ); return x11Ids; } bool AbstractClient::isShadeable() const { return false; } void AbstractClient::setShade(bool set) { set ? setShade(ShadeNormal) : setShade(ShadeNone); } void AbstractClient::setShade(ShadeMode mode) { Q_UNUSED(mode) } ShadeMode AbstractClient::shadeMode() const { return ShadeNone; } AbstractClient::Position AbstractClient::titlebarPosition() const { // TODO: still needed, remove? return PositionTop; } bool AbstractClient::titlebarPositionUnderMouse() const { if (!isDecorated()) { return false; } const auto sectionUnderMouse = decoration()->sectionUnderMouse(); if (sectionUnderMouse == Qt::TitleBarArea) { return true; } // check other sections based on titlebarPosition switch (titlebarPosition()) { case AbstractClient::PositionTop: return (sectionUnderMouse == Qt::TopLeftSection || sectionUnderMouse == Qt::TopSection || sectionUnderMouse == Qt::TopRightSection); case AbstractClient::PositionLeft: return (sectionUnderMouse == Qt::TopLeftSection || sectionUnderMouse == Qt::LeftSection || sectionUnderMouse == Qt::BottomLeftSection); case AbstractClient::PositionRight: return (sectionUnderMouse == Qt::BottomRightSection || sectionUnderMouse == Qt::RightSection || sectionUnderMouse == Qt::TopRightSection); case AbstractClient::PositionBottom: return (sectionUnderMouse == Qt::BottomLeftSection || sectionUnderMouse == Qt::BottomSection || sectionUnderMouse == Qt::BottomRightSection); default: // nothing return false; } } void AbstractClient::setMinimized(bool set) { set ? minimize() : unminimize(); } void AbstractClient::minimize(bool avoid_animation) { if (!isMinimizable() || isMinimized()) return; if (isShade() && info) // NETWM restriction - KWindowInfo::isMinimized() == Hidden && !Shaded info->setState(NET::States(), NET::Shaded); m_minimized = true; doMinimize(); updateWindowRules(Rules::Minimize); FocusChain::self()->update(this, FocusChain::MakeFirstMinimized); // TODO: merge signal with s_minimized emit clientMinimized(this, !avoid_animation); emit minimizedChanged(); } void AbstractClient::unminimize(bool avoid_animation) { if (!isMinimized()) return; if (rules()->checkMinimize(false)) { return; } if (isShade() && info) // NETWM restriction - KWindowInfo::isMinimized() == Hidden && !Shaded info->setState(NET::Shaded, NET::Shaded); m_minimized = false; doMinimize(); updateWindowRules(Rules::Minimize); emit clientUnminimized(this, !avoid_animation); emit minimizedChanged(); } void AbstractClient::doMinimize() { } QPalette AbstractClient::palette() const { if (!m_palette) { return QPalette(); } return m_palette->palette(); } const Decoration::DecorationPalette *AbstractClient::decorationPalette() const { return m_palette.get(); } void AbstractClient::updateColorScheme(QString path) { if (path.isEmpty()) { path = QStringLiteral("kdeglobals"); } if (!m_palette || m_colorScheme != path) { m_colorScheme = path; if (m_palette) { disconnect(m_palette.get(), &Decoration::DecorationPalette::changed, this, &AbstractClient::handlePaletteChange); } auto it = s_palettes.find(m_colorScheme); if (it == s_palettes.end() || it->expired()) { m_palette = std::make_shared(m_colorScheme); if (m_palette->isValid()) { s_palettes[m_colorScheme] = m_palette; } else { if (!s_defaultPalette) { s_defaultPalette = std::make_shared(QStringLiteral("kdeglobals")); s_palettes[QStringLiteral("kdeglobals")] = s_defaultPalette; } m_palette = s_defaultPalette; } if (m_colorScheme == QStringLiteral("kdeglobals")) { s_defaultPalette = m_palette; } } else { m_palette = it->lock(); } connect(m_palette.get(), &Decoration::DecorationPalette::changed, this, &AbstractClient::handlePaletteChange); emit paletteChanged(palette()); emit colorSchemeChanged(); } } void AbstractClient::handlePaletteChange() { emit paletteChanged(palette()); } void AbstractClient::keepInArea(QRect area, bool partial) { if (partial) { // increase the area so that can have only 100 pixels in the area area.setLeft(qMin(area.left() - width() + 100, area.left())); area.setTop(qMin(area.top() - height() + 100, area.top())); area.setRight(qMax(area.right() + width() - 100, area.right())); area.setBottom(qMax(area.bottom() + height() - 100, area.bottom())); } if (!partial) { // resize to fit into area if (area.width() < width() || area.height() < height()) resizeWithChecks(qMin(area.width(), width()), qMin(area.height(), height())); } int tx = x(), ty = y(); if (frameGeometry().right() > area.right() && width() <= area.width()) tx = area.right() - width() + 1; if (frameGeometry().bottom() > area.bottom() && height() <= area.height()) ty = area.bottom() - height() + 1; if (!area.contains(frameGeometry().topLeft())) { if (tx < area.x()) tx = area.x(); if (ty < area.y()) ty = area.y(); } if (tx != x() || ty != y()) move(tx, ty); } QSize AbstractClient::maxSize() const { return rules()->checkMaxSize(QSize(INT_MAX, INT_MAX)); } QSize AbstractClient::minSize() const { return rules()->checkMinSize(QSize(0, 0)); } void AbstractClient::blockGeometryUpdates(bool block) { if (block) { if (m_blockGeometryUpdates == 0) m_pendingGeometryUpdate = PendingGeometryNone; ++m_blockGeometryUpdates; } else { if (--m_blockGeometryUpdates == 0) { if (m_pendingGeometryUpdate != PendingGeometryNone) { if (isShade()) setFrameGeometry(QRect(pos(), adjustedSize()), NormalGeometrySet); else setFrameGeometry(frameGeometry(), NormalGeometrySet); m_pendingGeometryUpdate = PendingGeometryNone; } } } } void AbstractClient::maximize(MaximizeMode m) { setMaximize(m & MaximizeVertical, m & MaximizeHorizontal); } void AbstractClient::setMaximize(bool vertically, bool horizontally) { // changeMaximize() flips the state, so change from set->flip const MaximizeMode oldMode = maximizeMode(); changeMaximize( oldMode & MaximizeHorizontal ? !horizontally : horizontally, oldMode & MaximizeVertical ? !vertically : vertically, false); const MaximizeMode newMode = maximizeMode(); if (oldMode != newMode) { emit clientMaximizedStateChanged(this, newMode); emit clientMaximizedStateChanged(this, vertically, horizontally); } } void AbstractClient::move(int x, int y, ForceGeometry_t force) { // resuming geometry updates is handled only in setGeometry() Q_ASSERT(pendingGeometryUpdate() == PendingGeometryNone || areGeometryUpdatesBlocked()); QPoint p(x, y); if (!areGeometryUpdatesBlocked() && p != rules()->checkPosition(p)) { qCDebug(KWIN_CORE) << "forced position fail:" << p << ":" << rules()->checkPosition(p); } if (force == NormalGeometrySet && m_frameGeometry.topLeft() == p) return; m_frameGeometry.moveTopLeft(p); if (areGeometryUpdatesBlocked()) { if (pendingGeometryUpdate() == PendingGeometryForced) {} // maximum, nothing needed else if (force == ForceGeometrySet) setPendingGeometryUpdate(PendingGeometryForced); else setPendingGeometryUpdate(PendingGeometryNormal); return; } doMove(x, y); updateWindowRules(Rules::Position); screens()->setCurrent(this); workspace()->updateStackingOrder(); // client itself is not damaged + emit frameGeometryChanged(this, frameGeometryBeforeUpdateBlocking()); addRepaintDuringGeometryUpdates(); updateGeometryBeforeUpdateBlocking(); - emit geometryChanged(); } bool AbstractClient::startMoveResize() { Q_ASSERT(!isMoveResize()); Q_ASSERT(QWidget::keyboardGrabber() == nullptr); Q_ASSERT(QWidget::mouseGrabber() == nullptr); stopDelayedMoveResize(); if (QApplication::activePopupWidget() != nullptr) return false; // popups have grab if (isFullScreen() && (screens()->count() < 2 || !isMovableAcrossScreens())) return false; if (!doStartMoveResize()) { return false; } invalidateDecorationDoubleClickTimer(); setMoveResize(true); workspace()->setMoveResizeClient(this); const Position mode = moveResizePointerMode(); if (mode != PositionCenter) { // means "isResize()" but moveResizeMode = true is set below if (maximizeMode() == MaximizeFull) { // partial is cond. reset in finishMoveResize setGeometryRestore(frameGeometry()); // "restore" to current geometry setMaximize(false, false); } } if (quickTileMode() != QuickTileMode(QuickTileFlag::None) && mode != PositionCenter) { // Cannot use isResize() yet // Exit quick tile mode when the user attempts to resize a tiled window updateQuickTileMode(QuickTileFlag::None); // Do so without restoring original geometry setGeometryRestore(frameGeometry()); emit quickTileModeChanged(); } updateHaveResizeEffect(); updateInitialMoveResizeGeometry(); checkUnrestrictedMoveResize(); emit clientStartUserMovedResized(this); if (ScreenEdges::self()->isDesktopSwitchingMovingClients()) ScreenEdges::self()->reserveDesktopSwitching(true, Qt::Vertical|Qt::Horizontal); return true; } void AbstractClient::finishMoveResize(bool cancel) { GeometryUpdatesBlocker blocker(this); const bool wasResize = isResize(); // store across leaveMoveResize leaveMoveResize(); if (cancel) setFrameGeometry(initialMoveResizeGeometry()); else { const QRect &moveResizeGeom = moveResizeGeometry(); if (wasResize) { const bool restoreH = maximizeMode() == MaximizeHorizontal && moveResizeGeom.width() != initialMoveResizeGeometry().width(); const bool restoreV = maximizeMode() == MaximizeVertical && moveResizeGeom.height() != initialMoveResizeGeometry().height(); if (restoreH || restoreV) { changeMaximize(restoreH, restoreV, false); } } setFrameGeometry(moveResizeGeom); } checkScreen(); // needs to be done because clientFinishUserMovedResized has not yet re-activated online alignment if (screen() != moveResizeStartScreen()) { workspace()->sendClientToScreen(this, screen()); // checks rule validity if (maximizeMode() != MaximizeRestore) checkWorkspacePosition(); } if (isElectricBorderMaximizing()) { setQuickTileMode(electricBorderMode()); setElectricBorderMaximizing(false); } else if (!cancel) { QRect geom_restore = geometryRestore(); if (!(maximizeMode() & MaximizeHorizontal)) { geom_restore.setX(frameGeometry().x()); geom_restore.setWidth(frameGeometry().width()); } if (!(maximizeMode() & MaximizeVertical)) { geom_restore.setY(frameGeometry().y()); geom_restore.setHeight(frameGeometry().height()); } setGeometryRestore(geom_restore); } // FRAME update(); emit clientFinishUserMovedResized(this); } // This function checks if it actually makes sense to perform a restricted move/resize. // If e.g. the titlebar is already outside of the workarea, there's no point in performing // a restricted move resize, because then e.g. resize would also move the window (#74555). // NOTE: Most of it is duplicated from handleMoveResize(). void AbstractClient::checkUnrestrictedMoveResize() { if (isUnrestrictedMoveResize()) return; const QRect &moveResizeGeom = moveResizeGeometry(); QRect desktopArea = workspace()->clientArea(WorkArea, moveResizeGeom.center(), desktop()); int left_marge, right_marge, top_marge, bottom_marge, titlebar_marge; // restricted move/resize - keep at least part of the titlebar always visible // how much must remain visible when moved away in that direction left_marge = qMin(100 + borderRight(), moveResizeGeom.width()); right_marge = qMin(100 + borderLeft(), moveResizeGeom.width()); // width/height change with opaque resizing, use the initial ones titlebar_marge = initialMoveResizeGeometry().height(); top_marge = borderBottom(); bottom_marge = borderTop(); if (isResize()) { if (moveResizeGeom.bottom() < desktopArea.top() + top_marge) setUnrestrictedMoveResize(true); if (moveResizeGeom.top() > desktopArea.bottom() - bottom_marge) setUnrestrictedMoveResize(true); if (moveResizeGeom.right() < desktopArea.left() + left_marge) setUnrestrictedMoveResize(true); if (moveResizeGeom.left() > desktopArea.right() - right_marge) setUnrestrictedMoveResize(true); if (!isUnrestrictedMoveResize() && moveResizeGeom.top() < desktopArea.top()) // titlebar mustn't go out setUnrestrictedMoveResize(true); } if (isMove()) { if (moveResizeGeom.bottom() < desktopArea.top() + titlebar_marge - 1) setUnrestrictedMoveResize(true); // no need to check top_marge, titlebar_marge already handles it if (moveResizeGeom.top() > desktopArea.bottom() - bottom_marge + 1) // titlebar mustn't go out setUnrestrictedMoveResize(true); if (moveResizeGeom.right() < desktopArea.left() + left_marge) setUnrestrictedMoveResize(true); if (moveResizeGeom.left() > desktopArea.right() - right_marge) setUnrestrictedMoveResize(true); } } // When the user pressed mouse on the titlebar, don't activate move immediatelly, // since it may be just a click. Activate instead after a delay. Move used to be // activated only after moving by several pixels, but that looks bad. void AbstractClient::startDelayedMoveResize() { Q_ASSERT(!m_moveResize.delayedTimer); m_moveResize.delayedTimer = new QTimer(this); m_moveResize.delayedTimer->setSingleShot(true); connect(m_moveResize.delayedTimer, &QTimer::timeout, this, [this]() { Q_ASSERT(isMoveResizePointerButtonDown()); if (!startMoveResize()) { setMoveResizePointerButtonDown(false); } updateCursor(); stopDelayedMoveResize(); } ); m_moveResize.delayedTimer->start(QApplication::startDragTime()); } void AbstractClient::stopDelayedMoveResize() { delete m_moveResize.delayedTimer; m_moveResize.delayedTimer = nullptr; } void AbstractClient::updateMoveResize(const QPointF ¤tGlobalCursor) { handleMoveResize(pos(), currentGlobalCursor.toPoint()); } void AbstractClient::handleMoveResize(const QPoint &local, const QPoint &global) { const QRect oldGeo = frameGeometry(); handleMoveResize(local.x(), local.y(), global.x(), global.y()); if (!isFullScreen() && isMove()) { if (quickTileMode() != QuickTileMode(QuickTileFlag::None) && oldGeo != frameGeometry()) { GeometryUpdatesBlocker blocker(this); setQuickTileMode(QuickTileFlag::None); const QRect &geom_restore = geometryRestore(); setMoveOffset(QPoint(double(moveOffset().x()) / double(oldGeo.width()) * double(geom_restore.width()), double(moveOffset().y()) / double(oldGeo.height()) * double(geom_restore.height()))); if (rules()->checkMaximize(MaximizeRestore) == MaximizeRestore) setMoveResizeGeometry(geom_restore); handleMoveResize(local.x(), local.y(), global.x(), global.y()); // fix position } else if (quickTileMode() == QuickTileMode(QuickTileFlag::None) && isResizable()) { checkQuickTilingMaximizationZones(global.x(), global.y()); } } } void AbstractClient::handleMoveResize(int x, int y, int x_root, int y_root) { if (isWaitingForMoveResizeSync()) return; // we're still waiting for the client or the timeout const Position mode = moveResizePointerMode(); if ((mode == PositionCenter && !isMovableAcrossScreens()) || (mode != PositionCenter && (isShade() || !isResizable()))) return; if (!isMoveResize()) { QPoint p(QPoint(x/* - padding_left*/, y/* - padding_top*/) - moveOffset()); if (p.manhattanLength() >= QApplication::startDragDistance()) { if (!startMoveResize()) { setMoveResizePointerButtonDown(false); updateCursor(); return; } updateCursor(); } else return; } // ShadeHover or ShadeActive, ShadeNormal was already avoided above if (mode != PositionCenter && shadeMode() != ShadeNone) setShade(ShadeNone); QPoint globalPos(x_root, y_root); // these two points limit the geometry rectangle, i.e. if bottomleft resizing is done, // the bottomleft corner should be at is at (topleft.x(), bottomright().y()) QPoint topleft = globalPos - moveOffset(); QPoint bottomright = globalPos + invertedMoveOffset(); QRect previousMoveResizeGeom = moveResizeGeometry(); // TODO move whole group when moving its leader or when the leader is not mapped? auto titleBarRect = [this](bool &transposed, int &requiredPixels) -> QRect { const QRect &moveResizeGeom = moveResizeGeometry(); QRect r(moveResizeGeom); r.moveTopLeft(QPoint(0,0)); switch (titlebarPosition()) { default: case PositionTop: r.setHeight(borderTop()); break; case PositionLeft: r.setWidth(borderLeft()); transposed = true; break; case PositionBottom: r.setTop(r.bottom() - borderBottom()); break; case PositionRight: r.setLeft(r.right() - borderRight()); transposed = true; break; } // When doing a restricted move we must always keep 100px of the titlebar // visible to allow the user to be able to move it again. requiredPixels = qMin(100 * (transposed ? r.width() : r.height()), moveResizeGeom.width() * moveResizeGeom.height()); return r; }; bool update = false; if (isResize()) { QRect orig = initialMoveResizeGeometry(); SizeMode sizeMode = SizeModeAny; auto calculateMoveResizeGeom = [this, &topleft, &bottomright, &orig, &sizeMode, &mode]() { switch(mode) { case PositionTopLeft: setMoveResizeGeometry(QRect(topleft, orig.bottomRight())); break; case PositionBottomRight: setMoveResizeGeometry(QRect(orig.topLeft(), bottomright)); break; case PositionBottomLeft: setMoveResizeGeometry(QRect(QPoint(topleft.x(), orig.y()), QPoint(orig.right(), bottomright.y()))); break; case PositionTopRight: setMoveResizeGeometry(QRect(QPoint(orig.x(), topleft.y()), QPoint(bottomright.x(), orig.bottom()))); break; case PositionTop: setMoveResizeGeometry(QRect(QPoint(orig.left(), topleft.y()), orig.bottomRight())); sizeMode = SizeModeFixedH; // try not to affect height break; case PositionBottom: setMoveResizeGeometry(QRect(orig.topLeft(), QPoint(orig.right(), bottomright.y()))); sizeMode = SizeModeFixedH; break; case PositionLeft: setMoveResizeGeometry(QRect(QPoint(topleft.x(), orig.top()), orig.bottomRight())); sizeMode = SizeModeFixedW; break; case PositionRight: setMoveResizeGeometry(QRect(orig.topLeft(), QPoint(bottomright.x(), orig.bottom()))); sizeMode = SizeModeFixedW; break; case PositionCenter: default: abort(); break; } }; // first resize (without checking constrains), then snap, then check bounds, then check constrains calculateMoveResizeGeom(); // adjust new size to snap to other windows/borders setMoveResizeGeometry(workspace()->adjustClientSize(this, moveResizeGeometry(), mode)); if (!isUnrestrictedMoveResize()) { // Make sure the titlebar isn't behind a restricted area. We don't need to restrict // the other directions. If not visible enough, move the window to the closest valid // point. We bruteforce this by slowly moving the window back to its previous position QRegion availableArea(workspace()->clientArea(FullArea, -1, 0)); // On the screen availableArea -= workspace()->restrictedMoveArea(desktop()); // Strut areas bool transposed = false; int requiredPixels; QRect bTitleRect = titleBarRect(transposed, requiredPixels); int lastVisiblePixels = -1; QRect lastTry = moveResizeGeometry(); bool titleFailed = false; for (;;) { const QRect titleRect(bTitleRect.translated(moveResizeGeometry().topLeft())); int visiblePixels = 0; int realVisiblePixels = 0; for (const QRect &rect : availableArea) { const QRect r = rect & titleRect; realVisiblePixels += r.width() * r.height(); if ((transposed && r.width() == titleRect.width()) || // Only the full size regions... (!transposed && r.height() == titleRect.height())) // ...prevents long slim areas visiblePixels += r.width() * r.height(); } if (visiblePixels >= requiredPixels) break; // We have reached a valid position if (realVisiblePixels <= lastVisiblePixels) { if (titleFailed && realVisiblePixels < lastVisiblePixels) break; // we won't become better else { if (!titleFailed) setMoveResizeGeometry(lastTry); titleFailed = true; } } lastVisiblePixels = realVisiblePixels; QRect moveResizeGeom = moveResizeGeometry(); lastTry = moveResizeGeom; // Not visible enough, move the window to the closest valid point. We bruteforce // this by slowly moving the window back to its previous position. // The geometry changes at up to two edges, the one with the title (if) shall take // precedence. The opposing edge has no impact on visiblePixels and only one of // the adjacent can alter at a time, ie. it's enough to ignore adjacent edges // if the title edge altered bool leftChanged = previousMoveResizeGeom.left() != moveResizeGeom.left(); bool rightChanged = previousMoveResizeGeom.right() != moveResizeGeom.right(); bool topChanged = previousMoveResizeGeom.top() != moveResizeGeom.top(); bool btmChanged = previousMoveResizeGeom.bottom() != moveResizeGeom.bottom(); auto fixChangedState = [titleFailed](bool &major, bool &counter, bool &ad1, bool &ad2) { counter = false; if (titleFailed) major = false; if (major) ad1 = ad2 = false; }; switch (titlebarPosition()) { default: case PositionTop: fixChangedState(topChanged, btmChanged, leftChanged, rightChanged); break; case PositionLeft: fixChangedState(leftChanged, rightChanged, topChanged, btmChanged); break; case PositionBottom: fixChangedState(btmChanged, topChanged, leftChanged, rightChanged); break; case PositionRight: fixChangedState(rightChanged, leftChanged, topChanged, btmChanged); break; } if (topChanged) moveResizeGeom.setTop(moveResizeGeom.y() + sign(previousMoveResizeGeom.y() - moveResizeGeom.y())); else if (leftChanged) moveResizeGeom.setLeft(moveResizeGeom.x() + sign(previousMoveResizeGeom.x() - moveResizeGeom.x())); else if (btmChanged) moveResizeGeom.setBottom(moveResizeGeom.bottom() + sign(previousMoveResizeGeom.bottom() - moveResizeGeom.bottom())); else if (rightChanged) moveResizeGeom.setRight(moveResizeGeom.right() + sign(previousMoveResizeGeom.right() - moveResizeGeom.right())); else break; // no position changed - that's certainly not good setMoveResizeGeometry(moveResizeGeom); } } // Always obey size hints, even when in "unrestricted" mode QSize size = adjustedSize(moveResizeGeometry().size(), sizeMode); // the new topleft and bottomright corners (after checking size constrains), if they'll be needed topleft = QPoint(moveResizeGeometry().right() - size.width() + 1, moveResizeGeometry().bottom() - size.height() + 1); bottomright = QPoint(moveResizeGeometry().left() + size.width() - 1, moveResizeGeometry().top() + size.height() - 1); orig = moveResizeGeometry(); // if aspect ratios are specified, both dimensions may change. // Therefore grow to the right/bottom if needed. // TODO it should probably obey gravity rather than always using right/bottom ? if (sizeMode == SizeModeFixedH) orig.setRight(bottomright.x()); else if (sizeMode == SizeModeFixedW) orig.setBottom(bottomright.y()); calculateMoveResizeGeom(); if (moveResizeGeometry().size() != previousMoveResizeGeom.size()) update = true; } else if (isMove()) { Q_ASSERT(mode == PositionCenter); if (!isMovable()) { // isMovableAcrossScreens() must have been true to get here // Special moving of maximized windows on Xinerama screens int screen = screens()->number(globalPos); if (isFullScreen()) setMoveResizeGeometry(workspace()->clientArea(FullScreenArea, screen, 0)); else { QRect moveResizeGeom = workspace()->clientArea(MaximizeArea, screen, 0); QSize adjSize = adjustedSize(moveResizeGeom.size(), SizeModeMax); if (adjSize != moveResizeGeom.size()) { QRect r(moveResizeGeom); moveResizeGeom.setSize(adjSize); moveResizeGeom.moveCenter(r.center()); } setMoveResizeGeometry(moveResizeGeom); } } else { // first move, then snap, then check bounds QRect moveResizeGeom = moveResizeGeometry(); moveResizeGeom.moveTopLeft(topleft); moveResizeGeom.moveTopLeft(workspace()->adjustClientPosition(this, moveResizeGeom.topLeft(), isUnrestrictedMoveResize())); setMoveResizeGeometry(moveResizeGeom); if (!isUnrestrictedMoveResize()) { const QRegion strut = workspace()->restrictedMoveArea(desktop()); // Strut areas QRegion availableArea(workspace()->clientArea(FullArea, -1, 0)); // On the screen availableArea -= strut; // Strut areas bool transposed = false; int requiredPixels; QRect bTitleRect = titleBarRect(transposed, requiredPixels); for (;;) { QRect moveResizeGeom = moveResizeGeometry(); const QRect titleRect(bTitleRect.translated(moveResizeGeom.topLeft())); int visiblePixels = 0; for (const QRect &rect : availableArea) { const QRect r = rect & titleRect; if ((transposed && r.width() == titleRect.width()) || // Only the full size regions... (!transposed && r.height() == titleRect.height())) // ...prevents long slim areas visiblePixels += r.width() * r.height(); } if (visiblePixels >= requiredPixels) break; // We have reached a valid position // (esp.) if there're more screens with different struts (panels) it the titlebar // will be movable outside the movearea (covering one of the panels) until it // crosses the panel "too much" (not enough visiblePixels) and then stucks because // it's usually only pushed by 1px to either direction // so we first check whether we intersect suc strut and move the window below it // immediately (it's still possible to hit the visiblePixels >= titlebarArea break // by moving the window slightly downwards, but it won't stuck) // see bug #274466 // and bug #301805 for why we can't just match the titlearea against the screen if (screens()->count() > 1) { // optimization // TODO: could be useful on partial screen struts (half-width panels etc.) int newTitleTop = -1; for (const QRect &r : strut) { if (r.top() == 0 && r.width() > r.height() && // "top panel" r.intersects(moveResizeGeom) && moveResizeGeom.top() < r.bottom()) { newTitleTop = r.bottom() + 1; break; } } if (newTitleTop > -1) { moveResizeGeom.moveTop(newTitleTop); // invalid position, possibly on screen change setMoveResizeGeometry(moveResizeGeom); break; } } int dx = sign(previousMoveResizeGeom.x() - moveResizeGeom.x()), dy = sign(previousMoveResizeGeom.y() - moveResizeGeom.y()); if (visiblePixels && dx) // means there's no full width cap -> favor horizontally dy = 0; else if (dy) dx = 0; // Move it back moveResizeGeom.translate(dx, dy); setMoveResizeGeometry(moveResizeGeom); if (moveResizeGeom == previousMoveResizeGeom) { break; // Prevent lockup } } } } if (moveResizeGeometry().topLeft() != previousMoveResizeGeom.topLeft()) update = true; } else abort(); if (!update) return; if (isResize() && !haveResizeEffect()) { doResizeSync(); } else performMoveResize(); if (isMove()) { ScreenEdges::self()->check(globalPos, QDateTime::fromMSecsSinceEpoch(xTime(), Qt::UTC)); } } void AbstractClient::performMoveResize() { const QRect &moveResizeGeom = moveResizeGeometry(); if (isMove() || (isResize() && !haveResizeEffect())) { setFrameGeometry(moveResizeGeom); } doPerformMoveResize(); positionGeometryTip(); emit clientStepUserMovedResized(this, moveResizeGeom); } bool AbstractClient::hasStrut() const { return false; } void AbstractClient::setupWindowManagementInterface() { if (m_windowManagementInterface) { // already setup return; } if (!waylandServer() || !surface()) { return; } if (!waylandServer()->windowManagement()) { return; } using namespace KWayland::Server; auto w = waylandServer()->windowManagement()->createWindow(waylandServer()->windowManagement()); w->setTitle(caption()); w->setVirtualDesktop(isOnAllDesktops() ? 0 : desktop() - 1); w->setActive(isActive()); w->setFullscreen(isFullScreen()); w->setKeepAbove(keepAbove()); w->setKeepBelow(keepBelow()); w->setMaximized(maximizeMode() == KWin::MaximizeFull); w->setMinimized(isMinimized()); w->setOnAllDesktops(isOnAllDesktops()); w->setDemandsAttention(isDemandingAttention()); w->setCloseable(isCloseable()); w->setMaximizeable(isMaximizable()); w->setMinimizeable(isMinimizable()); w->setFullscreenable(isFullScreenable()); w->setIcon(icon()); auto updateAppId = [this, w] { w->setAppId(QString::fromUtf8(m_desktopFileName.isEmpty() ? resourceClass() : m_desktopFileName)); }; updateAppId(); w->setSkipTaskbar(skipTaskbar()); w->setSkipSwitcher(skipSwitcher()); w->setPid(pid()); w->setShadeable(isShadeable()); w->setShaded(isShade()); w->setResizable(isResizable()); w->setMovable(isMovable()); w->setVirtualDesktopChangeable(true); // FIXME Matches X11Client::actionSupported(), but both should be implemented. w->setParentWindow(transientFor() ? transientFor()->windowManagementInterface() : nullptr); w->setGeometry(frameGeometry()); connect(this, &AbstractClient::skipTaskbarChanged, w, [w, this] { w->setSkipTaskbar(skipTaskbar()); } ); connect(this, &AbstractClient::skipSwitcherChanged, w, [w, this] { w->setSkipSwitcher(skipSwitcher()); } ); connect(this, &AbstractClient::captionChanged, w, [w, this] { w->setTitle(caption()); }); connect(this, &AbstractClient::activeChanged, w, [w, this] { w->setActive(isActive()); }); connect(this, &AbstractClient::fullScreenChanged, w, [w, this] { w->setFullscreen(isFullScreen()); }); connect(this, &AbstractClient::keepAboveChanged, w, &PlasmaWindowInterface::setKeepAbove); connect(this, &AbstractClient::keepBelowChanged, w, &PlasmaWindowInterface::setKeepBelow); connect(this, &AbstractClient::minimizedChanged, w, [w, this] { w->setMinimized(isMinimized()); }); connect(this, static_cast(&AbstractClient::clientMaximizedStateChanged), w, [w] (KWin::AbstractClient *c, MaximizeMode mode) { Q_UNUSED(c); w->setMaximized(mode == KWin::MaximizeFull); } ); connect(this, &AbstractClient::demandsAttentionChanged, w, [w, this] { w->setDemandsAttention(isDemandingAttention()); }); connect(this, &AbstractClient::iconChanged, w, [w, this] { w->setIcon(icon()); } ); connect(this, &AbstractClient::windowClassChanged, w, updateAppId); connect(this, &AbstractClient::desktopFileNameChanged, w, updateAppId); connect(this, &AbstractClient::shadeChanged, w, [w, this] { w->setShaded(isShade()); }); connect(this, &AbstractClient::transientChanged, w, [w, this] { w->setParentWindow(transientFor() ? transientFor()->windowManagementInterface() : nullptr); } ); - connect(this, &AbstractClient::geometryChanged, w, + connect(this, &AbstractClient::frameGeometryChanged, w, [w, this] { w->setGeometry(frameGeometry()); } ); connect(w, &PlasmaWindowInterface::closeRequested, this, [this] { closeWindow(); }); connect(w, &PlasmaWindowInterface::moveRequested, this, [this] { Cursor::setPos(frameGeometry().center()); performMouseCommand(Options::MouseMove, Cursor::pos()); } ); connect(w, &PlasmaWindowInterface::resizeRequested, this, [this] { Cursor::setPos(frameGeometry().bottomRight()); performMouseCommand(Options::MouseResize, Cursor::pos()); } ); connect(w, &PlasmaWindowInterface::virtualDesktopRequested, this, [this] (quint32 desktop) { workspace()->sendClientToDesktop(this, desktop + 1, true); } ); connect(w, &PlasmaWindowInterface::fullscreenRequested, this, [this] (bool set) { setFullScreen(set, false); } ); connect(w, &PlasmaWindowInterface::minimizedRequested, this, [this] (bool set) { if (set) { minimize(); } else { unminimize(); } } ); connect(w, &PlasmaWindowInterface::maximizedRequested, this, [this] (bool set) { maximize(set ? MaximizeFull : MaximizeRestore); } ); connect(w, &PlasmaWindowInterface::keepAboveRequested, this, [this] (bool set) { setKeepAbove(set); } ); connect(w, &PlasmaWindowInterface::keepBelowRequested, this, [this] (bool set) { setKeepBelow(set); } ); connect(w, &PlasmaWindowInterface::demandsAttentionRequested, this, [this] (bool set) { demandAttention(set); } ); connect(w, &PlasmaWindowInterface::activeRequested, this, [this] (bool set) { if (set) { workspace()->activateClient(this, true); } } ); connect(w, &PlasmaWindowInterface::shadedRequested, this, [this] (bool set) { setShade(set); } ); for (const auto vd : m_desktops) { w->addPlasmaVirtualDesktop(vd->id()); } //this is only for the legacy connect(this, &AbstractClient::desktopChanged, w, [w, this] { if (isOnAllDesktops()) { w->setOnAllDesktops(true); return; } w->setVirtualDesktop(desktop() - 1); w->setOnAllDesktops(false); } ); //Plasma Virtual desktop management //show/hide when the window enters/exits from desktop connect(w, &PlasmaWindowInterface::enterPlasmaVirtualDesktopRequested, this, [this] (const QString &desktopId) { VirtualDesktop *vd = VirtualDesktopManager::self()->desktopForId(desktopId.toUtf8()); if (vd) { enterDesktop(vd); } } ); connect(w, &PlasmaWindowInterface::enterNewPlasmaVirtualDesktopRequested, this, [this] () { VirtualDesktopManager::self()->setCount(VirtualDesktopManager::self()->count() + 1); enterDesktop(VirtualDesktopManager::self()->desktops().last()); } ); connect(w, &PlasmaWindowInterface::leavePlasmaVirtualDesktopRequested, this, [this] (const QString &desktopId) { VirtualDesktop *vd = VirtualDesktopManager::self()->desktopForId(desktopId.toUtf8()); if (vd) { leaveDesktop(vd); } } ); m_windowManagementInterface = w; } void AbstractClient::destroyWindowManagementInterface() { if (m_windowManagementInterface) { m_windowManagementInterface->unmap(); m_windowManagementInterface = nullptr; } } Options::MouseCommand AbstractClient::getMouseCommand(Qt::MouseButton button, bool *handled) const { *handled = false; if (button == Qt::NoButton) { return Options::MouseNothing; } if (isActive()) { if (options->isClickRaise()) { *handled = true; return Options::MouseActivateRaiseAndPassClick; } } else { *handled = true; switch (button) { case Qt::LeftButton: return options->commandWindow1(); case Qt::MiddleButton: return options->commandWindow2(); case Qt::RightButton: return options->commandWindow3(); default: // all other buttons pass Activate & Pass Client return Options::MouseActivateAndPassClick; } } return Options::MouseNothing; } Options::MouseCommand AbstractClient::getWheelCommand(Qt::Orientation orientation, bool *handled) const { *handled = false; if (orientation != Qt::Vertical) { return Options::MouseNothing; } if (!isActive()) { *handled = true; return options->commandWindowWheel(); } return Options::MouseNothing; } bool AbstractClient::performMouseCommand(Options::MouseCommand cmd, const QPoint &globalPos) { bool replay = false; switch(cmd) { case Options::MouseRaise: workspace()->raiseClient(this); break; case Options::MouseLower: { workspace()->lowerClient(this); // used to be activateNextClient(this), then topClientOnDesktop // since this is a mouseOp it's however safe to use the client under the mouse instead if (isActive() && options->focusPolicyIsReasonable()) { AbstractClient *next = workspace()->clientUnderMouse(screen()); if (next && next != this) workspace()->requestFocus(next, false); } break; } case Options::MouseOperationsMenu: if (isActive() && options->isClickRaise()) autoRaise(); workspace()->showWindowMenu(QRect(globalPos, globalPos), this); break; case Options::MouseToggleRaiseAndLower: workspace()->raiseOrLowerClient(this); break; case Options::MouseActivateAndRaise: { replay = isActive(); // for clickraise mode bool mustReplay = !rules()->checkAcceptFocus(acceptsFocus()); if (mustReplay) { auto it = workspace()->stackingOrder().constEnd(), begin = workspace()->stackingOrder().constBegin(); while (mustReplay && --it != begin && *it != this) { AbstractClient *c = qobject_cast(*it); if (!c || (c->keepAbove() && !keepAbove()) || (keepBelow() && !c->keepBelow())) continue; // can never raise above "it" mustReplay = !(c->isOnCurrentDesktop() && c->isOnCurrentActivity() && c->frameGeometry().intersects(frameGeometry())); } } workspace()->takeActivity(this, Workspace::ActivityFocus | Workspace::ActivityRaise); screens()->setCurrent(globalPos); replay = replay || mustReplay; break; } case Options::MouseActivateAndLower: workspace()->requestFocus(this); workspace()->lowerClient(this); screens()->setCurrent(globalPos); replay = replay || !rules()->checkAcceptFocus(acceptsFocus()); break; case Options::MouseActivate: replay = isActive(); // for clickraise mode workspace()->takeActivity(this, Workspace::ActivityFocus); screens()->setCurrent(globalPos); replay = replay || !rules()->checkAcceptFocus(acceptsFocus()); break; case Options::MouseActivateRaiseAndPassClick: workspace()->takeActivity(this, Workspace::ActivityFocus | Workspace::ActivityRaise); screens()->setCurrent(globalPos); replay = true; break; case Options::MouseActivateAndPassClick: workspace()->takeActivity(this, Workspace::ActivityFocus); screens()->setCurrent(globalPos); replay = true; break; case Options::MouseMaximize: maximize(MaximizeFull); break; case Options::MouseRestore: maximize(MaximizeRestore); break; case Options::MouseMinimize: minimize(); break; case Options::MouseAbove: { StackingUpdatesBlocker blocker(workspace()); if (keepBelow()) setKeepBelow(false); else setKeepAbove(true); break; } case Options::MouseBelow: { StackingUpdatesBlocker blocker(workspace()); if (keepAbove()) setKeepAbove(false); else setKeepBelow(true); break; } case Options::MousePreviousDesktop: workspace()->windowToPreviousDesktop(this); break; case Options::MouseNextDesktop: workspace()->windowToNextDesktop(this); break; case Options::MouseOpacityMore: if (!isDesktop()) // No point in changing the opacity of the desktop setOpacity(qMin(opacity() + 0.1, 1.0)); break; case Options::MouseOpacityLess: if (!isDesktop()) // No point in changing the opacity of the desktop setOpacity(qMax(opacity() - 0.1, 0.1)); break; case Options::MouseClose: closeWindow(); break; case Options::MouseActivateRaiseAndMove: case Options::MouseActivateRaiseAndUnrestrictedMove: workspace()->raiseClient(this); workspace()->requestFocus(this); screens()->setCurrent(globalPos); // fallthrough case Options::MouseMove: case Options::MouseUnrestrictedMove: { if (!isMovableAcrossScreens()) break; if (isMoveResize()) finishMoveResize(false); setMoveResizePointerMode(PositionCenter); setMoveResizePointerButtonDown(true); setMoveOffset(QPoint(globalPos.x() - x(), globalPos.y() - y())); // map from global setInvertedMoveOffset(rect().bottomRight() - moveOffset()); setUnrestrictedMoveResize((cmd == Options::MouseActivateRaiseAndUnrestrictedMove || cmd == Options::MouseUnrestrictedMove)); if (!startMoveResize()) setMoveResizePointerButtonDown(false); updateCursor(); break; } case Options::MouseResize: case Options::MouseUnrestrictedResize: { if (!isResizable() || isShade()) break; if (isMoveResize()) finishMoveResize(false); setMoveResizePointerButtonDown(true); const QPoint moveOffset = QPoint(globalPos.x() - x(), globalPos.y() - y()); // map from global setMoveOffset(moveOffset); int x = moveOffset.x(), y = moveOffset.y(); bool left = x < width() / 3; bool right = x >= 2 * width() / 3; bool top = y < height() / 3; bool bot = y >= 2 * height() / 3; Position mode; if (top) mode = left ? PositionTopLeft : (right ? PositionTopRight : PositionTop); else if (bot) mode = left ? PositionBottomLeft : (right ? PositionBottomRight : PositionBottom); else mode = (x < width() / 2) ? PositionLeft : PositionRight; setMoveResizePointerMode(mode); setInvertedMoveOffset(rect().bottomRight() - moveOffset); setUnrestrictedMoveResize((cmd == Options::MouseUnrestrictedResize)); if (!startMoveResize()) setMoveResizePointerButtonDown(false); updateCursor(); break; } case Options::MouseNothing: default: replay = true; break; } return replay; } void AbstractClient::setTransientFor(AbstractClient *transientFor) { if (transientFor == this) { // cannot be transient for one self return; } if (m_transientFor == transientFor) { return; } m_transientFor = transientFor; emit transientChanged(); } const AbstractClient *AbstractClient::transientFor() const { return m_transientFor; } AbstractClient *AbstractClient::transientFor() { return m_transientFor; } bool AbstractClient::hasTransientPlacementHint() const { return false; } QRect AbstractClient::transientPlacement(const QRect &bounds) const { Q_UNUSED(bounds); Q_UNREACHABLE(); return QRect(); } bool AbstractClient::hasTransient(const AbstractClient *c, bool indirect) const { Q_UNUSED(indirect); return c->transientFor() == this; } QList< AbstractClient* > AbstractClient::mainClients() const { if (const AbstractClient *t = transientFor()) { return QList{const_cast< AbstractClient* >(t)}; } return QList(); } QList AbstractClient::allMainClients() const { auto result = mainClients(); foreach (const auto *cl, result) { result += cl->allMainClients(); } return result; } void AbstractClient::setModal(bool m) { // Qt-3.2 can have even modal normal windows :( if (m_modal == m) return; m_modal = m; emit modalChanged(); // Changing modality for a mapped window is weird (?) // _NET_WM_STATE_MODAL should possibly rather be _NET_WM_WINDOW_TYPE_MODAL_DIALOG } bool AbstractClient::isModal() const { return m_modal; } void AbstractClient::addTransient(AbstractClient *cl) { Q_ASSERT(!m_transients.contains(cl)); Q_ASSERT(cl != this); m_transients.append(cl); } void AbstractClient::removeTransient(AbstractClient *cl) { m_transients.removeAll(cl); if (cl->transientFor() == this) { cl->setTransientFor(nullptr); } } void AbstractClient::removeTransientFromList(AbstractClient *cl) { m_transients.removeAll(cl); } bool AbstractClient::isActiveFullScreen() const { if (!isFullScreen()) return false; const auto ac = workspace()->mostRecentlyActivatedClient(); // instead of activeClient() - avoids flicker // according to NETWM spec implementation notes suggests // "focused windows having state _NET_WM_STATE_FULLSCREEN" to be on the highest layer. // we'll also take the screen into account return ac && (ac == this || ac->screen() != screen()|| ac->allMainClients().contains(const_cast(this))); } #define BORDER(which) \ int AbstractClient::border##which() const \ { \ return isDecorated() ? decoration()->border##which() : 0; \ } BORDER(Bottom) BORDER(Left) BORDER(Right) BORDER(Top) #undef BORDER QSize AbstractClient::sizeForClientSize(const QSize &wsize, SizeMode mode, bool noframe) const { Q_UNUSED(mode) Q_UNUSED(noframe) return wsize + QSize(borderLeft() + borderRight(), borderTop() + borderBottom()); } void AbstractClient::addRepaintDuringGeometryUpdates() { const QRect deco_rect = visibleRect(); addLayerRepaint(m_visibleRectBeforeGeometryUpdate); addLayerRepaint(deco_rect); // trigger repaint of window's new location m_visibleRectBeforeGeometryUpdate = deco_rect; } QRect AbstractClient::bufferGeometryBeforeUpdateBlocking() const { return m_bufferGeometryBeforeUpdateBlocking; } QRect AbstractClient::frameGeometryBeforeUpdateBlocking() const { return m_frameGeometryBeforeUpdateBlocking; } void AbstractClient::updateGeometryBeforeUpdateBlocking() { m_bufferGeometryBeforeUpdateBlocking = bufferGeometry(); m_frameGeometryBeforeUpdateBlocking = frameGeometry(); } void AbstractClient::doMove(int, int) { } void AbstractClient::updateInitialMoveResizeGeometry() { m_moveResize.initialGeometry = frameGeometry(); m_moveResize.geometry = m_moveResize.initialGeometry; m_moveResize.startScreen = screen(); } void AbstractClient::updateCursor() { Position m = moveResizePointerMode(); if (!isResizable() || isShade()) m = PositionCenter; CursorShape c = Qt::ArrowCursor; switch(m) { case PositionTopLeft: c = KWin::ExtendedCursor::SizeNorthWest; break; case PositionBottomRight: c = KWin::ExtendedCursor::SizeSouthEast; break; case PositionBottomLeft: c = KWin::ExtendedCursor::SizeSouthWest; break; case PositionTopRight: c = KWin::ExtendedCursor::SizeNorthEast; break; case PositionTop: c = KWin::ExtendedCursor::SizeNorth; break; case PositionBottom: c = KWin::ExtendedCursor::SizeSouth; break; case PositionLeft: c = KWin::ExtendedCursor::SizeWest; break; case PositionRight: c = KWin::ExtendedCursor::SizeEast; break; default: if (isMoveResize()) c = Qt::SizeAllCursor; else c = Qt::ArrowCursor; break; } if (c == m_moveResize.cursor) return; m_moveResize.cursor = c; emit moveResizeCursorChanged(c); } void AbstractClient::leaveMoveResize() { workspace()->setMoveResizeClient(nullptr); setMoveResize(false); if (ScreenEdges::self()->isDesktopSwitchingMovingClients()) ScreenEdges::self()->reserveDesktopSwitching(false, Qt::Vertical|Qt::Horizontal); if (isElectricBorderMaximizing()) { outline()->hide(); elevate(false); } } bool AbstractClient::s_haveResizeEffect = false; void AbstractClient::updateHaveResizeEffect() { s_haveResizeEffect = effects && static_cast(effects)->provides(Effect::Resize); } bool AbstractClient::doStartMoveResize() { return true; } void AbstractClient::positionGeometryTip() { } void AbstractClient::doPerformMoveResize() { } bool AbstractClient::isWaitingForMoveResizeSync() const { return false; } void AbstractClient::doResizeSync() { } void AbstractClient::checkQuickTilingMaximizationZones(int xroot, int yroot) { QuickTileMode mode = QuickTileFlag::None; bool innerBorder = false; for (int i=0; i < screens()->count(); ++i) { if (!screens()->geometry(i).contains(QPoint(xroot, yroot))) continue; auto isInScreen = [i](const QPoint &pt) { for (int j = 0; j < screens()->count(); ++j) { if (j == i) continue; if (screens()->geometry(j).contains(pt)) { return true; } } return false; }; QRect area = workspace()->clientArea(MaximizeArea, QPoint(xroot, yroot), desktop()); if (options->electricBorderTiling()) { if (xroot <= area.x() + 20) { mode |= QuickTileFlag::Left; innerBorder = isInScreen(QPoint(area.x() - 1, yroot)); } else if (xroot >= area.x() + area.width() - 20) { mode |= QuickTileFlag::Right; innerBorder = isInScreen(QPoint(area.right() + 1, yroot)); } } if (mode != QuickTileMode(QuickTileFlag::None)) { if (yroot <= area.y() + area.height() * options->electricBorderCornerRatio()) mode |= QuickTileFlag::Top; else if (yroot >= area.y() + area.height() - area.height() * options->electricBorderCornerRatio()) mode |= QuickTileFlag::Bottom; } else if (options->electricBorderMaximize() && yroot <= area.y() + 5 && isMaximizable()) { mode = QuickTileFlag::Maximize; innerBorder = isInScreen(QPoint(xroot, area.y() - 1)); } break; // no point in checking other screens to contain this... "point"... } if (mode != electricBorderMode()) { setElectricBorderMode(mode); if (innerBorder) { if (!m_electricMaximizingDelay) { m_electricMaximizingDelay = new QTimer(this); m_electricMaximizingDelay->setInterval(250); m_electricMaximizingDelay->setSingleShot(true); connect(m_electricMaximizingDelay, &QTimer::timeout, [this]() { if (isMove()) setElectricBorderMaximizing(electricBorderMode() != QuickTileMode(QuickTileFlag::None)); }); } m_electricMaximizingDelay->start(); } else { setElectricBorderMaximizing(mode != QuickTileMode(QuickTileFlag::None)); } } } void AbstractClient::keyPressEvent(uint key_code) { if (!isMove() && !isResize()) return; bool is_control = key_code & Qt::CTRL; bool is_alt = key_code & Qt::ALT; key_code = key_code & ~Qt::KeyboardModifierMask; int delta = is_control ? 1 : is_alt ? 32 : 8; QPoint pos = Cursor::pos(); switch(key_code) { case Qt::Key_Left: pos.rx() -= delta; break; case Qt::Key_Right: pos.rx() += delta; break; case Qt::Key_Up: pos.ry() -= delta; break; case Qt::Key_Down: pos.ry() += delta; break; case Qt::Key_Space: case Qt::Key_Return: case Qt::Key_Enter: setMoveResizePointerButtonDown(false); finishMoveResize(false); updateCursor(); break; case Qt::Key_Escape: setMoveResizePointerButtonDown(false); finishMoveResize(true); updateCursor(); break; default: return; } Cursor::setPos(pos); } QSize AbstractClient::resizeIncrements() const { return QSize(1, 1); } void AbstractClient::dontMoveResize() { setMoveResizePointerButtonDown(false); stopDelayedMoveResize(); if (isMoveResize()) finishMoveResize(false); } AbstractClient::Position AbstractClient::mousePosition() const { if (isDecorated()) { switch (decoration()->sectionUnderMouse()) { case Qt::BottomLeftSection: return PositionBottomLeft; case Qt::BottomRightSection: return PositionBottomRight; case Qt::BottomSection: return PositionBottom; case Qt::LeftSection: return PositionLeft; case Qt::RightSection: return PositionRight; case Qt::TopSection: return PositionTop; case Qt::TopLeftSection: return PositionTopLeft; case Qt::TopRightSection: return PositionTopRight; default: return PositionCenter; } } return PositionCenter; } void AbstractClient::endMoveResize() { setMoveResizePointerButtonDown(false); stopDelayedMoveResize(); if (isMoveResize()) { finishMoveResize(false); setMoveResizePointerMode(mousePosition()); } updateCursor(); } void AbstractClient::destroyDecoration() { delete m_decoration.decoration; m_decoration.decoration = nullptr; } bool AbstractClient::decorationHasAlpha() const { if (!isDecorated() || decoration()->isOpaque()) { // either no decoration or decoration has alpha disabled return false; } return true; } void AbstractClient::triggerDecorationRepaint() { if (isDecorated()) { decoration()->update(); } } void AbstractClient::layoutDecorationRects(QRect &left, QRect &top, QRect &right, QRect &bottom) const { if (!isDecorated()) { return; } QRect r = decoration()->rect(); top = QRect(r.x(), r.y(), r.width(), borderTop()); bottom = QRect(r.x(), r.y() + r.height() - borderBottom(), r.width(), borderBottom()); left = QRect(r.x(), r.y() + top.height(), borderLeft(), r.height() - top.height() - bottom.height()); right = QRect(r.x() + r.width() - borderRight(), r.y() + top.height(), borderRight(), r.height() - top.height() - bottom.height()); } void AbstractClient::processDecorationMove(const QPoint &localPos, const QPoint &globalPos) { if (isMoveResizePointerButtonDown()) { handleMoveResize(localPos.x(), localPos.y(), globalPos.x(), globalPos.y()); return; } // TODO: handle modifiers Position newmode = mousePosition(); if (newmode != moveResizePointerMode()) { setMoveResizePointerMode(newmode); updateCursor(); } } bool AbstractClient::processDecorationButtonPress(QMouseEvent *event, bool ignoreMenu) { Options::MouseCommand com = Options::MouseNothing; bool active = isActive(); if (!wantsInput()) // we cannot be active, use it anyway active = true; // check whether it is a double click if (event->button() == Qt::LeftButton && titlebarPositionUnderMouse()) { if (m_decoration.doubleClickTimer.isValid()) { const qint64 interval = m_decoration.doubleClickTimer.elapsed(); m_decoration.doubleClickTimer.invalidate(); if (interval > QGuiApplication::styleHints()->mouseDoubleClickInterval()) { m_decoration.doubleClickTimer.start(); // expired -> new first click and pot. init } else { Workspace::self()->performWindowOperation(this, options->operationTitlebarDblClick()); dontMoveResize(); return false; } } else { m_decoration.doubleClickTimer.start(); // new first click and pot. init, could be invalidated by release - see below } } if (event->button() == Qt::LeftButton) com = active ? options->commandActiveTitlebar1() : options->commandInactiveTitlebar1(); else if (event->button() == Qt::MidButton) com = active ? options->commandActiveTitlebar2() : options->commandInactiveTitlebar2(); else if (event->button() == Qt::RightButton) com = active ? options->commandActiveTitlebar3() : options->commandInactiveTitlebar3(); if (event->button() == Qt::LeftButton && com != Options::MouseOperationsMenu // actions where it's not possible to get the matching && com != Options::MouseMinimize) // mouse release event { setMoveResizePointerMode(mousePosition()); setMoveResizePointerButtonDown(true); setMoveOffset(event->pos()); setInvertedMoveOffset(rect().bottomRight() - moveOffset()); setUnrestrictedMoveResize(false); startDelayedMoveResize(); updateCursor(); } // In the new API the decoration may process the menu action to display an inactive tab's menu. // If the event is unhandled then the core will create one for the active window in the group. if (!ignoreMenu || com != Options::MouseOperationsMenu) performMouseCommand(com, event->globalPos()); return !( // Return events that should be passed to the decoration in the new API com == Options::MouseRaise || com == Options::MouseOperationsMenu || com == Options::MouseActivateAndRaise || com == Options::MouseActivate || com == Options::MouseActivateRaiseAndPassClick || com == Options::MouseActivateAndPassClick || com == Options::MouseNothing); } void AbstractClient::processDecorationButtonRelease(QMouseEvent *event) { if (isDecorated()) { if (event->isAccepted() || !titlebarPositionUnderMouse()) { invalidateDecorationDoubleClickTimer(); // click was for the deco and shall not init a doubleclick } } if (event->buttons() == Qt::NoButton) { setMoveResizePointerButtonDown(false); stopDelayedMoveResize(); if (isMoveResize()) { finishMoveResize(false); setMoveResizePointerMode(mousePosition()); } updateCursor(); } } void AbstractClient::startDecorationDoubleClickTimer() { m_decoration.doubleClickTimer.start(); } void AbstractClient::invalidateDecorationDoubleClickTimer() { m_decoration.doubleClickTimer.invalidate(); } bool AbstractClient::providesContextHelp() const { return false; } void AbstractClient::showContextHelp() { } QPointer AbstractClient::decoratedClient() const { return m_decoration.client; } void AbstractClient::setDecoratedClient(QPointer< Decoration::DecoratedClientImpl > client) { m_decoration.client = client; } void AbstractClient::enterEvent(const QPoint &globalPos) { // TODO: shade hover if (options->focusPolicy() == Options::ClickToFocus || workspace()->userActionsMenu()->isShown()) return; if (options->isAutoRaise() && !isDesktop() && !isDock() && workspace()->focusChangeEnabled() && globalPos != workspace()->focusMousePosition() && workspace()->topClientOnDesktop(VirtualDesktopManager::self()->current(), options->isSeparateScreenFocus() ? screen() : -1) != this) { startAutoRaise(); } if (isDesktop() || isDock()) return; // for FocusFollowsMouse, change focus only if the mouse has actually been moved, not if the focus // change came because of window changes (e.g. closing a window) - #92290 if (options->focusPolicy() != Options::FocusFollowsMouse || globalPos != workspace()->focusMousePosition()) { workspace()->requestDelayFocus(this); } } void AbstractClient::leaveEvent() { cancelAutoRaise(); workspace()->cancelDelayFocus(); // TODO: shade hover // TODO: send hover leave to deco // TODO: handle Options::FocusStrictlyUnderMouse } QRect AbstractClient::iconGeometry() const { if (!windowManagementInterface() || !waylandServer()) { // window management interface is only available if the surface is mapped return QRect(); } int minDistance = INT_MAX; AbstractClient *candidatePanel = nullptr; QRect candidateGeom; for (auto i = windowManagementInterface()->minimizedGeometries().constBegin(), end = windowManagementInterface()->minimizedGeometries().constEnd(); i != end; ++i) { AbstractClient *client = waylandServer()->findAbstractClient(i.key()); if (!client) { continue; } const int distance = QPoint(client->pos() - pos()).manhattanLength(); if (distance < minDistance) { minDistance = distance; candidatePanel = client; candidateGeom = i.value(); } } if (!candidatePanel) { return QRect(); } return candidateGeom.translated(candidatePanel->pos()); } QRect AbstractClient::inputGeometry() const { if (isDecorated()) { return Toplevel::inputGeometry() + decoration()->resizeOnlyBorders(); } return Toplevel::inputGeometry(); } QRect AbstractClient::virtualKeyboardGeometry() const { return m_virtualKeyboardGeometry; } void AbstractClient::setVirtualKeyboardGeometry(const QRect &geo) { // No keyboard anymore if (geo.isEmpty() && !m_keyboardGeometryRestore.isEmpty()) { setFrameGeometry(m_keyboardGeometryRestore); m_keyboardGeometryRestore = QRect(); } else if (geo.isEmpty()) { return; // The keyboard has just been opened (rather than resized) save client geometry for a restore } else if (m_keyboardGeometryRestore.isEmpty()) { m_keyboardGeometryRestore = frameGeometry(); } m_virtualKeyboardGeometry = geo; // Don't resize Desktop and fullscreen windows if (isFullScreen() || isDesktop()) { return; } if (!geo.intersects(m_keyboardGeometryRestore)) { return; } const QRect availableArea = workspace()->clientArea(MaximizeArea, this); QRect newWindowGeometry = m_keyboardGeometryRestore; newWindowGeometry.moveBottom(geo.top()); newWindowGeometry.setTop(qMax(newWindowGeometry.top(), availableArea.top())); setFrameGeometry(newWindowGeometry); } bool AbstractClient::dockWantsInput() const { return false; } void AbstractClient::setDesktopFileName(QByteArray name) { name = rules()->checkDesktopFile(name).toUtf8(); if (name == m_desktopFileName) { return; } m_desktopFileName = name; updateWindowRules(Rules::DesktopFile); emit desktopFileNameChanged(); } QString AbstractClient::iconFromDesktopFile() const { if (m_desktopFileName.isEmpty()) { return QString(); } QString desktopFile = QString::fromUtf8(m_desktopFileName); if (!desktopFile.endsWith(QLatin1String(".desktop"))) { desktopFile.append(QLatin1String(".desktop")); } KDesktopFile df(desktopFile); return df.readIcon(); } bool AbstractClient::hasApplicationMenu() const { return ApplicationMenu::self()->applicationMenuEnabled() && !m_applicationMenuServiceName.isEmpty() && !m_applicationMenuObjectPath.isEmpty(); } void AbstractClient::updateApplicationMenuServiceName(const QString &serviceName) { const bool old_hasApplicationMenu = hasApplicationMenu(); m_applicationMenuServiceName = serviceName; const bool new_hasApplicationMenu = hasApplicationMenu(); if (old_hasApplicationMenu != new_hasApplicationMenu) { emit hasApplicationMenuChanged(new_hasApplicationMenu); } } void AbstractClient::updateApplicationMenuObjectPath(const QString &objectPath) { const bool old_hasApplicationMenu = hasApplicationMenu(); m_applicationMenuObjectPath = objectPath; const bool new_hasApplicationMenu = hasApplicationMenu(); if (old_hasApplicationMenu != new_hasApplicationMenu) { emit hasApplicationMenuChanged(new_hasApplicationMenu); } } void AbstractClient::setApplicationMenuActive(bool applicationMenuActive) { if (m_applicationMenuActive != applicationMenuActive) { m_applicationMenuActive = applicationMenuActive; emit applicationMenuActiveChanged(applicationMenuActive); } } void AbstractClient::showApplicationMenu(int actionId) { if (isDecorated()) { decoration()->showApplicationMenu(actionId); } else { // we don't know where the application menu button will be, show it in the top left corner instead Workspace::self()->showApplicationMenu(QRect(), this, actionId); } } bool AbstractClient::unresponsive() const { return m_unresponsive; } void AbstractClient::setUnresponsive(bool unresponsive) { if (m_unresponsive != unresponsive) { m_unresponsive = unresponsive; emit unresponsiveChanged(m_unresponsive); emit captionChanged(); } } QString AbstractClient::shortcutCaptionSuffix() const { if (shortcut().isEmpty()) { return QString(); } return QLatin1String(" {") + shortcut().toString() + QLatin1Char('}'); } AbstractClient *AbstractClient::findClientWithSameCaption() const { auto fetchNameInternalPredicate = [this](const AbstractClient *cl) { return (!cl->isSpecialWindow() || cl->isToolbar()) && cl != this && cl->captionNormal() == captionNormal() && cl->captionSuffix() == captionSuffix(); }; return workspace()->findAbstractClient(fetchNameInternalPredicate); } QString AbstractClient::caption() const { QString cap = captionNormal() + captionSuffix(); if (unresponsive()) { cap += QLatin1String(" "); cap += i18nc("Application is not responding, appended to window title", "(Not Responding)"); } return cap; } void AbstractClient::removeRule(Rules* rule) { m_rules.remove(rule); } void AbstractClient::discardTemporaryRules() { m_rules.discardTemporary(); } void AbstractClient::evaluateWindowRules() { setupWindowRules(true); applyWindowRules(); } void AbstractClient::setOnActivities(QStringList newActivitiesList) { Q_UNUSED(newActivitiesList) } void AbstractClient::checkNoBorder() { setNoBorder(false); } bool AbstractClient::groupTransient() const { return false; } const Group *AbstractClient::group() const { return nullptr; } Group *AbstractClient::group() { return nullptr; } bool AbstractClient::isInternal() const { return false; } bool AbstractClient::supportsWindowRules() const { return true; } QMargins AbstractClient::frameMargins() const { return QMargins(borderLeft(), borderTop(), borderRight(), borderBottom()); } QPoint AbstractClient::framePosToClientPos(const QPoint &point) const { return point + QPoint(borderLeft(), borderTop()); } QPoint AbstractClient::clientPosToFramePos(const QPoint &point) const { return point - QPoint(borderLeft(), borderTop()); } QSize AbstractClient::frameSizeToClientSize(const QSize &size) const { const int width = size.width() - borderLeft() - borderRight(); const int height = size.height() - borderTop() - borderBottom(); return QSize(width, height); } QSize AbstractClient::clientSizeToFrameSize(const QSize &size) const { const int width = size.width() + borderLeft() + borderRight(); const int height = size.height() + borderTop() + borderBottom(); return QSize(width, height); } QRect AbstractClient::frameRectToClientRect(const QRect &rect) const { const QPoint position = framePosToClientPos(rect.topLeft()); const QSize size = frameSizeToClientSize(rect.size()); return QRect(position, size); } QRect AbstractClient::clientRectToFrameRect(const QRect &rect) const { const QPoint position = clientPosToFramePos(rect.topLeft()); const QSize size = clientSizeToFrameSize(rect.size()); return QRect(position, size); } void AbstractClient::setElectricBorderMode(QuickTileMode mode) { if (mode != QuickTileMode(QuickTileFlag::Maximize)) { // sanitize the mode, ie. simplify "invalid" combinations if ((mode & QuickTileFlag::Horizontal) == QuickTileMode(QuickTileFlag::Horizontal)) mode &= ~QuickTileMode(QuickTileFlag::Horizontal); if ((mode & QuickTileFlag::Vertical) == QuickTileMode(QuickTileFlag::Vertical)) mode &= ~QuickTileMode(QuickTileFlag::Vertical); } m_electricMode = mode; } void AbstractClient::setElectricBorderMaximizing(bool maximizing) { m_electricMaximizing = maximizing; if (maximizing) outline()->show(electricBorderMaximizeGeometry(Cursor::pos(), desktop()), moveResizeGeometry()); else outline()->hide(); elevate(maximizing); } QRect AbstractClient::electricBorderMaximizeGeometry(QPoint pos, int desktop) { if (electricBorderMode() == QuickTileMode(QuickTileFlag::Maximize)) { if (maximizeMode() == MaximizeFull) return geometryRestore(); else return workspace()->clientArea(MaximizeArea, pos, desktop); } QRect ret = workspace()->clientArea(MaximizeArea, pos, desktop); if (electricBorderMode() & QuickTileFlag::Left) ret.setRight(ret.left()+ret.width()/2 - 1); else if (electricBorderMode() & QuickTileFlag::Right) ret.setLeft(ret.right()-(ret.width()-ret.width()/2) + 1); if (electricBorderMode() & QuickTileFlag::Top) ret.setBottom(ret.top()+ret.height()/2 - 1); else if (electricBorderMode() & QuickTileFlag::Bottom) ret.setTop(ret.bottom()-(ret.height()-ret.height()/2) + 1); return ret; } void AbstractClient::setQuickTileMode(QuickTileMode mode, bool keyboard) { // Only allow quick tile on a regular window. if (!isResizable()) { return; } workspace()->updateFocusMousePosition(Cursor::pos()); // may cause leave event GeometryUpdatesBlocker blocker(this); if (mode == QuickTileMode(QuickTileFlag::Maximize)) { m_quickTileMode = int(QuickTileFlag::None); if (maximizeMode() == MaximizeFull) { setMaximize(false, false); } else { QRect prev_geom_restore = geometryRestore(); // setMaximize() would set moveResizeGeom as geom_restore m_quickTileMode = int(QuickTileFlag::Maximize); setMaximize(true, true); QRect clientArea = workspace()->clientArea(MaximizeArea, this); if (frameGeometry().top() != clientArea.top()) { QRect r(frameGeometry()); r.moveTop(clientArea.top()); setFrameGeometry(r); } setGeometryRestore(prev_geom_restore); } emit quickTileModeChanged(); return; } // sanitize the mode, ie. simplify "invalid" combinations if ((mode & QuickTileFlag::Horizontal) == QuickTileMode(QuickTileFlag::Horizontal)) mode &= ~QuickTileMode(QuickTileFlag::Horizontal); if ((mode & QuickTileFlag::Vertical) == QuickTileMode(QuickTileFlag::Vertical)) mode &= ~QuickTileMode(QuickTileFlag::Vertical); setElectricBorderMode(mode); // used by ::electricBorderMaximizeGeometry(.) // restore from maximized so that it is possible to tile maximized windows with one hit or by dragging if (maximizeMode() != MaximizeRestore) { if (mode != QuickTileMode(QuickTileFlag::None)) { // decorations may turn off some borders when tiled const ForceGeometry_t geom_mode = isDecorated() ? ForceGeometrySet : NormalGeometrySet; m_quickTileMode = int(QuickTileFlag::None); // Temporary, so the maximize code doesn't get all confused setMaximize(false, false); setFrameGeometry(electricBorderMaximizeGeometry(keyboard ? frameGeometry().center() : Cursor::pos(), desktop()), geom_mode); // Store the mode change m_quickTileMode = mode; } else { m_quickTileMode = mode; setMaximize(false, false); } emit quickTileModeChanged(); return; } if (mode != QuickTileMode(QuickTileFlag::None)) { QPoint whichScreen = keyboard ? frameGeometry().center() : Cursor::pos(); // If trying to tile to the side that the window is already tiled to move the window to the next // screen if it exists, otherwise toggle the mode (set QuickTileFlag::None) if (quickTileMode() == mode) { const int numScreens = screens()->count(); const int curScreen = screen(); int nextScreen = curScreen; QVarLengthArray screens(numScreens); for (int i = 0; i < numScreens; ++i) // Cache screens[i] = Screens::self()->geometry(i); for (int i = 0; i < numScreens; ++i) { if (i == curScreen) continue; if (screens[i].bottom() <= screens[curScreen].top() || screens[i].top() >= screens[curScreen].bottom()) continue; // not in horizontal line const int x = screens[i].center().x(); if ((mode & QuickTileFlag::Horizontal) == QuickTileMode(QuickTileFlag::Left)) { if (x >= screens[curScreen].center().x() || (curScreen != nextScreen && x <= screens[nextScreen].center().x())) continue; // not left of current or more left then found next } else if ((mode & QuickTileFlag::Horizontal) == QuickTileMode(QuickTileFlag::Right)) { if (x <= screens[curScreen].center().x() || (curScreen != nextScreen && x >= screens[nextScreen].center().x())) continue; // not right of current or more right then found next } nextScreen = i; } if (nextScreen == curScreen) { mode = QuickTileFlag::None; // No other screens, toggle tiling } else { // Move to other screen setFrameGeometry(geometryRestore().translated(screens[nextScreen].topLeft() - screens[curScreen].topLeft())); whichScreen = screens[nextScreen].center(); // Swap sides if (mode & QuickTileFlag::Horizontal) { mode = (~mode & QuickTileFlag::Horizontal) | (mode & QuickTileFlag::Vertical); } } setElectricBorderMode(mode); // used by ::electricBorderMaximizeGeometry(.) } else if (quickTileMode() == QuickTileMode(QuickTileFlag::None)) { // Not coming out of an existing tile, not shifting monitors, we're setting a brand new tile. // Store geometry first, so we can go out of this tile later. setGeometryRestore(frameGeometry()); } if (mode != QuickTileMode(QuickTileFlag::None)) { m_quickTileMode = mode; // decorations may turn off some borders when tiled const ForceGeometry_t geom_mode = isDecorated() ? ForceGeometrySet : NormalGeometrySet; // Temporary, so the maximize code doesn't get all confused m_quickTileMode = int(QuickTileFlag::None); setFrameGeometry(electricBorderMaximizeGeometry(whichScreen, desktop()), geom_mode); } // Store the mode change m_quickTileMode = mode; } if (mode == QuickTileMode(QuickTileFlag::None)) { m_quickTileMode = int(QuickTileFlag::None); // Untiling, so just restore geometry, and we're done. if (!geometryRestore().isValid()) // invalid if we started maximized and wait for placement setGeometryRestore(frameGeometry()); // decorations may turn off some borders when tiled const ForceGeometry_t geom_mode = isDecorated() ? ForceGeometrySet : NormalGeometrySet; setFrameGeometry(geometryRestore(), geom_mode); checkWorkspacePosition(); // Just in case it's a different screen } emit quickTileModeChanged(); } void AbstractClient::sendToScreen(int newScreen) { newScreen = rules()->checkScreen(newScreen); if (isActive()) { screens()->setCurrent(newScreen); // might impact the layer of a fullscreen window foreach (AbstractClient *cc, workspace()->allClientList()) { if (cc->isFullScreen() && cc->screen() == newScreen) { cc->updateLayer(); } } } if (screen() == newScreen) // Don't use isOnScreen(), that's true even when only partially return; GeometryUpdatesBlocker blocker(this); // operating on the maximized / quicktiled window would leave the old geom_restore behind, // so we clear the state first MaximizeMode maxMode = maximizeMode(); QuickTileMode qtMode = quickTileMode(); if (maxMode != MaximizeRestore) maximize(MaximizeRestore); if (qtMode != QuickTileMode(QuickTileFlag::None)) setQuickTileMode(QuickTileFlag::None, true); QRect oldScreenArea = workspace()->clientArea(MaximizeArea, this); QRect screenArea = workspace()->clientArea(MaximizeArea, newScreen, desktop()); // the window can have its center so that the position correction moves the new center onto // the old screen, what will tile it where it is. Ie. the screen is not changed // this happens esp. with electric border quicktiling if (qtMode != QuickTileMode(QuickTileFlag::None)) keepInArea(oldScreenArea); QRect oldGeom = frameGeometry(); QRect newGeom = oldGeom; // move the window to have the same relative position to the center of the screen // (i.e. one near the middle of the right edge will also end up near the middle of the right edge) QPoint center = newGeom.center() - oldScreenArea.center(); center.setX(center.x() * screenArea.width() / oldScreenArea.width()); center.setY(center.y() * screenArea.height() / oldScreenArea.height()); center += screenArea.center(); newGeom.moveCenter(center); setFrameGeometry(newGeom); // If the window was inside the old screen area, explicitly make sure its inside also the new screen area. // Calling checkWorkspacePosition() should ensure that, but when moving to a small screen the window could // be big enough to overlap outside of the new screen area, making struts from other screens come into effect, // which could alter the resulting geometry. if (oldScreenArea.contains(oldGeom)) { keepInArea(screenArea); } // align geom_restore - checkWorkspacePosition operates on it setGeometryRestore(frameGeometry()); checkWorkspacePosition(oldGeom); // re-align geom_restore to constrained geometry setGeometryRestore(frameGeometry()); // finally reset special states // NOTICE that MaximizeRestore/QuickTileFlag::None checks are required. // eg. setting QuickTileFlag::None would break maximization if (maxMode != MaximizeRestore) maximize(maxMode); if (qtMode != QuickTileMode(QuickTileFlag::None) && qtMode != quickTileMode()) setQuickTileMode(qtMode, true); auto tso = workspace()->ensureStackingOrder(transients()); for (auto it = tso.constBegin(), end = tso.constEnd(); it != end; ++it) (*it)->sendToScreen(newScreen); } void AbstractClient::checkWorkspacePosition(QRect oldGeometry, int oldDesktop, QRect oldClientGeometry) { enum { Left = 0, Top, Right, Bottom }; const int border[4] = { borderLeft(), borderTop(), borderRight(), borderBottom() }; if( !oldGeometry.isValid()) oldGeometry = frameGeometry(); if( oldDesktop == -2 ) oldDesktop = desktop(); if (!oldClientGeometry.isValid()) oldClientGeometry = oldGeometry.adjusted(border[Left], border[Top], -border[Right], -border[Bottom]); if (isDesktop()) return; if (isFullScreen()) { QRect area = workspace()->clientArea(FullScreenArea, this); if (frameGeometry() != area) setFrameGeometry(area); return; } if (isDock()) return; if (maximizeMode() != MaximizeRestore) { // TODO update geom_restore? changeMaximize(false, false, true); // adjust size const QRect screenArea = workspace()->clientArea(ScreenArea, this); QRect geom = frameGeometry(); checkOffscreenPosition(&geom, screenArea); setFrameGeometry(geom); return; } if (quickTileMode() != QuickTileMode(QuickTileFlag::None)) { setFrameGeometry(electricBorderMaximizeGeometry(frameGeometry().center(), desktop())); return; } // this can be true only if this window was mapped before KWin // was started - in such case, don't adjust position to workarea, // because the window already had its position, and if a window // with a strut altering the workarea would be managed in initialization // after this one, this window would be moved if (!workspace() || workspace()->initializing()) return; // If the window was touching an edge before but not now move it so it is again. // Old and new maximums have different starting values so windows on the screen // edge will move when a new strut is placed on the edge. QRect oldScreenArea; if( workspace()->inUpdateClientArea()) { // we need to find the screen area as it was before the change oldScreenArea = QRect( 0, 0, workspace()->oldDisplayWidth(), workspace()->oldDisplayHeight()); int distance = INT_MAX; foreach(const QRect &r, workspace()->previousScreenSizes()) { int d = r.contains( oldGeometry.center()) ? 0 : ( r.center() - oldGeometry.center()).manhattanLength(); if( d < distance ) { distance = d; oldScreenArea = r; } } } else { oldScreenArea = workspace()->clientArea(ScreenArea, oldGeometry.center(), oldDesktop); } const QRect oldGeomTall = QRect(oldGeometry.x(), oldScreenArea.y(), oldGeometry.width(), oldScreenArea.height()); // Full screen height const QRect oldGeomWide = QRect(oldScreenArea.x(), oldGeometry.y(), oldScreenArea.width(), oldGeometry.height()); // Full screen width int oldTopMax = oldScreenArea.y(); int oldRightMax = oldScreenArea.x() + oldScreenArea.width(); int oldBottomMax = oldScreenArea.y() + oldScreenArea.height(); int oldLeftMax = oldScreenArea.x(); const QRect screenArea = workspace()->clientArea(ScreenArea, geometryRestore().center(), desktop()); int topMax = screenArea.y(); int rightMax = screenArea.x() + screenArea.width(); int bottomMax = screenArea.y() + screenArea.height(); int leftMax = screenArea.x(); QRect newGeom = geometryRestore(); // geometry(); QRect newClientGeom = newGeom.adjusted(border[Left], border[Top], -border[Right], -border[Bottom]); const QRect newGeomTall = QRect(newGeom.x(), screenArea.y(), newGeom.width(), screenArea.height()); // Full screen height const QRect newGeomWide = QRect(screenArea.x(), newGeom.y(), screenArea.width(), newGeom.height()); // Full screen width // Get the max strut point for each side where the window is (E.g. Highest point for // the bottom struts bounded by the window's left and right sides). // These 4 compute old bounds ... auto moveAreaFunc = workspace()->inUpdateClientArea() ? &Workspace::previousRestrictedMoveArea : //... the restricted areas changed &Workspace::restrictedMoveArea; //... when e.g. active desktop or screen changes for (const QRect &r : (workspace()->*moveAreaFunc)(oldDesktop, StrutAreaTop)) { QRect rect = r & oldGeomTall; if (!rect.isEmpty()) oldTopMax = qMax(oldTopMax, rect.y() + rect.height()); } for (const QRect &r : (workspace()->*moveAreaFunc)(oldDesktop, StrutAreaRight)) { QRect rect = r & oldGeomWide; if (!rect.isEmpty()) oldRightMax = qMin(oldRightMax, rect.x()); } for (const QRect &r : (workspace()->*moveAreaFunc)(oldDesktop, StrutAreaBottom)) { QRect rect = r & oldGeomTall; if (!rect.isEmpty()) oldBottomMax = qMin(oldBottomMax, rect.y()); } for (const QRect &r : (workspace()->*moveAreaFunc)(oldDesktop, StrutAreaLeft)) { QRect rect = r & oldGeomWide; if (!rect.isEmpty()) oldLeftMax = qMax(oldLeftMax, rect.x() + rect.width()); } // These 4 compute new bounds for (const QRect &r : workspace()->restrictedMoveArea(desktop(), StrutAreaTop)) { QRect rect = r & newGeomTall; if (!rect.isEmpty()) topMax = qMax(topMax, rect.y() + rect.height()); } for (const QRect &r : workspace()->restrictedMoveArea(desktop(), StrutAreaRight)) { QRect rect = r & newGeomWide; if (!rect.isEmpty()) rightMax = qMin(rightMax, rect.x()); } for (const QRect &r : workspace()->restrictedMoveArea(desktop(), StrutAreaBottom)) { QRect rect = r & newGeomTall; if (!rect.isEmpty()) bottomMax = qMin(bottomMax, rect.y()); } for (const QRect &r : workspace()->restrictedMoveArea(desktop(), StrutAreaLeft)) { QRect rect = r & newGeomWide; if (!rect.isEmpty()) leftMax = qMax(leftMax, rect.x() + rect.width()); } // Check if the sides were inside or touching but are no longer bool keep[4] = {false, false, false, false}; bool save[4] = {false, false, false, false}; int padding[4] = {0, 0, 0, 0}; if (oldGeometry.x() >= oldLeftMax) save[Left] = newGeom.x() < leftMax; if (oldGeometry.x() == oldLeftMax) keep[Left] = newGeom.x() != leftMax; else if (oldClientGeometry.x() == oldLeftMax && newClientGeom.x() != leftMax) { padding[0] = border[Left]; keep[Left] = true; } if (oldGeometry.y() >= oldTopMax) save[Top] = newGeom.y() < topMax; if (oldGeometry.y() == oldTopMax) keep[Top] = newGeom.y() != topMax; else if (oldClientGeometry.y() == oldTopMax && newClientGeom.y() != topMax) { padding[1] = border[Left]; keep[Top] = true; } if (oldGeometry.right() <= oldRightMax - 1) save[Right] = newGeom.right() > rightMax - 1; if (oldGeometry.right() == oldRightMax - 1) keep[Right] = newGeom.right() != rightMax - 1; else if (oldClientGeometry.right() == oldRightMax - 1 && newClientGeom.right() != rightMax - 1) { padding[2] = border[Right]; keep[Right] = true; } if (oldGeometry.bottom() <= oldBottomMax - 1) save[Bottom] = newGeom.bottom() > bottomMax - 1; if (oldGeometry.bottom() == oldBottomMax - 1) keep[Bottom] = newGeom.bottom() != bottomMax - 1; else if (oldClientGeometry.bottom() == oldBottomMax - 1 && newClientGeom.bottom() != bottomMax - 1) { padding[3] = border[Bottom]; keep[Bottom] = true; } // if randomly touches opposing edges, do not favor either if (keep[Left] && keep[Right]) { keep[Left] = keep[Right] = false; padding[0] = padding[2] = 0; } if (keep[Top] && keep[Bottom]) { keep[Top] = keep[Bottom] = false; padding[1] = padding[3] = 0; } if (save[Left] || keep[Left]) newGeom.moveLeft(qMax(leftMax, screenArea.x()) - padding[0]); if (padding[0] && screens()->intersecting(newGeom) > 1) newGeom.moveLeft(newGeom.left() + padding[0]); if (save[Top] || keep[Top]) newGeom.moveTop(qMax(topMax, screenArea.y()) - padding[1]); if (padding[1] && screens()->intersecting(newGeom) > 1) newGeom.moveTop(newGeom.top() + padding[1]); if (save[Right] || keep[Right]) newGeom.moveRight(qMin(rightMax - 1, screenArea.right()) + padding[2]); if (padding[2] && screens()->intersecting(newGeom) > 1) newGeom.moveRight(newGeom.right() - padding[2]); if (oldGeometry.x() >= oldLeftMax && newGeom.x() < leftMax) newGeom.setLeft(qMax(leftMax, screenArea.x())); else if (oldClientGeometry.x() >= oldLeftMax && newGeom.x() + border[Left] < leftMax) { newGeom.setLeft(qMax(leftMax, screenArea.x()) - border[Left]); if (screens()->intersecting(newGeom) > 1) newGeom.setLeft(newGeom.left() + border[Left]); } if (save[Bottom] || keep[Bottom]) newGeom.moveBottom(qMin(bottomMax - 1, screenArea.bottom()) + padding[3]); if (padding[3] && screens()->intersecting(newGeom) > 1) newGeom.moveBottom(newGeom.bottom() - padding[3]); if (oldGeometry.y() >= oldTopMax && newGeom.y() < topMax) newGeom.setTop(qMax(topMax, screenArea.y())); else if (oldClientGeometry.y() >= oldTopMax && newGeom.y() + border[Top] < topMax) { newGeom.setTop(qMax(topMax, screenArea.y()) - border[Top]); if (screens()->intersecting(newGeom) > 1) newGeom.setTop(newGeom.top() + border[Top]); } checkOffscreenPosition(&newGeom, screenArea); // Obey size hints. TODO: We really should make sure it stays in the right place if (!isShade()) newGeom.setSize(adjustedSize(newGeom.size())); if (newGeom != frameGeometry()) setFrameGeometry(newGeom); } void AbstractClient::checkOffscreenPosition(QRect* geom, const QRect& screenArea) { if (geom->left() > screenArea.right()) { geom->moveLeft(screenArea.right() - screenArea.width()/4); } else if (geom->right() < screenArea.left()) { geom->moveRight(screenArea.left() + screenArea.width()/4); } if (geom->top() > screenArea.bottom()) { geom->moveTop(screenArea.bottom() - screenArea.height()/4); } else if (geom->bottom() < screenArea.top()) { geom->moveBottom(screenArea.top() + screenArea.width()/4); } } QSize AbstractClient::adjustedSize(const QSize& frame, SizeMode mode) const { // first, get the window size for the given frame size s QSize wsize = frameSizeToClientSize(frame); if (wsize.isEmpty()) wsize = QSize(qMax(wsize.width(), 1), qMax(wsize.height(), 1)); return sizeForClientSize(wsize, mode, false); } // this helper returns proper size even if the window is shaded // see also the comment in X11Client::setGeometry() QSize AbstractClient::adjustedSize() const { return sizeForClientSize(clientSize()); } } diff --git a/abstract_client.h b/abstract_client.h index 0356d7af7..e07c1b87c 100644 --- a/abstract_client.h +++ b/abstract_client.h @@ -1,1377 +1,1377 @@ /******************************************************************** KWin - the KDE window manager This file is part of the KDE project. Copyright (C) 2015 Martin Gräßlin Copyright (C) 2019 Vlad Zahorodnii This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . *********************************************************************/ #ifndef KWIN_ABSTRACT_CLIENT_H #define KWIN_ABSTRACT_CLIENT_H #include "toplevel.h" #include "options.h" #include "rules.h" #include "cursor.h" #include #include #include namespace KWayland { namespace Server { class PlasmaWindowInterface; } } namespace KDecoration2 { class Decoration; } namespace KWin { class Group; namespace TabBox { class TabBoxClientImpl; } namespace Decoration { class DecoratedClientImpl; class DecorationPalette; } class KWIN_EXPORT AbstractClient : public Toplevel { Q_OBJECT /** * Whether this Client is fullScreen. A Client might either be fullScreen due to the _NET_WM property * or through a legacy support hack. The fullScreen state can only be changed if the Client does not * use the legacy hack. To be sure whether the state changed, connect to the notify signal. */ Q_PROPERTY(bool fullScreen READ isFullScreen WRITE setFullScreen NOTIFY fullScreenChanged) /** * Whether the Client can be set to fullScreen. The property is evaluated each time it is invoked. * Because of that there is no notify signal. */ Q_PROPERTY(bool fullScreenable READ isFullScreenable) /** * Whether this Client is active or not. Use Workspace::activateClient() to activate a Client. * @see Workspace::activateClient */ Q_PROPERTY(bool active READ isActive NOTIFY activeChanged) /** * The desktop this Client is on. If the Client is on all desktops the property has value -1. * This is a legacy property, use x11DesktopIds instead */ Q_PROPERTY(int desktop READ desktop WRITE setDesktop NOTIFY desktopChanged) /** * Whether the Client is on all desktops. That is desktop is -1. */ Q_PROPERTY(bool onAllDesktops READ isOnAllDesktops WRITE setOnAllDesktops NOTIFY desktopChanged) /** * The x11 ids for all desktops this client is in. On X11 this list will always have a length of 1 */ Q_PROPERTY(QVector x11DesktopIds READ x11DesktopIds NOTIFY x11DesktopIdsChanged) /** * Indicates that the window should not be included on a taskbar. */ Q_PROPERTY(bool skipTaskbar READ skipTaskbar WRITE setSkipTaskbar NOTIFY skipTaskbarChanged) /** * Indicates that the window should not be included on a Pager. */ Q_PROPERTY(bool skipPager READ skipPager WRITE setSkipPager NOTIFY skipPagerChanged) /** * Whether the Client should be excluded from window switching effects. */ Q_PROPERTY(bool skipSwitcher READ skipSwitcher WRITE setSkipSwitcher NOTIFY skipSwitcherChanged) /** * Whether the window can be closed by the user. The value is evaluated each time the getter is called. * Because of that no changed signal is provided. */ Q_PROPERTY(bool closeable READ isCloseable) Q_PROPERTY(QIcon icon READ icon NOTIFY iconChanged) /** * Whether the Client is set to be kept above other windows. */ Q_PROPERTY(bool keepAbove READ keepAbove WRITE setKeepAbove NOTIFY keepAboveChanged) /** * Whether the Client is set to be kept below other windows. */ Q_PROPERTY(bool keepBelow READ keepBelow WRITE setKeepBelow NOTIFY keepBelowChanged) /** * Whether the Client can be shaded. The property is evaluated each time it is invoked. * Because of that there is no notify signal. */ Q_PROPERTY(bool shadeable READ isShadeable) /** * Whether the Client is shaded. */ Q_PROPERTY(bool shade READ isShade WRITE setShade NOTIFY shadeChanged) /** * Whether the Client can be minimized. The property is evaluated each time it is invoked. * Because of that there is no notify signal. */ Q_PROPERTY(bool minimizable READ isMinimizable) /** * Whether the Client is minimized. */ Q_PROPERTY(bool minimized READ isMinimized WRITE setMinimized NOTIFY minimizedChanged) /** * The optional geometry representing the minimized Client in e.g a taskbar. * See _NET_WM_ICON_GEOMETRY at https://standards.freedesktop.org/wm-spec/wm-spec-latest.html . * The value is evaluated each time the getter is called. * Because of that no changed signal is provided. */ Q_PROPERTY(QRect iconGeometry READ iconGeometry) /** * Returns whether the window is any of special windows types (desktop, dock, splash, ...), * i.e. window types that usually don't have a window frame and the user does not use window * management (moving, raising,...) on them. * The value is evaluated each time the getter is called. * Because of that no changed signal is provided. */ Q_PROPERTY(bool specialWindow READ isSpecialWindow) /** * Whether window state _NET_WM_STATE_DEMANDS_ATTENTION is set. This state indicates that some * action in or with the window happened. For example, it may be set by the Window Manager if * the window requested activation but the Window Manager refused it, or the application may set * it if it finished some work. This state may be set by both the Client and the Window Manager. * It should be unset by the Window Manager when it decides the window got the required attention * (usually, that it got activated). */ Q_PROPERTY(bool demandsAttention READ isDemandingAttention WRITE demandAttention NOTIFY demandsAttentionChanged) /** * The Caption of the Client. Read from WM_NAME property together with a suffix for hostname and shortcut. * To read only the caption as provided by WM_NAME, use the getter with an additional @c false value. */ Q_PROPERTY(QString caption READ caption NOTIFY captionChanged) /** * Minimum size as specified in WM_NORMAL_HINTS */ Q_PROPERTY(QSize minSize READ minSize) /** * Maximum size as specified in WM_NORMAL_HINTS */ Q_PROPERTY(QSize maxSize READ maxSize) /** * Whether the Client can accept keyboard focus. * The value is evaluated each time the getter is called. * Because of that no changed signal is provided. */ Q_PROPERTY(bool wantsInput READ wantsInput) /** * Whether the Client is a transient Window to another Window. * @see transientFor */ Q_PROPERTY(bool transient READ isTransient NOTIFY transientChanged) /** * The Client to which this Client is a transient if any. */ Q_PROPERTY(KWin::AbstractClient *transientFor READ transientFor NOTIFY transientChanged) /** * Whether the Client represents a modal window. */ Q_PROPERTY(bool modal READ isModal NOTIFY modalChanged) /** - * The geometry of this Client. Be aware that depending on resize mode the geometryChanged signal - * might be emitted at each resize step or only at the end of the resize operation. + * The geometry of this Client. Be aware that depending on resize mode the frameGeometryChanged + * signal might be emitted at each resize step or only at the end of the resize operation. */ Q_PROPERTY(QRect geometry READ frameGeometry WRITE setFrameGeometry) /** * Whether the Client is currently being moved by the user. * Notify signal is emitted when the Client starts or ends move/resize mode. */ Q_PROPERTY(bool move READ isMove NOTIFY moveResizedChanged) /** * Whether the Client is currently being resized by the user. * Notify signal is emitted when the Client starts or ends move/resize mode. */ Q_PROPERTY(bool resize READ isResize NOTIFY moveResizedChanged) /** * Whether the decoration is currently using an alpha channel. */ Q_PROPERTY(bool decorationHasAlpha READ decorationHasAlpha) /** * Whether the window has a decoration or not. * This property is not allowed to be set by applications themselves. * The decision whether a window has a border or not belongs to the window manager. * If this property gets abused by application developers, it will be removed again. */ Q_PROPERTY(bool noBorder READ noBorder WRITE setNoBorder) /** * Whether the Client provides context help. Mostly needed by decorations to decide whether to * show the help button or not. */ Q_PROPERTY(bool providesContextHelp READ providesContextHelp CONSTANT) /** * Whether the Client can be maximized both horizontally and vertically. * The property is evaluated each time it is invoked. * Because of that there is no notify signal. */ Q_PROPERTY(bool maximizable READ isMaximizable) /** * Whether the Client is moveable. Even if it is not moveable, it might be possible to move * it to another screen. The property is evaluated each time it is invoked. * Because of that there is no notify signal. * @see moveableAcrossScreens */ Q_PROPERTY(bool moveable READ isMovable) /** * Whether the Client can be moved to another screen. The property is evaluated each time it is invoked. * Because of that there is no notify signal. * @see moveable */ Q_PROPERTY(bool moveableAcrossScreens READ isMovableAcrossScreens) /** * Whether the Client can be resized. The property is evaluated each time it is invoked. * Because of that there is no notify signal. */ Q_PROPERTY(bool resizeable READ isResizable) /** * The desktop file name of the application this AbstractClient belongs to. * * This is either the base name without full path and without file extension of the * desktop file for the window's application (e.g. "org.kde.foo"). * * The application's desktop file name can also be the full path to the desktop file * (e.g. "/opt/kde/share/org.kde.foo.desktop") in case it's not in a standard location. */ Q_PROPERTY(QByteArray desktopFileName READ desktopFileName NOTIFY desktopFileNameChanged) /** * Whether an application menu is available for this Client */ Q_PROPERTY(bool hasApplicationMenu READ hasApplicationMenu NOTIFY hasApplicationMenuChanged) /** * Whether the application menu for this Client is currently opened */ Q_PROPERTY(bool applicationMenuActive READ applicationMenuActive NOTIFY applicationMenuActiveChanged) /** * Whether this client is unresponsive. * * When an application failed to react on a ping request in time, it is * considered unresponsive. This usually indicates that the application froze or crashed. */ Q_PROPERTY(bool unresponsive READ unresponsive NOTIFY unresponsiveChanged) /** * The color scheme set on this client * Absolute file path, or name of palette in the user's config directory following KColorSchemes format. * An empty string indicates the default palette from kdeglobals is used. * @note this indicates the colour scheme requested, which might differ from the theme applied if the colorScheme cannot be found */ Q_PROPERTY(QString colorScheme READ colorScheme NOTIFY colorSchemeChanged) public: ~AbstractClient() override; QWeakPointer tabBoxClient() const { return m_tabBoxClient.toWeakRef(); } bool isFirstInTabBox() const { return m_firstInTabBox; } bool skipSwitcher() const { return m_skipSwitcher; } void setSkipSwitcher(bool set); bool skipTaskbar() const { return m_skipTaskbar; } void setSkipTaskbar(bool set); void setOriginalSkipTaskbar(bool set); bool originalSkipTaskbar() const { return m_originalSkipTaskbar; } bool skipPager() const { return m_skipPager; } void setSkipPager(bool set); const QIcon &icon() const { return m_icon; } bool isActive() const { return m_active; } /** * Sets the client's active state to \a act. * * This function does only change the visual appearance of the client, * it does not change the focus setting. Use * Workspace::activateClient() or Workspace::requestFocus() instead. * * If a client receives or looses the focus, it calls setActive() on * its own. */ void setActive(bool); bool keepAbove() const { return m_keepAbove; } void setKeepAbove(bool); bool keepBelow() const { return m_keepBelow; } void setKeepBelow(bool); void demandAttention(bool set = true); bool isDemandingAttention() const { return m_demandsAttention; } void cancelAutoRaise(); bool wantsTabFocus() const; QMargins frameMargins() const override; QPoint clientPos() const override { return QPoint(borderLeft(), borderTop()); } virtual void updateMouseGrab(); /** * @returns The caption consisting of captionNormal and captionSuffix * @see captionNormal * @see captionSuffix */ QString caption() const; /** * @returns The caption as set by the AbstractClient without any suffix. * @see caption * @see captionSuffix */ virtual QString captionNormal() const = 0; /** * @returns The suffix added to the caption (e.g. shortcut, machine name, etc.) * @see caption * @see captionNormal */ virtual QString captionSuffix() const = 0; virtual bool isCloseable() const = 0; // TODO: remove boolean trap virtual bool isShown(bool shaded_is_shown) const = 0; virtual bool isHiddenInternal() const = 0; // TODO: remove boolean trap virtual void hideClient(bool hide) = 0; virtual bool isFullScreenable() const = 0; virtual bool isFullScreen() const = 0; // TODO: remove boolean trap virtual AbstractClient *findModal(bool allow_itself = false) = 0; virtual bool isTransient() const; /** * @returns Whether there is a hint available to place the AbstractClient on it's parent, default @c false. * @see transientPlacementHint */ virtual bool hasTransientPlacementHint() const; /** * Only valid id hasTransientPlacementHint is true * @returns The position the transient wishes to position itself */ virtual QRect transientPlacement(const QRect &bounds) const; const AbstractClient* transientFor() const; AbstractClient* transientFor(); /** * @returns @c true if c is the transient_for window for this client, * or recursively the transient_for window * @todo: remove boolean trap */ virtual bool hasTransient(const AbstractClient* c, bool indirect) const; const QList& transients() const; // Is not indirect virtual void removeTransient(AbstractClient* cl); virtual QList mainClients() const; // Call once before loop , is not indirect QList allMainClients() const; // Call once before loop , is indirect /** * Returns true for "special" windows and false for windows which are "normal" * (normal=window which has a border, can be moved by the user, can be closed, etc.) * true for Desktop, Dock, Splash, Override and TopMenu (and Toolbar??? - for now) * false for Normal, Dialog, Utility and Menu (and Toolbar??? - not yet) TODO */ bool isSpecialWindow() const; void sendToScreen(int screen); const QKeySequence &shortcut() const { return _shortcut; } void setShortcut(const QString &cut); virtual bool performMouseCommand(Options::MouseCommand, const QPoint &globalPos); void setOnAllDesktops(bool set); void setDesktop(int); void enterDesktop(VirtualDesktop *desktop); void leaveDesktop(VirtualDesktop *desktop); /** * Set the window as being on the attached list of desktops * On X11 it will be set to the last entry */ void setDesktops(QVector desktops); int desktop() const override { return m_desktops.isEmpty() ? (int)NET::OnAllDesktops : m_desktops.last()->x11DesktopNumber(); } QVector desktops() const override { return m_desktops; } QVector x11DesktopIds() const; void setMinimized(bool set); /** * Minimizes this client plus its transients */ void minimize(bool avoid_animation = false); void unminimize(bool avoid_animation = false); bool isMinimized() const { return m_minimized; } virtual void setFullScreen(bool set, bool user = true) = 0; virtual void setClientShown(bool shown); virtual QRect geometryRestore() const = 0; /** * The currently applied maximize mode */ virtual MaximizeMode maximizeMode() const = 0; /** * The maximise mode requested by the server. * For X this always matches maximizeMode, for wayland clients it * is asynchronous */ virtual MaximizeMode requestedMaximizeMode() const; void maximize(MaximizeMode); /** * Sets the maximization according to @p vertically and @p horizontally. */ Q_INVOKABLE void setMaximize(bool vertically, bool horizontally); virtual bool noBorder() const = 0; virtual void setNoBorder(bool set) = 0; virtual void blockActivityUpdates(bool b = true) = 0; QPalette palette() const; const Decoration::DecorationPalette *decorationPalette() const; /** * Returns whether the window is resizable or has a fixed size. */ virtual bool isResizable() const = 0; /** * Returns whether the window is moveable or has a fixed position. */ virtual bool isMovable() const = 0; /** * Returns whether the window can be moved to another screen. */ virtual bool isMovableAcrossScreens() const = 0; /** * @c true only for @c ShadeNormal */ bool isShade() const { return shadeMode() == ShadeNormal; } /** * Default implementation returns @c ShadeNone */ virtual ShadeMode shadeMode() const; // Prefer isShade() void setShade(bool set); /** * Default implementation does nothing */ virtual void setShade(ShadeMode mode); /** * Whether the Client can be shaded. Default implementation returns @c false. */ virtual bool isShadeable() const; /** * Returns whether the window is maximizable or not. */ virtual bool isMaximizable() const = 0; virtual bool isMinimizable() const = 0; virtual QRect iconGeometry() const; virtual bool userCanSetFullScreen() const = 0; virtual bool userCanSetNoBorder() const = 0; virtual void checkNoBorder(); virtual void setOnActivities(QStringList newActivitiesList); virtual void setOnAllActivities(bool set) = 0; const WindowRules* rules() const { return &m_rules; } void removeRule(Rules* r); void setupWindowRules(bool ignore_temporary); void evaluateWindowRules(); virtual void applyWindowRules(); virtual void takeFocus() = 0; virtual bool wantsInput() const = 0; /** * Whether a dock window wants input. * * By default KWin doesn't pass focus to a dock window unless a force activate * request is provided. * * This method allows to have dock windows take focus also through flags set on * the window. * * The default implementation returns @c false. */ virtual bool dockWantsInput() const; void checkWorkspacePosition(QRect oldGeometry = QRect(), int oldDesktop = -2, QRect oldClientGeometry = QRect()); virtual xcb_timestamp_t userTime() const; virtual void updateWindowRules(Rules::Types selection); void growHorizontal(); void shrinkHorizontal(); void growVertical(); void shrinkVertical(); void updateMoveResize(const QPointF ¤tGlobalCursor); /** * Ends move resize when all pointer buttons are up again. */ void endMoveResize(); void keyPressEvent(uint key_code); void enterEvent(const QPoint &globalPos); void leaveEvent(); /** * These values represent positions inside an area */ enum Position { // without prefix, they'd conflict with Qt::TopLeftCorner etc. :( PositionCenter = 0x00, PositionLeft = 0x01, PositionRight = 0x02, PositionTop = 0x04, PositionBottom = 0x08, PositionTopLeft = PositionLeft | PositionTop, PositionTopRight = PositionRight | PositionTop, PositionBottomLeft = PositionLeft | PositionBottom, PositionBottomRight = PositionRight | PositionBottom }; Position titlebarPosition() const; bool titlebarPositionUnderMouse() const; // a helper for the workspace window packing. tests for screen validity and updates since in maximization case as with normal moving void packTo(int left, int top); /** * Sets the quick tile mode ("snap") of this window. * This will also handle preserving and restoring of window geometry as necessary. * @param mode The tile mode (left/right) to give this window. * @param keyboard Defines whether to take keyboard cursor into account. */ void setQuickTileMode(QuickTileMode mode, bool keyboard = false); QuickTileMode quickTileMode() const { return QuickTileMode(m_quickTileMode); } Layer layer() const override; void updateLayer(); enum ForceGeometry_t { NormalGeometrySet, ForceGeometrySet }; virtual void move(int x, int y, ForceGeometry_t force = NormalGeometrySet); void move(const QPoint &p, ForceGeometry_t force = NormalGeometrySet); virtual void resizeWithChecks(int w, int h, ForceGeometry_t force = NormalGeometrySet) = 0; void resizeWithChecks(const QSize& s, ForceGeometry_t force = NormalGeometrySet); void keepInArea(QRect area, bool partial = false); virtual QSize minSize() const; virtual QSize maxSize() const; virtual void setFrameGeometry(int x, int y, int w, int h, ForceGeometry_t force = NormalGeometrySet) = 0; void setFrameGeometry(const QRect &rect, ForceGeometry_t force = NormalGeometrySet); /** * How to resize the window in order to obey constraints (mainly aspect ratios). */ enum SizeMode { SizeModeAny, SizeModeFixedW, ///< Try not to affect width SizeModeFixedH, ///< Try not to affect height SizeModeMax ///< Try not to make it larger in either direction }; /** * Calculates the appropriate frame size for the given client size @p wsize. * * @p wsize is adapted according to the window's size hints (minimum, maximum and incremental size changes). * * Default implementation returns the passed in @p wsize. */ virtual QSize sizeForClientSize(const QSize &wsize, SizeMode mode = SizeModeAny, bool noframe = false) const; /** * Adjust the frame size @p frame according to the window's size hints. */ QSize adjustedSize(const QSize&, SizeMode mode = SizeModeAny) const; QSize adjustedSize() const; /** * Calculates the matching client position for the given frame position @p point. */ virtual QPoint framePosToClientPos(const QPoint &point) const; /** * Calculates the matching frame position for the given client position @p point. */ virtual QPoint clientPosToFramePos(const QPoint &point) const; /** * Calculates the matching client size for the given frame size @p size. * * Notice that size constraints won't be applied. * * Default implementation returns the frame size with frame margins being excluded. */ virtual QSize frameSizeToClientSize(const QSize &size) const; /** * Calculates the matching frame size for the given client size @p size. * * Notice that size constraints won't be applied. * * Default implementation returns the client size with frame margins being included. */ virtual QSize clientSizeToFrameSize(const QSize &size) const; /** * Calculates the matching client rect for the given frame rect @p rect. * * Notice that size constraints won't be applied. */ QRect frameRectToClientRect(const QRect &rect) const; /** * Calculates the matching frame rect for the given client rect @p rect. * * Notice that size constraints won't be applied. */ QRect clientRectToFrameRect(const QRect &rect) const; /** * Returns @c true if the Client is being interactively moved; otherwise @c false. */ bool isMove() const { return isMoveResize() && moveResizePointerMode() == PositionCenter; } /** * Returns @c true if the Client is being interactively resized; otherwise @c false. */ bool isResize() const { return isMoveResize() && moveResizePointerMode() != PositionCenter; } /** * Cursor shape for move/resize mode. */ CursorShape cursor() const { return m_moveResize.cursor; } virtual bool hasStrut() const; void setModal(bool modal); bool isModal() const; /** * Determines the mouse command for the given @p button in the current state. * * The @p handled argument specifies whether the button was handled or not. * This value should be used to determine whether the mouse button should be * passed to the AbstractClient or being filtered out. */ Options::MouseCommand getMouseCommand(Qt::MouseButton button, bool *handled) const; Options::MouseCommand getWheelCommand(Qt::Orientation orientation, bool *handled) const; // decoration related KDecoration2::Decoration *decoration() { return m_decoration.decoration; } const KDecoration2::Decoration *decoration() const { return m_decoration.decoration; } bool isDecorated() const { return m_decoration.decoration != nullptr; } QPointer decoratedClient() const; void setDecoratedClient(QPointer client); bool decorationHasAlpha() const; void triggerDecorationRepaint(); virtual void layoutDecorationRects(QRect &left, QRect &top, QRect &right, QRect &bottom) const; void processDecorationMove(const QPoint &localPos, const QPoint &globalPos); bool processDecorationButtonPress(QMouseEvent *event, bool ignoreMenu = false); void processDecorationButtonRelease(QMouseEvent *event); /** * TODO: fix boolean traps */ virtual void updateDecoration(bool check_workspace_pos, bool force = false) = 0; /** * Returns whether the window provides context help or not. If it does, * you should show a help menu item or a help button like '?' and call * contextHelp() if this is invoked. * * Default implementation returns @c false. * @see showContextHelp; */ virtual bool providesContextHelp() const; /** * Invokes context help on the window. Only works if the window * actually provides context help. * * Default implementation does nothing. * * @see providesContextHelp() */ virtual void showContextHelp(); QRect inputGeometry() const override; /** * @returns the geometry of the virtual keyboard * This geometry is in global coordinates */ QRect virtualKeyboardGeometry() const; /** * Sets the geometry of the virtual keyboard, The window may resize itself in order to make space for the keybaord * This geometry is in global coordinates */ void setVirtualKeyboardGeometry(const QRect &geo); /** * Restores the AbstractClient after it had been hidden due to show on screen edge functionality. * The AbstractClient also gets raised (e.g. Panel mode windows can cover) and the AbstractClient * gets informed in a window specific way that it is shown and raised again. */ virtual void showOnScreenEdge() = 0; QByteArray desktopFileName() const { return m_desktopFileName; } /** * Tries to terminate the process of this AbstractClient. * * Implementing subclasses can perform a windowing system solution for terminating. */ virtual void killWindow() = 0; enum class SameApplicationCheck { RelaxedForActive = 1 << 0, AllowCrossProcesses = 1 << 1 }; Q_DECLARE_FLAGS(SameApplicationChecks, SameApplicationCheck) static bool belongToSameApplication(const AbstractClient* c1, const AbstractClient* c2, SameApplicationChecks checks = SameApplicationChecks()); bool hasApplicationMenu() const; bool applicationMenuActive() const { return m_applicationMenuActive; } void setApplicationMenuActive(bool applicationMenuActive); QString applicationMenuServiceName() const { return m_applicationMenuServiceName; } QString applicationMenuObjectPath() const { return m_applicationMenuObjectPath; } QString colorScheme() const { return m_colorScheme; } /** * Request showing the application menu bar * @param actionId The DBus menu ID of the action that should be highlighted, 0 for the root menu */ void showApplicationMenu(int actionId); bool unresponsive() const; virtual bool isInitialPositionSet() const { return false; } /** * Default implementation returns @c null. * Mostly intended for X11 clients, from EWMH: * @verbatim * If the WM_TRANSIENT_FOR property is set to None or Root window, the window should be * treated as a transient for all other windows in the same group. It has been noted that this * is a slight ICCCM violation, but as this behavior is pretty standard for many toolkits and * window managers, and is extremely unlikely to break anything, it seems reasonable to document * it as standard. * @endverbatim */ virtual bool groupTransient() const; /** * Default implementation returns @c null. * * Mostly for X11 clients, holds the client group */ virtual const Group *group() const; /** * Default implementation returns @c null. * * Mostly for X11 clients, holds the client group */ virtual Group *group(); /** * Returns whether this is an internal client. * * Internal clients are created by KWin and used for special purpose windows, * like the task switcher, etc. * * Default implementation returns @c false. */ virtual bool isInternal() const; /** * Returns whether window rules can be applied to this client. * * Default implementation returns @c true. */ virtual bool supportsWindowRules() const; public Q_SLOTS: virtual void closeWindow() = 0; Q_SIGNALS: void fullScreenChanged(); void skipTaskbarChanged(); void skipPagerChanged(); void skipSwitcherChanged(); void iconChanged(); void activeChanged(); void keepAboveChanged(bool); void keepBelowChanged(bool); /** * Emitted whenever the demands attention state changes. */ void demandsAttentionChanged(); void desktopPresenceChanged(KWin::AbstractClient*, int); // to be forwarded by Workspace void desktopChanged(); void x11DesktopIdsChanged(); void shadeChanged(); void minimizedChanged(); void clientMinimized(KWin::AbstractClient* client, bool animate); void clientUnminimized(KWin::AbstractClient* client, bool animate); void paletteChanged(const QPalette &p); void colorSchemeChanged(); void captionChanged(); void clientMaximizedStateChanged(KWin::AbstractClient*, MaximizeMode); void clientMaximizedStateChanged(KWin::AbstractClient* c, bool h, bool v); void transientChanged(); void modalChanged(); void quickTileModeChanged(); void moveResizedChanged(); void moveResizeCursorChanged(CursorShape); void clientStartUserMovedResized(KWin::AbstractClient*); void clientStepUserMovedResized(KWin::AbstractClient *, const QRect&); void clientFinishUserMovedResized(KWin::AbstractClient*); void closeableChanged(bool); void minimizeableChanged(bool); void shadeableChanged(bool); void maximizeableChanged(bool); void desktopFileNameChanged(); void hasApplicationMenuChanged(bool); void applicationMenuActiveChanged(bool); void unresponsiveChanged(bool); protected: AbstractClient(); void setFirstInTabBox(bool enable) { m_firstInTabBox = enable; } void setIcon(const QIcon &icon); void startAutoRaise(); void autoRaise(); /** * Whether the window accepts focus. * The difference to wantsInput is that the implementation should not check rules and return * what the window effectively supports. */ virtual bool acceptsFocus() const = 0; /** * Called from setActive once the active value got updated, but before the changed signal * is emitted. * * Default implementation does nothing. */ virtual void doSetActive(); /** * Called from setKeepAbove once the keepBelow value got updated, but before the changed signal * is emitted. * * Default implementation does nothing. */ virtual void doSetKeepAbove(); /** * Called from setKeepBelow once the keepBelow value got updated, but before the changed signal * is emitted. * * Default implementation does nothing. */ virtual void doSetKeepBelow(); /** * Called from setDeskop once the desktop value got updated, but before the changed signal * is emitted. * * Default implementation does nothing. * @param desktop The new desktop the Client is on * @param was_desk The desktop the Client was on before */ virtual void doSetDesktop(int desktop, int was_desk); /** * Called from @ref minimize and @ref unminimize once the minimized value got updated, but before the * changed signal is emitted. * * Default implementation does nothig. */ virtual void doMinimize(); virtual bool belongsToSameApplication(const AbstractClient *other, SameApplicationChecks checks) const = 0; virtual void doSetSkipTaskbar(); virtual void doSetSkipPager(); virtual void doSetSkipSwitcher(); void setupWindowManagementInterface(); void destroyWindowManagementInterface(); void updateColorScheme(QString path); virtual void updateColorScheme() = 0; void setTransientFor(AbstractClient *transientFor); virtual void addTransient(AbstractClient* cl); /** * Just removes the @p cl from the transients without any further checks. */ void removeTransientFromList(AbstractClient* cl); Layer belongsToLayer() const; virtual bool belongsToDesktop() const; void invalidateLayer(); bool isActiveFullScreen() const; virtual Layer layerForDock() const; // electric border / quick tiling void setElectricBorderMode(QuickTileMode mode); QuickTileMode electricBorderMode() const { return m_electricMode; } void setElectricBorderMaximizing(bool maximizing); bool isElectricBorderMaximizing() const { return m_electricMaximizing; } QRect electricBorderMaximizeGeometry(QPoint pos, int desktop); void updateQuickTileMode(QuickTileMode newMode) { m_quickTileMode = newMode; } KWayland::Server::PlasmaWindowInterface *windowManagementInterface() const { return m_windowManagementInterface; } // geometry handling void checkOffscreenPosition(QRect *geom, const QRect &screenArea); int borderLeft() const; int borderRight() const; int borderTop() const; int borderBottom() const; virtual void changeMaximize(bool horizontal, bool vertical, bool adjust) = 0; virtual void setGeometryRestore(const QRect &geo) = 0; /** * Called from move after updating the geometry. Can be reimplemented to perform specific tasks. * The base implementation does nothing. */ virtual void doMove(int x, int y); void blockGeometryUpdates(bool block); void blockGeometryUpdates(); void unblockGeometryUpdates(); bool areGeometryUpdatesBlocked() const; enum PendingGeometry_t { PendingGeometryNone, PendingGeometryNormal, PendingGeometryForced }; PendingGeometry_t pendingGeometryUpdate() const; void setPendingGeometryUpdate(PendingGeometry_t update); QRect bufferGeometryBeforeUpdateBlocking() const; QRect frameGeometryBeforeUpdateBlocking() const; void updateGeometryBeforeUpdateBlocking(); /** * Schedules a repaint for the visibleRect before and after a * geometry update. The current visibleRect is stored for the * next time this method is called as the before geometry. */ void addRepaintDuringGeometryUpdates(); /** * @returns whether the Client is currently in move resize mode */ bool isMoveResize() const { return m_moveResize.enabled; } /** * Sets whether the Client is in move resize mode to @p enabled. */ void setMoveResize(bool enabled) { m_moveResize.enabled = enabled; } /** * @returns whether the move resize mode is unrestricted. */ bool isUnrestrictedMoveResize() const { return m_moveResize.unrestricted; } /** * Sets whether move resize mode is unrestricted to @p set. */ void setUnrestrictedMoveResize(bool set) { m_moveResize.unrestricted = set; } QPoint moveOffset() const { return m_moveResize.offset; } void setMoveOffset(const QPoint &offset) { m_moveResize.offset = offset; } QPoint invertedMoveOffset() const { return m_moveResize.invertedOffset; } void setInvertedMoveOffset(const QPoint &offset) { m_moveResize.invertedOffset = offset; } QRect initialMoveResizeGeometry() const { return m_moveResize.initialGeometry; } /** * Sets the initial move resize geometry to the current geometry. */ void updateInitialMoveResizeGeometry(); QRect moveResizeGeometry() const { return m_moveResize.geometry; } void setMoveResizeGeometry(const QRect &geo) { m_moveResize.geometry = geo; } Position moveResizePointerMode() const { return m_moveResize.pointer; } void setMoveResizePointerMode(Position mode) { m_moveResize.pointer = mode; } bool isMoveResizePointerButtonDown() const { return m_moveResize.buttonDown; } void setMoveResizePointerButtonDown(bool down) { m_moveResize.buttonDown = down; } int moveResizeStartScreen() const { return m_moveResize.startScreen; } void checkUnrestrictedMoveResize(); /** * Sets an appropriate cursor shape for the logical mouse position. */ void updateCursor(); void startDelayedMoveResize(); void stopDelayedMoveResize(); bool startMoveResize(); /** * Called from startMoveResize. * * Implementing classes should return @c false if starting move resize should * get aborted. In that case startMoveResize will also return @c false. * * Base implementation returns @c true. */ virtual bool doStartMoveResize(); void finishMoveResize(bool cancel); /** * Leaves the move resize mode. * * Inheriting classes must invoke the base implementation which * ensures that the internal mode is properly ended. */ virtual void leaveMoveResize(); virtual void positionGeometryTip(); void performMoveResize(); /** * Called from performMoveResize() after actually performing the change of geometry. * Implementing subclasses can perform windowing system specific handling here. * * Default implementation does nothing. */ virtual void doPerformMoveResize(); /* * Checks if the mouse cursor is near the edge of the screen and if so * activates quick tiling or maximization */ void checkQuickTilingMaximizationZones(int xroot, int yroot); /** * Whether a sync request is still pending. * Default implementation returns @c false. */ virtual bool isWaitingForMoveResizeSync() const; /** * Called during handling a resize. Implementing subclasses can use this * method to perform windowing system specific syncing. * * Default implementation does nothing. */ virtual void doResizeSync(); void handleMoveResize(int x, int y, int x_root, int y_root); void handleMoveResize(const QPoint &local, const QPoint &global); void dontMoveResize(); virtual QSize resizeIncrements() const; /** * Returns the position depending on the Decoration's section under mouse. * If no decoration it returns PositionCenter. */ Position mousePosition() const; static bool haveResizeEffect() { return s_haveResizeEffect; } static void updateHaveResizeEffect(); static void resetHaveResizeEffect() { s_haveResizeEffect = false; } void setDecoration(KDecoration2::Decoration *decoration) { m_decoration.decoration = decoration; } virtual void destroyDecoration(); void startDecorationDoubleClickTimer(); void invalidateDecorationDoubleClickTimer(); void setDesktopFileName(QByteArray name); QString iconFromDesktopFile() const; void updateApplicationMenuServiceName(const QString &serviceName); void updateApplicationMenuObjectPath(const QString &objectPath); void setUnresponsive(bool unresponsive); virtual void setShortcutInternal(); QString shortcutCaptionSuffix() const; virtual void updateCaption() = 0; /** * Looks for another AbstractClient with same captionNormal and captionSuffix. * If no such AbstractClient exists @c nullptr is returned. */ AbstractClient *findClientWithSameCaption() const; void finishWindowRules(); void discardTemporaryRules(); bool tabTo(AbstractClient *other, bool behind, bool activate); private: void handlePaletteChange(); QSharedPointer m_tabBoxClient; bool m_firstInTabBox = false; bool m_skipTaskbar = false; /** * Unaffected by KWin */ bool m_originalSkipTaskbar = false; bool m_skipPager = false; bool m_skipSwitcher = false; QIcon m_icon; bool m_active = false; bool m_keepAbove = false; bool m_keepBelow = false; bool m_demandsAttention = false; bool m_minimized = false; QTimer *m_autoRaiseTimer = nullptr; QVector m_desktops; QString m_colorScheme; std::shared_ptr m_palette; static QHash> s_palettes; static std::shared_ptr s_defaultPalette; KWayland::Server::PlasmaWindowInterface *m_windowManagementInterface = nullptr; AbstractClient *m_transientFor = nullptr; QList m_transients; bool m_modal = false; Layer m_layer = UnknownLayer; // electric border/quick tiling QuickTileMode m_electricMode = QuickTileFlag::None; bool m_electricMaximizing = false; // The quick tile mode of this window. int m_quickTileMode = int(QuickTileFlag::None); QTimer *m_electricMaximizingDelay = nullptr; // geometry int m_blockGeometryUpdates = 0; // > 0 = New geometry is remembered, but not actually set PendingGeometry_t m_pendingGeometryUpdate = PendingGeometryNone; friend class GeometryUpdatesBlocker; QRect m_visibleRectBeforeGeometryUpdate; QRect m_bufferGeometryBeforeUpdateBlocking; QRect m_frameGeometryBeforeUpdateBlocking; QRect m_virtualKeyboardGeometry; QRect m_keyboardGeometryRestore; struct { bool enabled = false; bool unrestricted = false; QPoint offset; QPoint invertedOffset; QRect initialGeometry; QRect geometry; Position pointer = PositionCenter; bool buttonDown = false; CursorShape cursor = Qt::ArrowCursor; int startScreen = 0; QTimer *delayedTimer = nullptr; } m_moveResize; struct { KDecoration2::Decoration *decoration = nullptr; QPointer client; QElapsedTimer doubleClickTimer; } m_decoration; QByteArray m_desktopFileName; bool m_applicationMenuActive = false; QString m_applicationMenuServiceName; QString m_applicationMenuObjectPath; bool m_unresponsive = false; QKeySequence _shortcut; WindowRules m_rules; static bool s_haveResizeEffect; }; /** * Helper for AbstractClient::blockGeometryUpdates() being called in pairs (true/false) */ class GeometryUpdatesBlocker { public: explicit GeometryUpdatesBlocker(AbstractClient* c) : cl(c) { cl->blockGeometryUpdates(true); } ~GeometryUpdatesBlocker() { cl->blockGeometryUpdates(false); } private: AbstractClient* cl; }; inline void AbstractClient::move(const QPoint& p, ForceGeometry_t force) { move(p.x(), p.y(), force); } inline void AbstractClient::resizeWithChecks(const QSize& s, AbstractClient::ForceGeometry_t force) { resizeWithChecks(s.width(), s.height(), force); } inline void AbstractClient::setFrameGeometry(const QRect &rect, ForceGeometry_t force) { setFrameGeometry(rect.x(), rect.y(), rect.width(), rect.height(), force); } inline const QList& AbstractClient::transients() const { return m_transients; } inline bool AbstractClient::areGeometryUpdatesBlocked() const { return m_blockGeometryUpdates != 0; } inline void AbstractClient::blockGeometryUpdates() { m_blockGeometryUpdates++; } inline void AbstractClient::unblockGeometryUpdates() { m_blockGeometryUpdates--; } inline AbstractClient::PendingGeometry_t AbstractClient::pendingGeometryUpdate() const { return m_pendingGeometryUpdate; } inline void AbstractClient::setPendingGeometryUpdate(PendingGeometry_t update) { m_pendingGeometryUpdate = update; } } Q_DECLARE_METATYPE(KWin::AbstractClient*) Q_DECLARE_METATYPE(QList) Q_DECLARE_OPERATORS_FOR_FLAGS(KWin::AbstractClient::SameApplicationChecks) #endif diff --git a/autotests/integration/activation_test.cpp b/autotests/integration/activation_test.cpp index e0e74c238..fc9314036 100644 --- a/autotests/integration/activation_test.cpp +++ b/autotests/integration/activation_test.cpp @@ -1,585 +1,585 @@ /******************************************************************** KWin - the KDE window manager This file is part of the KDE project. Copyright (C) 2019 Vlad Zahorodnii This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . *********************************************************************/ #include "kwin_wayland_test.h" #include "cursor.h" #include "platform.h" #include "screens.h" #include "wayland_server.h" #include "workspace.h" #include "xdgshellclient.h" #include namespace KWin { static const QString s_socketName = QStringLiteral("wayland_test_activation-0"); class ActivationTest : public QObject { Q_OBJECT private Q_SLOTS: void initTestCase(); void init(); void cleanup(); void testSwitchToWindowToLeft(); void testSwitchToWindowToRight(); void testSwitchToWindowAbove(); void testSwitchToWindowBelow(); void testSwitchToWindowMaximized(); void testSwitchToWindowFullScreen(); private: void stackScreensHorizontally(); void stackScreensVertically(); }; void ActivationTest::initTestCase() { qRegisterMetaType(); qRegisterMetaType(); QSignalSpy workspaceCreatedSpy(kwinApp(), &Application::workspaceCreated); QVERIFY(workspaceCreatedSpy.isValid()); kwinApp()->platform()->setInitialWindowSize(QSize(1280, 1024)); QVERIFY(waylandServer()->init(s_socketName.toLocal8Bit())); QMetaObject::invokeMethod(kwinApp()->platform(), "setVirtualOutputs", Qt::DirectConnection, Q_ARG(int, 2)); 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 ActivationTest::init() { QVERIFY(Test::setupWaylandConnection()); screens()->setCurrent(0); Cursor::setPos(QPoint(640, 512)); } void ActivationTest::cleanup() { Test::destroyWaylandConnection(); stackScreensHorizontally(); } void ActivationTest::testSwitchToWindowToLeft() { // This test verifies that "Switch to Window to the Left" shortcut works. using namespace KWayland::Client; // Prepare the test environment. stackScreensHorizontally(); // Create several clients on the left screen. QScopedPointer surface1(Test::createSurface()); QScopedPointer shellSurface1(Test::createXdgShellStableSurface(surface1.data())); XdgShellClient *client1 = Test::renderAndWaitForShown(surface1.data(), QSize(100, 50), Qt::blue); QVERIFY(client1); QVERIFY(client1->isActive()); QScopedPointer surface2(Test::createSurface()); QScopedPointer shellSurface2(Test::createXdgShellStableSurface(surface2.data())); XdgShellClient *client2 = Test::renderAndWaitForShown(surface2.data(), QSize(100, 50), Qt::blue); QVERIFY(client2); QVERIFY(client2->isActive()); client1->move(QPoint(300, 200)); client2->move(QPoint(500, 200)); // Create several clients on the right screen. QScopedPointer surface3(Test::createSurface()); QScopedPointer shellSurface3(Test::createXdgShellStableSurface(surface3.data())); XdgShellClient *client3 = Test::renderAndWaitForShown(surface3.data(), QSize(100, 50), Qt::blue); QVERIFY(client3); QVERIFY(client3->isActive()); QScopedPointer surface4(Test::createSurface()); QScopedPointer shellSurface4(Test::createXdgShellStableSurface(surface4.data())); XdgShellClient *client4 = Test::renderAndWaitForShown(surface4.data(), QSize(100, 50), Qt::blue); QVERIFY(client4); QVERIFY(client4->isActive()); client3->move(QPoint(1380, 200)); client4->move(QPoint(1580, 200)); // Switch to window to the left. workspace()->switchWindow(Workspace::DirectionWest); QVERIFY(client3->isActive()); // Switch to window to the left. workspace()->switchWindow(Workspace::DirectionWest); QVERIFY(client2->isActive()); // Switch to window to the left. workspace()->switchWindow(Workspace::DirectionWest); QVERIFY(client1->isActive()); // Switch to window to the left. workspace()->switchWindow(Workspace::DirectionWest); QVERIFY(client4->isActive()); // Destroy all clients. shellSurface1.reset(); QVERIFY(Test::waitForWindowDestroyed(client1)); shellSurface2.reset(); QVERIFY(Test::waitForWindowDestroyed(client2)); shellSurface3.reset(); QVERIFY(Test::waitForWindowDestroyed(client3)); shellSurface4.reset(); QVERIFY(Test::waitForWindowDestroyed(client4)); } void ActivationTest::testSwitchToWindowToRight() { // This test verifies that "Switch to Window to the Right" shortcut works. using namespace KWayland::Client; // Prepare the test environment. stackScreensHorizontally(); // Create several clients on the left screen. QScopedPointer surface1(Test::createSurface()); QScopedPointer shellSurface1(Test::createXdgShellStableSurface(surface1.data())); XdgShellClient *client1 = Test::renderAndWaitForShown(surface1.data(), QSize(100, 50), Qt::blue); QVERIFY(client1); QVERIFY(client1->isActive()); QScopedPointer surface2(Test::createSurface()); QScopedPointer shellSurface2(Test::createXdgShellStableSurface(surface2.data())); XdgShellClient *client2 = Test::renderAndWaitForShown(surface2.data(), QSize(100, 50), Qt::blue); QVERIFY(client2); QVERIFY(client2->isActive()); client1->move(QPoint(300, 200)); client2->move(QPoint(500, 200)); // Create several clients on the right screen. QScopedPointer surface3(Test::createSurface()); QScopedPointer shellSurface3(Test::createXdgShellStableSurface(surface3.data())); XdgShellClient *client3 = Test::renderAndWaitForShown(surface3.data(), QSize(100, 50), Qt::blue); QVERIFY(client3); QVERIFY(client3->isActive()); QScopedPointer surface4(Test::createSurface()); QScopedPointer shellSurface4(Test::createXdgShellStableSurface(surface4.data())); XdgShellClient *client4 = Test::renderAndWaitForShown(surface4.data(), QSize(100, 50), Qt::blue); QVERIFY(client4); QVERIFY(client4->isActive()); client3->move(QPoint(1380, 200)); client4->move(QPoint(1580, 200)); // Switch to window to the right. workspace()->switchWindow(Workspace::DirectionEast); QVERIFY(client1->isActive()); // Switch to window to the right. workspace()->switchWindow(Workspace::DirectionEast); QVERIFY(client2->isActive()); // Switch to window to the right. workspace()->switchWindow(Workspace::DirectionEast); QVERIFY(client3->isActive()); // Switch to window to the right. workspace()->switchWindow(Workspace::DirectionEast); QVERIFY(client4->isActive()); // Destroy all clients. shellSurface1.reset(); QVERIFY(Test::waitForWindowDestroyed(client1)); shellSurface2.reset(); QVERIFY(Test::waitForWindowDestroyed(client2)); shellSurface3.reset(); QVERIFY(Test::waitForWindowDestroyed(client3)); shellSurface4.reset(); QVERIFY(Test::waitForWindowDestroyed(client4)); } void ActivationTest::testSwitchToWindowAbove() { // This test verifies that "Switch to Window Above" shortcut works. using namespace KWayland::Client; // Prepare the test environment. stackScreensVertically(); // Create several clients on the top screen. QScopedPointer surface1(Test::createSurface()); QScopedPointer shellSurface1(Test::createXdgShellStableSurface(surface1.data())); XdgShellClient *client1 = Test::renderAndWaitForShown(surface1.data(), QSize(100, 50), Qt::blue); QVERIFY(client1); QVERIFY(client1->isActive()); QScopedPointer surface2(Test::createSurface()); QScopedPointer shellSurface2(Test::createXdgShellStableSurface(surface2.data())); XdgShellClient *client2 = Test::renderAndWaitForShown(surface2.data(), QSize(100, 50), Qt::blue); QVERIFY(client2); QVERIFY(client2->isActive()); client1->move(QPoint(200, 300)); client2->move(QPoint(200, 500)); // Create several clients on the bottom screen. QScopedPointer surface3(Test::createSurface()); QScopedPointer shellSurface3(Test::createXdgShellStableSurface(surface3.data())); XdgShellClient *client3 = Test::renderAndWaitForShown(surface3.data(), QSize(100, 50), Qt::blue); QVERIFY(client3); QVERIFY(client3->isActive()); QScopedPointer surface4(Test::createSurface()); QScopedPointer shellSurface4(Test::createXdgShellStableSurface(surface4.data())); XdgShellClient *client4 = Test::renderAndWaitForShown(surface4.data(), QSize(100, 50), Qt::blue); QVERIFY(client4); QVERIFY(client4->isActive()); client3->move(QPoint(200, 1224)); client4->move(QPoint(200, 1424)); // Switch to window above. workspace()->switchWindow(Workspace::DirectionNorth); QVERIFY(client3->isActive()); // Switch to window above. workspace()->switchWindow(Workspace::DirectionNorth); QVERIFY(client2->isActive()); // Switch to window above. workspace()->switchWindow(Workspace::DirectionNorth); QVERIFY(client1->isActive()); // Switch to window above. workspace()->switchWindow(Workspace::DirectionNorth); QVERIFY(client4->isActive()); // Destroy all clients. shellSurface1.reset(); QVERIFY(Test::waitForWindowDestroyed(client1)); shellSurface2.reset(); QVERIFY(Test::waitForWindowDestroyed(client2)); shellSurface3.reset(); QVERIFY(Test::waitForWindowDestroyed(client3)); shellSurface4.reset(); QVERIFY(Test::waitForWindowDestroyed(client4)); } void ActivationTest::testSwitchToWindowBelow() { // This test verifies that "Switch to Window Bottom" shortcut works. using namespace KWayland::Client; // Prepare the test environment. stackScreensVertically(); // Create several clients on the top screen. QScopedPointer surface1(Test::createSurface()); QScopedPointer shellSurface1(Test::createXdgShellStableSurface(surface1.data())); XdgShellClient *client1 = Test::renderAndWaitForShown(surface1.data(), QSize(100, 50), Qt::blue); QVERIFY(client1); QVERIFY(client1->isActive()); QScopedPointer surface2(Test::createSurface()); QScopedPointer shellSurface2(Test::createXdgShellStableSurface(surface2.data())); XdgShellClient *client2 = Test::renderAndWaitForShown(surface2.data(), QSize(100, 50), Qt::blue); QVERIFY(client2); QVERIFY(client2->isActive()); client1->move(QPoint(200, 300)); client2->move(QPoint(200, 500)); // Create several clients on the bottom screen. QScopedPointer surface3(Test::createSurface()); QScopedPointer shellSurface3(Test::createXdgShellStableSurface(surface3.data())); XdgShellClient *client3 = Test::renderAndWaitForShown(surface3.data(), QSize(100, 50), Qt::blue); QVERIFY(client3); QVERIFY(client3->isActive()); QScopedPointer surface4(Test::createSurface()); QScopedPointer shellSurface4(Test::createXdgShellStableSurface(surface4.data())); XdgShellClient *client4 = Test::renderAndWaitForShown(surface4.data(), QSize(100, 50), Qt::blue); QVERIFY(client4); QVERIFY(client4->isActive()); client3->move(QPoint(200, 1224)); client4->move(QPoint(200, 1424)); // Switch to window below. workspace()->switchWindow(Workspace::DirectionSouth); QVERIFY(client1->isActive()); // Switch to window below. workspace()->switchWindow(Workspace::DirectionSouth); QVERIFY(client2->isActive()); // Switch to window below. workspace()->switchWindow(Workspace::DirectionSouth); QVERIFY(client3->isActive()); // Switch to window below. workspace()->switchWindow(Workspace::DirectionSouth); QVERIFY(client4->isActive()); // Destroy all clients. shellSurface1.reset(); QVERIFY(Test::waitForWindowDestroyed(client1)); shellSurface2.reset(); QVERIFY(Test::waitForWindowDestroyed(client2)); shellSurface3.reset(); QVERIFY(Test::waitForWindowDestroyed(client3)); shellSurface4.reset(); QVERIFY(Test::waitForWindowDestroyed(client4)); } void ActivationTest::testSwitchToWindowMaximized() { // This test verifies that we switch to the top-most maximized client, i.e. // the one that user sees at the moment. See bug 411356. using namespace KWayland::Client; // Prepare the test environment. stackScreensHorizontally(); // Create several maximized clients on the left screen. QScopedPointer surface1(Test::createSurface()); QScopedPointer shellSurface1(Test::createXdgShellStableSurface(surface1.data())); XdgShellClient *client1 = Test::renderAndWaitForShown(surface1.data(), QSize(100, 50), Qt::blue); QVERIFY(client1); QVERIFY(client1->isActive()); QSignalSpy configureRequestedSpy1(shellSurface1.data(), &XdgShellSurface::configureRequested); QVERIFY(configureRequestedSpy1.wait()); workspace()->slotWindowMaximize(); QVERIFY(configureRequestedSpy1.wait()); - QSignalSpy geometryChangedSpy1(client1, &XdgShellClient::geometryChanged); - QVERIFY(geometryChangedSpy1.isValid()); + QSignalSpy frameGeometryChangedSpy1(client1, &XdgShellClient::frameGeometryChanged); + QVERIFY(frameGeometryChangedSpy1.isValid()); shellSurface1->ackConfigure(configureRequestedSpy1.last().at(2).value()); Test::render(surface1.data(), configureRequestedSpy1.last().at(0).toSize(), Qt::red); - QVERIFY(geometryChangedSpy1.wait()); + QVERIFY(frameGeometryChangedSpy1.wait()); QScopedPointer surface2(Test::createSurface()); QScopedPointer shellSurface2(Test::createXdgShellStableSurface(surface2.data())); XdgShellClient *client2 = Test::renderAndWaitForShown(surface2.data(), QSize(100, 50), Qt::blue); QVERIFY(client2); QVERIFY(client2->isActive()); QSignalSpy configureRequestedSpy2(shellSurface2.data(), &XdgShellSurface::configureRequested); QVERIFY(configureRequestedSpy2.wait()); workspace()->slotWindowMaximize(); QVERIFY(configureRequestedSpy2.wait()); - QSignalSpy geometryChangedSpy2(client2, &XdgShellClient::geometryChanged); - QVERIFY(geometryChangedSpy2.isValid()); + QSignalSpy frameGeometryChangedSpy2(client2, &XdgShellClient::frameGeometryChanged); + QVERIFY(frameGeometryChangedSpy2.isValid()); shellSurface2->ackConfigure(configureRequestedSpy2.last().at(2).value()); Test::render(surface2.data(), configureRequestedSpy2.last().at(0).toSize(), Qt::red); - QVERIFY(geometryChangedSpy2.wait()); + QVERIFY(frameGeometryChangedSpy2.wait()); const QList stackingOrder = workspace()->stackingOrder(); QVERIFY(stackingOrder.indexOf(client1) < stackingOrder.indexOf(client2)); QCOMPARE(client1->maximizeMode(), MaximizeFull); QCOMPARE(client2->maximizeMode(), MaximizeFull); // Create several clients on the right screen. QScopedPointer surface3(Test::createSurface()); QScopedPointer shellSurface3(Test::createXdgShellStableSurface(surface3.data())); XdgShellClient *client3 = Test::renderAndWaitForShown(surface3.data(), QSize(100, 50), Qt::blue); QVERIFY(client3); QVERIFY(client3->isActive()); QScopedPointer surface4(Test::createSurface()); QScopedPointer shellSurface4(Test::createXdgShellStableSurface(surface4.data())); XdgShellClient *client4 = Test::renderAndWaitForShown(surface4.data(), QSize(100, 50), Qt::blue); QVERIFY(client4); QVERIFY(client4->isActive()); client3->move(QPoint(1380, 200)); client4->move(QPoint(1580, 200)); // Switch to window to the left. workspace()->switchWindow(Workspace::DirectionWest); QVERIFY(client3->isActive()); // Switch to window to the left. workspace()->switchWindow(Workspace::DirectionWest); QVERIFY(client2->isActive()); // Switch to window to the left. workspace()->switchWindow(Workspace::DirectionWest); QVERIFY(client4->isActive()); // Destroy all clients. shellSurface1.reset(); QVERIFY(Test::waitForWindowDestroyed(client1)); shellSurface2.reset(); QVERIFY(Test::waitForWindowDestroyed(client2)); shellSurface3.reset(); QVERIFY(Test::waitForWindowDestroyed(client3)); shellSurface4.reset(); QVERIFY(Test::waitForWindowDestroyed(client4)); } void ActivationTest::testSwitchToWindowFullScreen() { // This test verifies that we switch to the top-most fullscreen client, i.e. // the one that user sees at the moment. See bug 411356. using namespace KWayland::Client; // Prepare the test environment. stackScreensVertically(); // Create several maximized clients on the top screen. QScopedPointer surface1(Test::createSurface()); QScopedPointer shellSurface1(Test::createXdgShellStableSurface(surface1.data())); XdgShellClient *client1 = Test::renderAndWaitForShown(surface1.data(), QSize(100, 50), Qt::blue); QVERIFY(client1); QVERIFY(client1->isActive()); QSignalSpy configureRequestedSpy1(shellSurface1.data(), &XdgShellSurface::configureRequested); QVERIFY(configureRequestedSpy1.wait()); workspace()->slotWindowFullScreen(); QVERIFY(configureRequestedSpy1.wait()); - QSignalSpy geometryChangedSpy1(client1, &XdgShellClient::geometryChanged); - QVERIFY(geometryChangedSpy1.isValid()); + QSignalSpy frameGeometryChangedSpy1(client1, &XdgShellClient::frameGeometryChanged); + QVERIFY(frameGeometryChangedSpy1.isValid()); shellSurface1->ackConfigure(configureRequestedSpy1.last().at(2).value()); Test::render(surface1.data(), configureRequestedSpy1.last().at(0).toSize(), Qt::red); - QVERIFY(geometryChangedSpy1.wait()); + QVERIFY(frameGeometryChangedSpy1.wait()); QScopedPointer surface2(Test::createSurface()); QScopedPointer shellSurface2(Test::createXdgShellStableSurface(surface2.data())); XdgShellClient *client2 = Test::renderAndWaitForShown(surface2.data(), QSize(100, 50), Qt::blue); QVERIFY(client2); QVERIFY(client2->isActive()); QSignalSpy configureRequestedSpy2(shellSurface2.data(), &XdgShellSurface::configureRequested); QVERIFY(configureRequestedSpy2.wait()); workspace()->slotWindowFullScreen(); QVERIFY(configureRequestedSpy2.wait()); - QSignalSpy geometryChangedSpy2(client2, &XdgShellClient::geometryChanged); - QVERIFY(geometryChangedSpy2.isValid()); + QSignalSpy frameGeometryChangedSpy2(client2, &XdgShellClient::frameGeometryChanged); + QVERIFY(frameGeometryChangedSpy2.isValid()); shellSurface2->ackConfigure(configureRequestedSpy2.last().at(2).value()); Test::render(surface2.data(), configureRequestedSpy2.last().at(0).toSize(), Qt::red); - QVERIFY(geometryChangedSpy2.wait()); + QVERIFY(frameGeometryChangedSpy2.wait()); const QList stackingOrder = workspace()->stackingOrder(); QVERIFY(stackingOrder.indexOf(client1) < stackingOrder.indexOf(client2)); QVERIFY(client1->isFullScreen()); QVERIFY(client2->isFullScreen()); // Create several clients on the bottom screen. QScopedPointer surface3(Test::createSurface()); QScopedPointer shellSurface3(Test::createXdgShellStableSurface(surface3.data())); XdgShellClient *client3 = Test::renderAndWaitForShown(surface3.data(), QSize(100, 50), Qt::blue); QVERIFY(client3); QVERIFY(client3->isActive()); QScopedPointer surface4(Test::createSurface()); QScopedPointer shellSurface4(Test::createXdgShellStableSurface(surface4.data())); XdgShellClient *client4 = Test::renderAndWaitForShown(surface4.data(), QSize(100, 50), Qt::blue); QVERIFY(client4); QVERIFY(client4->isActive()); client3->move(QPoint(200, 1224)); client4->move(QPoint(200, 1424)); // Switch to window above. workspace()->switchWindow(Workspace::DirectionNorth); QVERIFY(client3->isActive()); // Switch to window above. workspace()->switchWindow(Workspace::DirectionNorth); QVERIFY(client2->isActive()); // Switch to window above. workspace()->switchWindow(Workspace::DirectionNorth); QVERIFY(client4->isActive()); // Destroy all clients. shellSurface1.reset(); QVERIFY(Test::waitForWindowDestroyed(client1)); shellSurface2.reset(); QVERIFY(Test::waitForWindowDestroyed(client2)); shellSurface3.reset(); QVERIFY(Test::waitForWindowDestroyed(client3)); shellSurface4.reset(); QVERIFY(Test::waitForWindowDestroyed(client4)); } void ActivationTest::stackScreensHorizontally() { // Process pending wl_output bind requests before destroying all outputs. QTest::qWait(1); const QVector screenGeometries { QRect(0, 0, 1280, 1024), QRect(1280, 0, 1280, 1024), }; const QVector screenScales { 1, 1, }; QMetaObject::invokeMethod(kwinApp()->platform(), "setVirtualOutputs", Qt::DirectConnection, Q_ARG(int, screenGeometries.count()), Q_ARG(QVector, screenGeometries), Q_ARG(QVector, screenScales) ); } void ActivationTest::stackScreensVertically() { // Process pending wl_output bind requests before destroying all outputs. QTest::qWait(1); const QVector screenGeometries { QRect(0, 0, 1280, 1024), QRect(0, 1024, 1280, 1024), }; const QVector screenScales { 1, 1, }; QMetaObject::invokeMethod(kwinApp()->platform(), "setVirtualOutputs", Qt::DirectConnection, Q_ARG(int, screenGeometries.count()), Q_ARG(QVector, screenGeometries), Q_ARG(QVector, screenScales) ); } } WAYLANDTEST_MAIN(KWin::ActivationTest) #include "activation_test.moc" diff --git a/autotests/integration/effects/maximize_animation_test.cpp b/autotests/integration/effects/maximize_animation_test.cpp index bccb41198..5d0f75607 100644 --- a/autotests/integration/effects/maximize_animation_test.cpp +++ b/autotests/integration/effects/maximize_animation_test.cpp @@ -1,212 +1,212 @@ /******************************************************************** KWin - the KDE window manager This file is part of the KDE project. Copyright (C) 2019 Vlad Zahorodnii This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . *********************************************************************/ #include "kwin_wayland_test.h" #include "abstract_client.h" #include "composite.h" #include "effectloader.h" #include "effects.h" #include "platform.h" #include "scene.h" #include "xdgshellclient.h" #include "wayland_server.h" #include "workspace.h" #include "effect_builtins.h" #include #include using namespace KWin; static const QString s_socketName = QStringLiteral("wayland_test_effects_maximize_animation-0"); class MaximizeAnimationTest : public QObject { Q_OBJECT private Q_SLOTS: void initTestCase(); void init(); void cleanup(); void testMaximizeRestore_data(); void testMaximizeRestore(); }; void MaximizeAnimationTest::initTestCase() { qputenv("XDG_DATA_DIRS", QCoreApplication::applicationDirPath().toUtf8()); qRegisterMetaType(); qRegisterMetaType(); QSignalSpy workspaceCreatedSpy(kwinApp(), &Application::workspaceCreated); QVERIFY(workspaceCreatedSpy.isValid()); kwinApp()->platform()->setInitialWindowSize(QSize(1280, 1024)); QVERIFY(waylandServer()->init(s_socketName.toLocal8Bit())); auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); KConfigGroup plugins(config, QStringLiteral("Plugins")); ScriptedEffectLoader loader; const auto builtinNames = BuiltInEffects::availableEffectNames() << loader.listOfKnownEffects(); for (const QString &name : builtinNames) { plugins.writeEntry(name + QStringLiteral("Enabled"), false); } config->sync(); kwinApp()->setConfig(config); qputenv("KWIN_EFFECTS_FORCE_ANIMATIONS", QByteArrayLiteral("1")); kwinApp()->start(); QVERIFY(workspaceCreatedSpy.wait()); waylandServer()->initWorkspace(); } void MaximizeAnimationTest::init() { QVERIFY(Test::setupWaylandConnection()); } void MaximizeAnimationTest::cleanup() { auto effectsImpl = qobject_cast(effects); QVERIFY(effectsImpl); effectsImpl->unloadAllEffects(); QVERIFY(effectsImpl->loadedEffects().isEmpty()); Test::destroyWaylandConnection(); } void MaximizeAnimationTest::testMaximizeRestore_data() { QTest::addColumn("type"); QTest::newRow("xdgShellV6") << Test::XdgShellSurfaceType::XdgShellV6; QTest::newRow("xdgWmBase") << Test::XdgShellSurfaceType::XdgShellStable; } void MaximizeAnimationTest::testMaximizeRestore() { // This test verifies that the maximize effect animates a client // when it's maximized or restored. using namespace KWayland::Client; // Create the test client. QScopedPointer surface(Test::createSurface()); QVERIFY(!surface.isNull()); QFETCH(Test::XdgShellSurfaceType, type); QScopedPointer shellSurface(createXdgShellSurface(type, surface.data(), nullptr, Test::CreationSetup::CreateOnly)); // Wait for the initial configure event. XdgShellSurface::States states; QSignalSpy configureRequestedSpy(shellSurface.data(), &XdgShellSurface::configureRequested); surface->commit(Surface::CommitFlag::None); QVERIFY(configureRequestedSpy.isValid()); QVERIFY(configureRequestedSpy.wait()); QCOMPARE(configureRequestedSpy.count(), 1); QCOMPARE(configureRequestedSpy.last().at(0).value(), QSize(0, 0)); states = configureRequestedSpy.last().at(1).value(); QVERIFY(!states.testFlag(XdgShellSurface::State::Activated)); QVERIFY(!states.testFlag(XdgShellSurface::State::Maximized)); // Draw contents of the surface. shellSurface->ackConfigure(configureRequestedSpy.last().at(2).value()); XdgShellClient *client = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); QVERIFY(client); QVERIFY(client->isActive()); QCOMPARE(client->maximizeMode(), MaximizeMode::MaximizeRestore); // We should receive a configure event when the client becomes active. QVERIFY(configureRequestedSpy.wait()); QCOMPARE(configureRequestedSpy.count(), 2); states = configureRequestedSpy.last().at(1).value(); QVERIFY(states.testFlag(XdgShellSurface::State::Activated)); QVERIFY(!states.testFlag(XdgShellSurface::State::Maximized)); // Load effect that will be tested. const QString effectName = QStringLiteral("kwin4_effect_maximize"); auto effectsImpl = qobject_cast(effects); QVERIFY(effectsImpl); QVERIFY(effectsImpl->loadEffect(effectName)); QCOMPARE(effectsImpl->loadedEffects().count(), 1); QCOMPARE(effectsImpl->loadedEffects().first(), effectName); Effect *effect = effectsImpl->findEffect(effectName); QVERIFY(effect); QVERIFY(!effect->isActive()); // Maximize the client. - QSignalSpy geometryChangedSpy(client, &XdgShellClient::geometryChanged); - QVERIFY(geometryChangedSpy.isValid()); + QSignalSpy frameGeometryChangedSpy(client, &XdgShellClient::frameGeometryChanged); + QVERIFY(frameGeometryChangedSpy.isValid()); QSignalSpy maximizeChangedSpy(client, qOverload(&XdgShellClient::clientMaximizedStateChanged)); QVERIFY(maximizeChangedSpy.isValid()); workspace()->slotWindowMaximize(); QVERIFY(configureRequestedSpy.wait()); QCOMPARE(configureRequestedSpy.count(), 3); QCOMPARE(configureRequestedSpy.last().at(0).value(), QSize(1280, 1024)); states = configureRequestedSpy.last().at(1).value(); QVERIFY(states.testFlag(XdgShellSurface::State::Activated)); QVERIFY(states.testFlag(XdgShellSurface::State::Maximized)); // Draw contents of the maximized client. shellSurface->ackConfigure(configureRequestedSpy.last().at(2).value()); Test::render(surface.data(), QSize(1280, 1024), Qt::red); - QVERIFY(geometryChangedSpy.wait()); - QCOMPARE(geometryChangedSpy.count(), 2); + QVERIFY(frameGeometryChangedSpy.wait()); + QCOMPARE(frameGeometryChangedSpy.count(), 1); QCOMPARE(maximizeChangedSpy.count(), 1); QCOMPARE(client->maximizeMode(), MaximizeMode::MaximizeFull); QVERIFY(effect->isActive()); // Eventually, the animation will be complete. QTRY_VERIFY(!effect->isActive()); // Restore the client. workspace()->slotWindowMaximize(); QVERIFY(configureRequestedSpy.wait()); QCOMPARE(configureRequestedSpy.count(), 4); QCOMPARE(configureRequestedSpy.last().at(0).value(), QSize(100, 50)); states = configureRequestedSpy.last().at(1).value(); QVERIFY(states.testFlag(XdgShellSurface::State::Activated)); QVERIFY(!states.testFlag(XdgShellSurface::State::Maximized)); // Draw contents of the restored client. shellSurface->ackConfigure(configureRequestedSpy.last().at(2).value()); Test::render(surface.data(), QSize(100, 50), Qt::blue); - QVERIFY(geometryChangedSpy.wait()); - QCOMPARE(geometryChangedSpy.count(), 4); + QVERIFY(frameGeometryChangedSpy.wait()); + QCOMPARE(frameGeometryChangedSpy.count(), 2); QCOMPARE(maximizeChangedSpy.count(), 2); QCOMPARE(client->maximizeMode(), MaximizeMode::MaximizeRestore); QVERIFY(effect->isActive()); // Eventually, the animation will be complete. QTRY_VERIFY(!effect->isActive()); // Destroy the test client. surface.reset(); QVERIFY(Test::waitForWindowDestroyed(client)); } WAYLANDTEST_MAIN(MaximizeAnimationTest) #include "maximize_animation_test.moc" diff --git a/autotests/integration/maximize_test.cpp b/autotests/integration/maximize_test.cpp index 1dd1ab22e..4f3a16487 100644 --- a/autotests/integration/maximize_test.cpp +++ b/autotests/integration/maximize_test.cpp @@ -1,376 +1,376 @@ /******************************************************************** 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 "cursor.h" #include "decorations/decorationbridge.h" #include "decorations/settings.h" #include "platform.h" #include "xdgshellclient.h" #include "screens.h" #include "wayland_server.h" #include "workspace.h" #include #include #include #include #include #include #include #include #include #include using namespace KWin; using namespace KWayland::Client; static const QString s_socketName = QStringLiteral("wayland_test_kwin_maximized-0"); class TestMaximized : public QObject { Q_OBJECT private Q_SLOTS: void initTestCase(); void init(); void cleanup(); void testMaximizedPassedToDeco(); void testInitiallyMaximized(); void testBorderlessMaximizedWindow(); void testBorderlessMaximizedWindowNoClientSideDecoration(); }; void TestMaximized::initTestCase() { qRegisterMetaType(); qRegisterMetaType(); QSignalSpy workspaceCreatedSpy(kwinApp(), &Application::workspaceCreated); QVERIFY(workspaceCreatedSpy.isValid()); kwinApp()->platform()->setInitialWindowSize(QSize(1280, 1024)); QVERIFY(waylandServer()->init(s_socketName.toLocal8Bit())); QMetaObject::invokeMethod(kwinApp()->platform(), "setVirtualOutputs", Qt::DirectConnection, Q_ARG(int, 2)); 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 TestMaximized::init() { QVERIFY(Test::setupWaylandConnection(Test::AdditionalWaylandInterface::Decoration | Test::AdditionalWaylandInterface::XdgDecoration | Test::AdditionalWaylandInterface::PlasmaShell)); screens()->setCurrent(0); KWin::Cursor::setPos(QPoint(1280, 512)); } void TestMaximized::cleanup() { Test::destroyWaylandConnection(); // adjust config auto group = kwinApp()->config()->group("Windows"); group.writeEntry("BorderlessMaximizedWindows", false); group.sync(); Workspace::self()->slotReconfigure(); QCOMPARE(options->borderlessMaximizedWindows(), false); } void TestMaximized::testMaximizedPassedToDeco() { // this test verifies that when a XdgShellClient gets maximized the Decoration receives the signal // Create the test client. QScopedPointer surface(Test::createSurface()); QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); QScopedPointer ssd(Test::waylandServerSideDecoration()->create(surface.data())); auto client = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); QVERIFY(client); QVERIFY(client->isDecorated()); auto decoration = client->decoration(); QVERIFY(decoration); QCOMPARE(client->maximizeMode(), MaximizeMode::MaximizeRestore); // Wait for configure event that signals the client is active now. QSignalSpy configureRequestedSpy(shellSurface.data(), &XdgShellSurface::configureRequested); QVERIFY(configureRequestedSpy.isValid()); QVERIFY(configureRequestedSpy.wait()); QCOMPARE(configureRequestedSpy.count(), 1); // When there are no borders, there is no change to them when maximizing. // TODO: we should test both cases with fixed fake decoration for autotests. const bool hasBorders = Decoration::DecorationBridge::self()->settings()->borderSize() != KDecoration2::BorderSize::None; // now maximize QSignalSpy bordersChangedSpy(decoration, &KDecoration2::Decoration::bordersChanged); QVERIFY(bordersChangedSpy.isValid()); QSignalSpy maximizedChangedSpy(decoration->client().data(), &KDecoration2::DecoratedClient::maximizedChanged); QVERIFY(maximizedChangedSpy.isValid()); - QSignalSpy geometryShapeChangedSpy(client, &AbstractClient::geometryShapeChanged); - QVERIFY(geometryShapeChangedSpy.isValid()); + QSignalSpy frameGeometryChangedSpy(client, &AbstractClient::frameGeometryChanged); + QVERIFY(frameGeometryChangedSpy.isValid()); workspace()->slotWindowMaximize(); QVERIFY(configureRequestedSpy.wait()); QCOMPARE(configureRequestedSpy.count(), 2); QCOMPARE(configureRequestedSpy.last().at(0).toSize(), QSize(1280, 1024 - decoration->borderTop())); shellSurface->ackConfigure(configureRequestedSpy.last().at(2).value()); Test::render(surface.data(), configureRequestedSpy.last().at(0).toSize(), Qt::red); - QVERIFY(geometryShapeChangedSpy.wait()); + QVERIFY(frameGeometryChangedSpy.wait()); // If no borders, there is only the initial geometry shape change, but none through border resizing. - QCOMPARE(geometryShapeChangedSpy.count(), hasBorders ? 2 : 1); + QCOMPARE(frameGeometryChangedSpy.count(), hasBorders ? 2 : 1); QCOMPARE(client->maximizeMode(), MaximizeMode::MaximizeFull); QCOMPARE(maximizedChangedSpy.count(), 1); QCOMPARE(maximizedChangedSpy.last().first().toBool(), true); QCOMPARE(bordersChangedSpy.count(), hasBorders ? 1 : 0); QCOMPARE(decoration->borderLeft(), 0); QCOMPARE(decoration->borderBottom(), 0); QCOMPARE(decoration->borderRight(), 0); QVERIFY(decoration->borderTop() != 0); // now unmaximize again workspace()->slotWindowMaximize(); QVERIFY(configureRequestedSpy.wait()); QCOMPARE(configureRequestedSpy.count(), 3); QCOMPARE(configureRequestedSpy.last().at(0).toSize(), QSize(100, 50)); shellSurface->ackConfigure(configureRequestedSpy.last().at(2).value()); Test::render(surface.data(), QSize(100, 50), Qt::red); - QVERIFY(geometryShapeChangedSpy.wait()); - QCOMPARE(geometryShapeChangedSpy.count(), hasBorders ? 4 : 2); + QVERIFY(frameGeometryChangedSpy.wait()); + QCOMPARE(frameGeometryChangedSpy.count(), hasBorders ? 4 : 2); QCOMPARE(client->maximizeMode(), MaximizeMode::MaximizeRestore); QCOMPARE(maximizedChangedSpy.count(), 2); QCOMPARE(maximizedChangedSpy.last().first().toBool(), false); QCOMPARE(bordersChangedSpy.count(), hasBorders ? 2 : 0); QVERIFY(decoration->borderTop() != 0); QVERIFY(decoration->borderLeft() != !hasBorders); QVERIFY(decoration->borderRight() != !hasBorders); QVERIFY(decoration->borderBottom() != !hasBorders); // Destroy the test client. shellSurface.reset(); QVERIFY(Test::waitForWindowDestroyed(client)); } void TestMaximized::testInitiallyMaximized() { // This test verifies that a window created as maximized, will be maximized. // Create the test client. QScopedPointer surface(Test::createSurface()); QScopedPointer shellSurface( Test::createXdgShellStableSurface(surface.data(), surface.data(), Test::CreationSetup::CreateOnly)); QSignalSpy configureRequestedSpy(shellSurface.data(), &XdgShellSurface::configureRequested); QVERIFY(configureRequestedSpy.isValid()); shellSurface->setMaximized(true); surface->commit(Surface::CommitFlag::None); // Wait for the initial configure event. XdgShellSurface::States states; QVERIFY(configureRequestedSpy.wait()); QCOMPARE(configureRequestedSpy.count(), 1); states = configureRequestedSpy.last().at(1).value(); QVERIFY(!states.testFlag(XdgShellSurface::State::Activated)); QVERIFY(states.testFlag(XdgShellSurface::State::Maximized)); // Now let's render in an incorrect size. shellSurface->ackConfigure(configureRequestedSpy.last().at(2).value()); XdgShellClient *client = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); QVERIFY(client); QCOMPARE(client->frameGeometry(), QRect(0, 0, 100, 50)); QEXPECT_FAIL("", "Should go out of maximzied", Continue); QCOMPARE(client->maximizeMode(), MaximizeMode::MaximizeRestore); // Destroy the client. shellSurface.reset(); QVERIFY(Test::waitForWindowDestroyed(client)); } void TestMaximized::testBorderlessMaximizedWindow() { // This test verifies that a maximized client looses it's server-side // decoration when the borderless maximized option is on. // Enable the borderless maximized windows option. auto group = kwinApp()->config()->group("Windows"); group.writeEntry("BorderlessMaximizedWindows", true); group.sync(); Workspace::self()->slotReconfigure(); QCOMPARE(options->borderlessMaximizedWindows(), true); // Create the test client. QScopedPointer surface(Test::createSurface()); QScopedPointer shellSurface( Test::createXdgShellStableSurface(surface.data(), surface.data(), Test::CreationSetup::CreateOnly)); QScopedPointer decoration( Test::xdgDecorationManager()->getToplevelDecoration(shellSurface.data())); QSignalSpy decorationConfiguredSpy(decoration.data(), &XdgDecoration::modeChanged); QVERIFY(decorationConfiguredSpy.isValid()); QSignalSpy configureRequestedSpy(shellSurface.data(), &XdgShellSurface::configureRequested); QVERIFY(configureRequestedSpy.isValid()); decoration->setMode(XdgDecoration::Mode::ServerSide); surface->commit(Surface::CommitFlag::None); // Wait for the initial configure event. XdgShellSurface::States states; QVERIFY(configureRequestedSpy.wait()); QCOMPARE(configureRequestedSpy.count(), 1); QCOMPARE(configureRequestedSpy.last().at(0).toSize(), QSize(0, 0)); states = configureRequestedSpy.last().at(1).value(); QVERIFY(!states.testFlag(XdgShellSurface::State::Activated)); QVERIFY(!states.testFlag(XdgShellSurface::State::Maximized)); // Map the client. shellSurface->ackConfigure(configureRequestedSpy.last().at(2).value()); XdgShellClient *client = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); QVERIFY(client); QVERIFY(client->isActive()); QCOMPARE(client->maximizeMode(), MaximizeMode::MaximizeRestore); QCOMPARE(client->requestedMaximizeMode(), MaximizeMode::MaximizeRestore); QCOMPARE(client->isDecorated(), true); // We should receive a configure event when the client becomes active. QVERIFY(configureRequestedSpy.wait()); QCOMPARE(configureRequestedSpy.count(), 2); states = configureRequestedSpy.last().at(1).value(); QVERIFY(states.testFlag(XdgShellSurface::State::Activated)); QVERIFY(!states.testFlag(XdgShellSurface::State::Maximized)); // Maximize the client. const QRect maximizeRestoreGeometry = client->frameGeometry(); workspace()->slotWindowMaximize(); QVERIFY(configureRequestedSpy.wait()); QCOMPARE(configureRequestedSpy.count(), 3); QCOMPARE(configureRequestedSpy.last().at(0).toSize(), QSize(1280, 1024)); states = configureRequestedSpy.last().at(1).value(); QVERIFY(states.testFlag(XdgShellSurface::State::Activated)); QVERIFY(states.testFlag(XdgShellSurface::State::Maximized)); - QSignalSpy geometryChangedSpy(client, &AbstractClient::geometryChanged); - QVERIFY(geometryChangedSpy.isValid()); + QSignalSpy frameGeometryChangedSpy(client, &AbstractClient::frameGeometryChanged); + QVERIFY(frameGeometryChangedSpy.isValid()); shellSurface->ackConfigure(configureRequestedSpy.last().at(2).value()); Test::render(surface.data(), QSize(1280, 1024), Qt::blue); - QVERIFY(geometryChangedSpy.wait()); + QVERIFY(frameGeometryChangedSpy.wait()); QCOMPARE(client->frameGeometry(), QRect(0, 0, 1280, 1024)); QCOMPARE(client->maximizeMode(), MaximizeMode::MaximizeFull); QCOMPARE(client->requestedMaximizeMode(), MaximizeMode::MaximizeFull); QCOMPARE(client->isDecorated(), false); // Restore the client. workspace()->slotWindowMaximize(); QVERIFY(configureRequestedSpy.wait()); QCOMPARE(configureRequestedSpy.count(), 4); QCOMPARE(configureRequestedSpy.last().at(0).toSize(), QSize(100, 50)); states = configureRequestedSpy.last().at(1).value(); QVERIFY(states.testFlag(XdgShellSurface::State::Activated)); QVERIFY(!states.testFlag(XdgShellSurface::State::Maximized)); shellSurface->ackConfigure(configureRequestedSpy.last().at(2).value()); Test::render(surface.data(), QSize(100, 50), Qt::red); - QVERIFY(geometryChangedSpy.wait()); + QVERIFY(frameGeometryChangedSpy.wait()); QCOMPARE(client->frameGeometry(), maximizeRestoreGeometry); QCOMPARE(client->maximizeMode(), MaximizeMode::MaximizeRestore); QCOMPARE(client->requestedMaximizeMode(), MaximizeMode::MaximizeRestore); QCOMPARE(client->isDecorated(), true); // Destroy the client. shellSurface.reset(); QVERIFY(Test::waitForWindowDestroyed(client)); } void TestMaximized::testBorderlessMaximizedWindowNoClientSideDecoration() { // test case verifies that borderless maximized windows doesn't cause // clients to render client-side decorations instead (BUG 405385) // adjust config auto group = kwinApp()->config()->group("Windows"); group.writeEntry("BorderlessMaximizedWindows", true); group.sync(); Workspace::self()->slotReconfigure(); QCOMPARE(options->borderlessMaximizedWindows(), true); QScopedPointer surface(Test::createSurface()); QScopedPointer xdgShellSurface(Test::createXdgShellStableSurface(surface.data())); QScopedPointer deco(Test::xdgDecorationManager()->getToplevelDecoration(xdgShellSurface.data())); QSignalSpy decorationConfiguredSpy(deco.data(), &XdgDecoration::modeChanged); QVERIFY(decorationConfiguredSpy.isValid()); auto client = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); - QSignalSpy geometryChangedSpy(client, &XdgShellClient::geometryChanged); - QVERIFY(geometryChangedSpy.isValid()); + QSignalSpy frameGeometryChangedSpy(client, &XdgShellClient::frameGeometryChanged); + QVERIFY(frameGeometryChangedSpy.isValid()); QSignalSpy sizeChangeRequestedSpy(xdgShellSurface.data(), &XdgShellSurface::sizeChanged); QVERIFY(sizeChangeRequestedSpy.isValid()); QSignalSpy configureRequestedSpy(xdgShellSurface.data(), &XdgShellSurface::configureRequested); QVERIFY(configureRequestedSpy.isValid()); QVERIFY(client->isDecorated()); QVERIFY(!client->noBorder()); configureRequestedSpy.wait(); QCOMPARE(decorationConfiguredSpy.count(), 1); QCOMPARE(deco->mode(), XdgDecoration::Mode::ServerSide); // go to maximized xdgShellSurface->setMaximized(true); QVERIFY(sizeChangeRequestedSpy.wait()); QCOMPARE(sizeChangeRequestedSpy.count(), 1); for (const auto &it: configureRequestedSpy) { xdgShellSurface->ackConfigure(it[2].toInt()); } Test::render(surface.data(), sizeChangeRequestedSpy.last().first().toSize(), Qt::red); - QVERIFY(geometryChangedSpy.wait()); + QVERIFY(frameGeometryChangedSpy.wait()); // no deco QVERIFY(!client->isDecorated()); QVERIFY(client->noBorder()); // but still server-side QCOMPARE(deco->mode(), XdgDecoration::Mode::ServerSide); // go back to normal xdgShellSurface->setMaximized(false); QVERIFY(sizeChangeRequestedSpy.wait()); QCOMPARE(sizeChangeRequestedSpy.count(), 2); for (const auto &it: configureRequestedSpy) { xdgShellSurface->ackConfigure(it[2].toInt()); } Test::render(surface.data(), sizeChangeRequestedSpy.last().first().toSize(), Qt::red); - QVERIFY(geometryChangedSpy.wait()); + QVERIFY(frameGeometryChangedSpy.wait()); QVERIFY(client->isDecorated()); QVERIFY(!client->noBorder()); QCOMPARE(deco->mode(), XdgDecoration::Mode::ServerSide); } WAYLANDTEST_MAIN(TestMaximized) #include "maximize_test.moc" diff --git a/autotests/integration/move_resize_window_test.cpp b/autotests/integration/move_resize_window_test.cpp index b36deed8e..f7ef81f54 100644 --- a/autotests/integration/move_resize_window_test.cpp +++ b/autotests/integration/move_resize_window_test.cpp @@ -1,1216 +1,1214 @@ /******************************************************************** 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 "kwin_wayland_test.h" #include "atoms.h" #include "platform.h" #include "abstract_client.h" #include "x11client.h" #include "cursor.h" #include "effects.h" #include "screens.h" #include "wayland_server.h" #include "workspace.h" #include "xdgshellclient.h" #include "deleted.h" #include #include #include #include #include #include #include #include #include #include Q_DECLARE_METATYPE(KWin::QuickTileMode) Q_DECLARE_METATYPE(KWin::MaximizeMode) namespace KWin { static const QString s_socketName = QStringLiteral("wayland_test_kwin_quick_tiling-0"); class MoveResizeWindowTest : public QObject { Q_OBJECT private Q_SLOTS: void initTestCase(); void init(); void cleanup(); void testMove(); void testResize(); void testPackTo_data(); void testPackTo(); void testPackAgainstClient_data(); void testPackAgainstClient(); void testGrowShrink_data(); void testGrowShrink(); void testPointerMoveEnd_data(); void testPointerMoveEnd(); void testClientSideMove_data(); void testClientSideMove(); void testPlasmaShellSurfaceMovable_data(); void testPlasmaShellSurfaceMovable(); void testNetMove(); void testAdjustClientGeometryOfAutohidingX11Panel_data(); void testAdjustClientGeometryOfAutohidingX11Panel(); void testAdjustClientGeometryOfAutohidingWaylandPanel_data(); void testAdjustClientGeometryOfAutohidingWaylandPanel(); void testResizeForVirtualKeyboard(); void testResizeForVirtualKeyboardWithMaximize(); void testResizeForVirtualKeyboardWithFullScreen(); void testDestroyMoveClient(); void testDestroyResizeClient(); void testUnmapMoveClient(); void testUnmapResizeClient(); private: KWayland::Client::ConnectionThread *m_connection = nullptr; KWayland::Client::Compositor *m_compositor = nullptr; }; void MoveResizeWindowTest::initTestCase() { qRegisterMetaType(); qRegisterMetaType(); qRegisterMetaType(); qRegisterMetaType("MaximizeMode"); QSignalSpy workspaceCreatedSpy(kwinApp(), &Application::workspaceCreated); QVERIFY(workspaceCreatedSpy.isValid()); kwinApp()->platform()->setInitialWindowSize(QSize(1280, 1024)); QVERIFY(waylandServer()->init(s_socketName.toLocal8Bit())); kwinApp()->start(); QVERIFY(workspaceCreatedSpy.wait()); QCOMPARE(screens()->count(), 1); QCOMPARE(screens()->geometry(0), QRect(0, 0, 1280, 1024)); } void MoveResizeWindowTest::init() { QVERIFY(Test::setupWaylandConnection(Test::AdditionalWaylandInterface::PlasmaShell | Test::AdditionalWaylandInterface::Seat)); QVERIFY(Test::waitForWaylandPointer()); m_connection = Test::waylandConnection(); m_compositor = Test::waylandCompositor(); screens()->setCurrent(0); } void MoveResizeWindowTest::cleanup() { Test::destroyWaylandConnection(); } void MoveResizeWindowTest::testMove() { using namespace KWayland::Client; QScopedPointer surface(Test::createSurface()); QVERIFY(!surface.isNull()); QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); QVERIFY(!shellSurface.isNull()); QSignalSpy sizeChangeSpy(shellSurface.data(), &XdgShellSurface::sizeChanged); QVERIFY(sizeChangeSpy.isValid()); // let's render auto c = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); QVERIFY(c); QCOMPARE(workspace()->activeClient(), c); QCOMPARE(c->frameGeometry(), QRect(0, 0, 100, 50)); - QSignalSpy geometryChangedSpy(c, &AbstractClient::geometryChanged); - QVERIFY(geometryChangedSpy.isValid()); QSignalSpy startMoveResizedSpy(c, &AbstractClient::clientStartUserMovedResized); QVERIFY(startMoveResizedSpy.isValid()); QSignalSpy moveResizedChangedSpy(c, &AbstractClient::moveResizedChanged); QVERIFY(moveResizedChangedSpy.isValid()); QSignalSpy clientStepUserMovedResizedSpy(c, &AbstractClient::clientStepUserMovedResized); QVERIFY(clientStepUserMovedResizedSpy.isValid()); QSignalSpy clientFinishUserMovedResizedSpy(c, &AbstractClient::clientFinishUserMovedResized); QVERIFY(clientFinishUserMovedResizedSpy.isValid()); // effects signal handlers QSignalSpy windowStartUserMovedResizedSpy(effects, &EffectsHandler::windowStartUserMovedResized); QVERIFY(windowStartUserMovedResizedSpy.isValid()); QSignalSpy windowStepUserMovedResizedSpy(effects, &EffectsHandler::windowStepUserMovedResized); QVERIFY(windowStepUserMovedResizedSpy.isValid()); QSignalSpy windowFinishUserMovedResizedSpy(effects, &EffectsHandler::windowFinishUserMovedResized); QVERIFY(windowFinishUserMovedResizedSpy.isValid()); // begin move QVERIFY(workspace()->moveResizeClient() == nullptr); QCOMPARE(c->isMove(), false); workspace()->slotWindowMove(); QCOMPARE(workspace()->moveResizeClient(), c); QCOMPARE(startMoveResizedSpy.count(), 1); QCOMPARE(moveResizedChangedSpy.count(), 1); QCOMPARE(windowStartUserMovedResizedSpy.count(), 1); QCOMPARE(c->isMove(), true); QCOMPARE(c->geometryRestore(), QRect(0, 0, 100, 50)); // send some key events, not going through input redirection const QPoint cursorPos = Cursor::pos(); c->keyPressEvent(Qt::Key_Right); c->updateMoveResize(Cursor::pos()); QCOMPARE(Cursor::pos(), cursorPos + QPoint(8, 0)); QEXPECT_FAIL("", "First event is ignored", Continue); QCOMPARE(clientStepUserMovedResizedSpy.count(), 1); clientStepUserMovedResizedSpy.clear(); windowStepUserMovedResizedSpy.clear(); c->keyPressEvent(Qt::Key_Right); c->updateMoveResize(Cursor::pos()); QCOMPARE(Cursor::pos(), cursorPos + QPoint(16, 0)); QCOMPARE(clientStepUserMovedResizedSpy.count(), 1); QCOMPARE(windowStepUserMovedResizedSpy.count(), 1); c->keyPressEvent(Qt::Key_Down | Qt::ALT); c->updateMoveResize(Cursor::pos()); QCOMPARE(clientStepUserMovedResizedSpy.count(), 2); QCOMPARE(windowStepUserMovedResizedSpy.count(), 2); QCOMPARE(c->frameGeometry(), QRect(16, 32, 100, 50)); QCOMPARE(Cursor::pos(), cursorPos + QPoint(16, 32)); // let's end QCOMPARE(clientFinishUserMovedResizedSpy.count(), 0); c->keyPressEvent(Qt::Key_Enter); QCOMPARE(clientFinishUserMovedResizedSpy.count(), 1); QCOMPARE(moveResizedChangedSpy.count(), 2); QCOMPARE(windowFinishUserMovedResizedSpy.count(), 1); QCOMPARE(c->frameGeometry(), QRect(16, 32, 100, 50)); QCOMPARE(c->isMove(), false); QVERIFY(workspace()->moveResizeClient() == nullptr); surface.reset(); QVERIFY(Test::waitForWindowDestroyed(c)); } void MoveResizeWindowTest::testResize() { // a test case which manually resizes a window using namespace KWayland::Client; QScopedPointer surface(Test::createSurface()); QVERIFY(!surface.isNull()); QScopedPointer shellSurface(Test::createXdgShellStableSurface( surface.data(), surface.data(), Test::CreationSetup::CreateOnly)); QVERIFY(!shellSurface.isNull()); // Wait for the initial configure event. XdgShellSurface::States states; QSignalSpy configureRequestedSpy(shellSurface.data(), &XdgShellSurface::configureRequested); QVERIFY(configureRequestedSpy.isValid()); surface->commit(Surface::CommitFlag::None); QVERIFY(configureRequestedSpy.wait()); QCOMPARE(configureRequestedSpy.count(), 1); states = configureRequestedSpy.last().at(1).value(); QVERIFY(!states.testFlag(XdgShellSurface::State::Activated)); QVERIFY(!states.testFlag(XdgShellSurface::State::Resizing)); // Let's render. shellSurface->ackConfigure(configureRequestedSpy.last().at(2).value()); auto c = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); QSignalSpy surfaceSizeChangedSpy(shellSurface.data(), &XdgShellSurface::sizeChanged); QVERIFY(surfaceSizeChangedSpy.isValid()); // We have to receive a configure event when the client becomes active. QVERIFY(configureRequestedSpy.wait()); QCOMPARE(configureRequestedSpy.count(), 2); states = configureRequestedSpy.last().at(1).value(); QVERIFY(states.testFlag(XdgShellSurface::State::Activated)); QVERIFY(!states.testFlag(XdgShellSurface::State::Resizing)); QVERIFY(c); QCOMPARE(workspace()->activeClient(), c); QCOMPARE(c->frameGeometry(), QRect(0, 0, 100, 50)); - QSignalSpy geometryChangedSpy(c, &AbstractClient::geometryChanged); - QVERIFY(geometryChangedSpy.isValid()); + QSignalSpy frameGeometryChangedSpy(c, &AbstractClient::frameGeometryChanged); + QVERIFY(frameGeometryChangedSpy.isValid()); QSignalSpy startMoveResizedSpy(c, &AbstractClient::clientStartUserMovedResized); QVERIFY(startMoveResizedSpy.isValid()); QSignalSpy moveResizedChangedSpy(c, &AbstractClient::moveResizedChanged); QVERIFY(moveResizedChangedSpy.isValid()); QSignalSpy clientStepUserMovedResizedSpy(c, &AbstractClient::clientStepUserMovedResized); QVERIFY(clientStepUserMovedResizedSpy.isValid()); QSignalSpy clientFinishUserMovedResizedSpy(c, &AbstractClient::clientFinishUserMovedResized); QVERIFY(clientFinishUserMovedResizedSpy.isValid()); // begin resize QCOMPARE(workspace()->moveResizeClient(), nullptr); QCOMPARE(c->isMove(), false); QCOMPARE(c->isResize(), false); workspace()->slotWindowResize(); QCOMPARE(workspace()->moveResizeClient(), c); QCOMPARE(startMoveResizedSpy.count(), 1); QCOMPARE(moveResizedChangedSpy.count(), 1); QCOMPARE(c->isResize(), true); QCOMPARE(c->geometryRestore(), QRect(0, 0, 100, 50)); QVERIFY(configureRequestedSpy.wait()); QCOMPARE(configureRequestedSpy.count(), 3); states = configureRequestedSpy.last().at(1).value(); QVERIFY(states.testFlag(XdgShellSurface::State::Activated)); QVERIFY(states.testFlag(XdgShellSurface::State::Resizing)); // Trigger a change. const QPoint cursorPos = Cursor::pos(); c->keyPressEvent(Qt::Key_Right); c->updateMoveResize(Cursor::pos()); QCOMPARE(Cursor::pos(), cursorPos + QPoint(8, 0)); // The client should receive a configure event with the new size. QVERIFY(configureRequestedSpy.wait()); QCOMPARE(configureRequestedSpy.count(), 4); states = configureRequestedSpy.last().at(1).value(); QVERIFY(states.testFlag(XdgShellSurface::State::Activated)); QVERIFY(states.testFlag(XdgShellSurface::State::Resizing)); QCOMPARE(surfaceSizeChangedSpy.count(), 1); QCOMPARE(surfaceSizeChangedSpy.last().first().toSize(), QSize(108, 50)); QCOMPARE(clientStepUserMovedResizedSpy.count(), 0); // Now render new size. shellSurface->ackConfigure(configureRequestedSpy.last().at(2).value()); Test::render(surface.data(), QSize(108, 50), Qt::blue); - QVERIFY(geometryChangedSpy.wait()); + QVERIFY(frameGeometryChangedSpy.wait()); QCOMPARE(c->frameGeometry(), QRect(0, 0, 108, 50)); QCOMPARE(clientStepUserMovedResizedSpy.count(), 1); // Go down. c->keyPressEvent(Qt::Key_Down); c->updateMoveResize(Cursor::pos()); QCOMPARE(Cursor::pos(), cursorPos + QPoint(8, 8)); // The client should receive another configure event. QVERIFY(configureRequestedSpy.wait()); QCOMPARE(configureRequestedSpy.count(), 5); states = configureRequestedSpy.last().at(1).value(); QVERIFY(states.testFlag(XdgShellSurface::State::Activated)); QVERIFY(states.testFlag(XdgShellSurface::State::Resizing)); QCOMPARE(surfaceSizeChangedSpy.count(), 2); QCOMPARE(surfaceSizeChangedSpy.last().first().toSize(), QSize(108, 58)); // Now render new size. shellSurface->ackConfigure(configureRequestedSpy.last().at(2).value()); Test::render(surface.data(), QSize(108, 58), Qt::blue); - QVERIFY(geometryChangedSpy.wait()); + QVERIFY(frameGeometryChangedSpy.wait()); QCOMPARE(c->frameGeometry(), QRect(0, 0, 108, 58)); QCOMPARE(clientStepUserMovedResizedSpy.count(), 2); // Let's finalize the resize operation. QCOMPARE(clientFinishUserMovedResizedSpy.count(), 0); c->keyPressEvent(Qt::Key_Enter); QCOMPARE(clientFinishUserMovedResizedSpy.count(), 1); QCOMPARE(moveResizedChangedSpy.count(), 2); QCOMPARE(c->isResize(), false); QCOMPARE(workspace()->moveResizeClient(), nullptr); QEXPECT_FAIL("", "XdgShellClient currently doesn't send final configure event", Abort); QVERIFY(configureRequestedSpy.wait()); QCOMPARE(configureRequestedSpy.count(), 6); states = configureRequestedSpy.last().at(1).value(); QVERIFY(states.testFlag(XdgShellSurface::State::Activated)); QVERIFY(!states.testFlag(XdgShellSurface::State::Resizing)); // Destroy the client. surface.reset(); QVERIFY(Test::waitForWindowDestroyed(c)); } void MoveResizeWindowTest::testPackTo_data() { QTest::addColumn("methodCall"); QTest::addColumn("expectedGeometry"); QTest::newRow("left") << QStringLiteral("slotWindowPackLeft") << QRect(0, 487, 100, 50); QTest::newRow("up") << QStringLiteral("slotWindowPackUp") << QRect(590, 0, 100, 50); QTest::newRow("right") << QStringLiteral("slotWindowPackRight") << QRect(1180, 487, 100, 50); QTest::newRow("down") << QStringLiteral("slotWindowPackDown") << QRect(590, 974, 100, 50); } void MoveResizeWindowTest::testPackTo() { using namespace KWayland::Client; QScopedPointer surface(Test::createSurface()); QVERIFY(!surface.isNull()); QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); QVERIFY(!shellSurface.isNull()); QSignalSpy sizeChangeSpy(shellSurface.data(), &XdgShellSurface::sizeChanged); QVERIFY(sizeChangeSpy.isValid()); // let's render auto c = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); QVERIFY(c); QCOMPARE(workspace()->activeClient(), c); QCOMPARE(c->frameGeometry(), QRect(0, 0, 100, 50)); // let's place it centered Placement::self()->placeCentered(c, QRect(0, 0, 1280, 1024)); QCOMPARE(c->frameGeometry(), QRect(590, 487, 100, 50)); QFETCH(QString, methodCall); QMetaObject::invokeMethod(workspace(), methodCall.toLocal8Bit().constData()); QTEST(c->frameGeometry(), "expectedGeometry"); surface.reset(); QVERIFY(Test::waitForWindowDestroyed(c)); } void MoveResizeWindowTest::testPackAgainstClient_data() { QTest::addColumn("methodCall"); QTest::addColumn("expectedGeometry"); QTest::newRow("left") << QStringLiteral("slotWindowPackLeft") << QRect(10, 487, 100, 50); QTest::newRow("up") << QStringLiteral("slotWindowPackUp") << QRect(590, 10, 100, 50); QTest::newRow("right") << QStringLiteral("slotWindowPackRight") << QRect(1170, 487, 100, 50); QTest::newRow("down") << QStringLiteral("slotWindowPackDown") << QRect(590, 964, 100, 50); } void MoveResizeWindowTest::testPackAgainstClient() { using namespace KWayland::Client; QScopedPointer surface1(Test::createSurface()); QVERIFY(!surface1.isNull()); QScopedPointer surface2(Test::createSurface()); QVERIFY(!surface2.isNull()); QScopedPointer surface3(Test::createSurface()); QVERIFY(!surface3.isNull()); QScopedPointer surface4(Test::createSurface()); QVERIFY(!surface4.isNull()); QScopedPointer shellSurface1(Test::createXdgShellStableSurface(surface1.data())); QVERIFY(!shellSurface1.isNull()); QScopedPointer shellSurface2(Test::createXdgShellStableSurface(surface2.data())); QVERIFY(!shellSurface2.isNull()); QScopedPointer shellSurface3(Test::createXdgShellStableSurface(surface3.data())); QVERIFY(!shellSurface3.isNull()); QScopedPointer shellSurface4(Test::createXdgShellStableSurface(surface4.data())); QVERIFY(!shellSurface4.isNull()); auto renderWindow = [this] (Surface *surface, const QString &methodCall, const QRect &expectedGeometry) { // let's render auto c = Test::renderAndWaitForShown(surface, QSize(10, 10), Qt::blue); QVERIFY(c); QCOMPARE(workspace()->activeClient(), c); QCOMPARE(c->frameGeometry().size(), QSize(10, 10)); // let's place it centered Placement::self()->placeCentered(c, QRect(0, 0, 1280, 1024)); QCOMPARE(c->frameGeometry(), QRect(635, 507, 10, 10)); QMetaObject::invokeMethod(workspace(), methodCall.toLocal8Bit().constData()); QCOMPARE(c->frameGeometry(), expectedGeometry); }; renderWindow(surface1.data(), QStringLiteral("slotWindowPackLeft"), QRect(0, 507, 10, 10)); renderWindow(surface2.data(), QStringLiteral("slotWindowPackUp"), QRect(635, 0, 10, 10)); renderWindow(surface3.data(), QStringLiteral("slotWindowPackRight"), QRect(1270, 507, 10, 10)); renderWindow(surface4.data(), QStringLiteral("slotWindowPackDown"), QRect(635, 1014, 10, 10)); QScopedPointer surface(Test::createSurface()); QVERIFY(!surface.isNull()); QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); QVERIFY(!shellSurface.isNull()); auto c = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); QVERIFY(c); QCOMPARE(workspace()->activeClient(), c); // let's place it centered Placement::self()->placeCentered(c, QRect(0, 0, 1280, 1024)); QCOMPARE(c->frameGeometry(), QRect(590, 487, 100, 50)); QFETCH(QString, methodCall); QMetaObject::invokeMethod(workspace(), methodCall.toLocal8Bit().constData()); QTEST(c->frameGeometry(), "expectedGeometry"); } void MoveResizeWindowTest::testGrowShrink_data() { QTest::addColumn("methodCall"); QTest::addColumn("expectedGeometry"); QTest::newRow("grow vertical") << QStringLiteral("slotWindowGrowVertical") << QRect(590, 487, 100, 537); QTest::newRow("grow horizontal") << QStringLiteral("slotWindowGrowHorizontal") << QRect(590, 487, 690, 50); QTest::newRow("shrink vertical") << QStringLiteral("slotWindowShrinkVertical") << QRect(590, 487, 100, 23); QTest::newRow("shrink horizontal") << QStringLiteral("slotWindowShrinkHorizontal") << QRect(590, 487, 40, 50); } void MoveResizeWindowTest::testGrowShrink() { using namespace KWayland::Client; // block geometry helper QScopedPointer surface1(Test::createSurface()); QVERIFY(!surface1.isNull()); QScopedPointer shellSurface1(Test::createXdgShellStableSurface(surface1.data())); QVERIFY(!shellSurface1.isNull()); Test::render(surface1.data(), QSize(650, 514), Qt::blue); QVERIFY(Test::waitForWaylandWindowShown()); workspace()->slotWindowPackRight(); workspace()->slotWindowPackDown(); QScopedPointer surface(Test::createSurface()); QVERIFY(!surface.isNull()); QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); QVERIFY(!shellSurface.isNull()); QSignalSpy sizeChangeSpy(shellSurface.data(), &XdgShellSurface::sizeChanged); QVERIFY(sizeChangeSpy.isValid()); // let's render auto c = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); QVERIFY(c); QCOMPARE(workspace()->activeClient(), c); // let's place it centered Placement::self()->placeCentered(c, QRect(0, 0, 1280, 1024)); QCOMPARE(c->frameGeometry(), QRect(590, 487, 100, 50)); QFETCH(QString, methodCall); QMetaObject::invokeMethod(workspace(), methodCall.toLocal8Bit().constData()); QVERIFY(sizeChangeSpy.wait()); Test::render(surface.data(), shellSurface->size(), Qt::red); - QSignalSpy geometryChangedSpy(c, &AbstractClient::geometryChanged); - QVERIFY(geometryChangedSpy.isValid()); + QSignalSpy frameGeometryChangedSpy(c, &AbstractClient::frameGeometryChanged); + QVERIFY(frameGeometryChangedSpy.isValid()); m_connection->flush(); - QVERIFY(geometryChangedSpy.wait()); + QVERIFY(frameGeometryChangedSpy.wait()); QTEST(c->frameGeometry(), "expectedGeometry"); } void MoveResizeWindowTest::testPointerMoveEnd_data() { QTest::addColumn("additionalButton"); QTest::newRow("BTN_RIGHT") << BTN_RIGHT; QTest::newRow("BTN_MIDDLE") << BTN_MIDDLE; QTest::newRow("BTN_SIDE") << BTN_SIDE; QTest::newRow("BTN_EXTRA") << BTN_EXTRA; QTest::newRow("BTN_FORWARD") << BTN_FORWARD; QTest::newRow("BTN_BACK") << BTN_BACK; QTest::newRow("BTN_TASK") << BTN_TASK; for (int i=BTN_TASK + 1; i < BTN_JOYSTICK; i++) { QTest::newRow(QByteArray::number(i, 16).constData()) << i; } } void MoveResizeWindowTest::testPointerMoveEnd() { // this test verifies that moving a window through pointer only ends if all buttons are released using namespace KWayland::Client; QScopedPointer surface(Test::createSurface()); QVERIFY(!surface.isNull()); QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); QVERIFY(!shellSurface.isNull()); QSignalSpy sizeChangeSpy(shellSurface.data(), &XdgShellSurface::sizeChanged); QVERIFY(sizeChangeSpy.isValid()); // let's render auto c = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); QVERIFY(c); QCOMPARE(c, workspace()->activeClient()); QVERIFY(!c->isMove()); // let's trigger the left button quint32 timestamp = 1; kwinApp()->platform()->pointerButtonPressed(BTN_LEFT, timestamp++); QVERIFY(!c->isMove()); workspace()->slotWindowMove(); QVERIFY(c->isMove()); // let's press another button QFETCH(int, additionalButton); kwinApp()->platform()->pointerButtonPressed(additionalButton, timestamp++); QVERIFY(c->isMove()); // release the left button, should still have the window moving kwinApp()->platform()->pointerButtonReleased(BTN_LEFT, timestamp++); QVERIFY(c->isMove()); // but releasing the other button should now end moving kwinApp()->platform()->pointerButtonReleased(additionalButton, timestamp++); QVERIFY(!c->isMove()); surface.reset(); QVERIFY(Test::waitForWindowDestroyed(c)); } void MoveResizeWindowTest::testClientSideMove_data() { QTest::addColumn("type"); QTest::newRow("xdgShellV6") << Test::XdgShellSurfaceType::XdgShellV6; QTest::newRow("xdgWmBase") << Test::XdgShellSurfaceType::XdgShellStable; } void MoveResizeWindowTest::testClientSideMove() { using namespace KWayland::Client; Cursor::setPos(640, 512); QScopedPointer pointer(Test::waylandSeat()->createPointer()); QSignalSpy pointerEnteredSpy(pointer.data(), &Pointer::entered); QVERIFY(pointerEnteredSpy.isValid()); QSignalSpy pointerLeftSpy(pointer.data(), &Pointer::left); QVERIFY(pointerLeftSpy.isValid()); QSignalSpy buttonSpy(pointer.data(), &Pointer::buttonStateChanged); QVERIFY(buttonSpy.isValid()); QScopedPointer surface(Test::createSurface()); QFETCH(Test::XdgShellSurfaceType, type); QScopedPointer shellSurface(Test::createXdgShellSurface(type, surface.data())); auto c = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); QVERIFY(c); // move pointer into center of geometry const QRect startGeometry = c->frameGeometry(); Cursor::setPos(startGeometry.center()); QVERIFY(pointerEnteredSpy.wait()); QCOMPARE(pointerEnteredSpy.first().last().toPoint(), QPoint(49, 24)); // simulate press quint32 timestamp = 1; kwinApp()->platform()->pointerButtonPressed(BTN_LEFT, timestamp++); QVERIFY(buttonSpy.wait()); QSignalSpy moveStartSpy(c, &AbstractClient::clientStartUserMovedResized); QVERIFY(moveStartSpy.isValid()); shellSurface->requestMove(Test::waylandSeat(), buttonSpy.first().first().value()); QVERIFY(moveStartSpy.wait()); QCOMPARE(c->isMove(), true); QVERIFY(pointerLeftSpy.wait()); // move a bit QSignalSpy clientMoveStepSpy(c, &AbstractClient::clientStepUserMovedResized); QVERIFY(clientMoveStepSpy.isValid()); const QPoint startPoint = startGeometry.center(); const int dragDistance = QApplication::startDragDistance(); // Why? kwinApp()->platform()->pointerMotion(startPoint + QPoint(dragDistance, dragDistance) + QPoint(6, 6), timestamp++); QCOMPARE(clientMoveStepSpy.count(), 1); // and release again kwinApp()->platform()->pointerButtonReleased(BTN_LEFT, timestamp++); QVERIFY(pointerEnteredSpy.wait()); QCOMPARE(c->isMove(), false); QCOMPARE(c->frameGeometry(), startGeometry.translated(QPoint(dragDistance, dragDistance) + QPoint(6, 6))); QCOMPARE(pointerEnteredSpy.last().last().toPoint(), QPoint(49, 24)); } void MoveResizeWindowTest::testPlasmaShellSurfaceMovable_data() { QTest::addColumn("role"); QTest::addColumn("movable"); QTest::addColumn("movableAcrossScreens"); QTest::addColumn("resizable"); QTest::newRow("normal") << KWayland::Client::PlasmaShellSurface::Role::Normal << true << true << true; QTest::newRow("desktop") << KWayland::Client::PlasmaShellSurface::Role::Desktop << false << false << false; QTest::newRow("panel") << KWayland::Client::PlasmaShellSurface::Role::Panel << false << false << false; QTest::newRow("osd") << KWayland::Client::PlasmaShellSurface::Role::OnScreenDisplay << false << false << false; } void MoveResizeWindowTest::testPlasmaShellSurfaceMovable() { // this test verifies that certain window types from PlasmaShellSurface are not moveable or resizable using namespace KWayland::Client; QScopedPointer surface(Test::createSurface()); QVERIFY(!surface.isNull()); QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); QVERIFY(!shellSurface.isNull()); // and a PlasmaShellSurface QScopedPointer plasmaSurface(Test::waylandPlasmaShell()->createSurface(surface.data())); QVERIFY(!plasmaSurface.isNull()); QFETCH(KWayland::Client::PlasmaShellSurface::Role, role); plasmaSurface->setRole(role); // let's render auto c = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); QVERIFY(c); QTEST(c->isMovable(), "movable"); QTEST(c->isMovableAcrossScreens(), "movableAcrossScreens"); QTEST(c->isResizable(), "resizable"); surface.reset(); QVERIFY(Test::waitForWindowDestroyed(c)); } struct XcbConnectionDeleter { static inline void cleanup(xcb_connection_t *pointer) { xcb_disconnect(pointer); } }; void MoveResizeWindowTest::testNetMove() { // this test verifies that a move request for an X11 window through NET API works // create an xcb window QScopedPointer c(xcb_connect(nullptr, nullptr)); QVERIFY(!xcb_connection_has_error(c.data())); xcb_window_t w = xcb_generate_id(c.data()); xcb_create_window(c.data(), XCB_COPY_FROM_PARENT, w, rootWindow(), 0, 0, 100, 100, 0, XCB_WINDOW_CLASS_INPUT_OUTPUT, XCB_COPY_FROM_PARENT, 0, nullptr); xcb_size_hints_t hints; memset(&hints, 0, sizeof(hints)); xcb_icccm_size_hints_set_position(&hints, 1, 0, 0); xcb_icccm_size_hints_set_size(&hints, 1, 100, 100); xcb_icccm_set_wm_normal_hints(c.data(), w, &hints); // let's set a no-border NETWinInfo winInfo(c.data(), w, rootWindow(), NET::WMWindowType, NET::Properties2()); winInfo.setWindowType(NET::Override); xcb_map_window(c.data(), w); xcb_flush(c.data()); QSignalSpy windowCreatedSpy(workspace(), &Workspace::clientAdded); QVERIFY(windowCreatedSpy.isValid()); QVERIFY(windowCreatedSpy.wait()); X11Client *client = windowCreatedSpy.first().first().value(); QVERIFY(client); QCOMPARE(client->window(), w); const QRect origGeo = client->frameGeometry(); // let's move the cursor outside the window Cursor::setPos(screens()->geometry(0).center()); QVERIFY(!origGeo.contains(Cursor::pos())); QSignalSpy moveStartSpy(client, &X11Client::clientStartUserMovedResized); QVERIFY(moveStartSpy.isValid()); QSignalSpy moveEndSpy(client, &X11Client::clientFinishUserMovedResized); QVERIFY(moveEndSpy.isValid()); QSignalSpy moveStepSpy(client, &X11Client::clientStepUserMovedResized); QVERIFY(moveStepSpy.isValid()); QVERIFY(!workspace()->moveResizeClient()); // use NETRootInfo to trigger a move request NETRootInfo root(c.data(), NET::Properties()); root.moveResizeRequest(w, origGeo.center().x(), origGeo.center().y(), NET::Move); xcb_flush(c.data()); QVERIFY(moveStartSpy.wait()); QCOMPARE(workspace()->moveResizeClient(), client); QVERIFY(client->isMove()); QCOMPARE(client->geometryRestore(), origGeo); QCOMPARE(Cursor::pos(), origGeo.center()); // let's move a step Cursor::setPos(Cursor::pos() + QPoint(10, 10)); QCOMPARE(moveStepSpy.count(), 1); QCOMPARE(moveStepSpy.first().last().toRect(), origGeo.translated(10, 10)); // let's cancel the move resize again through the net API root.moveResizeRequest(w, client->frameGeometry().center().x(), client->frameGeometry().center().y(), NET::MoveResizeCancel); xcb_flush(c.data()); QVERIFY(moveEndSpy.wait()); // and destroy the window again xcb_unmap_window(c.data(), w); xcb_destroy_window(c.data(), w); xcb_flush(c.data()); c.reset(); QSignalSpy windowClosedSpy(client, &X11Client::windowClosed); QVERIFY(windowClosedSpy.isValid()); QVERIFY(windowClosedSpy.wait()); } void MoveResizeWindowTest::testAdjustClientGeometryOfAutohidingX11Panel_data() { QTest::addColumn("panelGeometry"); QTest::addColumn("targetPoint"); QTest::addColumn("expectedAdjustedPoint"); QTest::addColumn("hideLocation"); QTest::newRow("top") << QRect(0, 0, 100, 20) << QPoint(50, 25) << QPoint(50, 20) << 0u; QTest::newRow("bottom") << QRect(0, 1024-20, 100, 20) << QPoint(50, 1024 - 25 - 50) << QPoint(50, 1024 - 20 - 50) << 2u; QTest::newRow("left") << QRect(0, 0, 20, 100) << QPoint(25, 50) << QPoint(20, 50) << 3u; QTest::newRow("right") << QRect(1280 - 20, 0, 20, 100) << QPoint(1280 - 25 - 100, 50) << QPoint(1280 - 20 - 100, 50) << 1u; } void MoveResizeWindowTest::testAdjustClientGeometryOfAutohidingX11Panel() { // this test verifies that auto hiding panels are ignored when adjusting client geometry // see BUG 365892 // first create our panel QScopedPointer c(xcb_connect(nullptr, nullptr)); QVERIFY(!xcb_connection_has_error(c.data())); xcb_window_t w = xcb_generate_id(c.data()); QFETCH(QRect, panelGeometry); xcb_create_window(c.data(), XCB_COPY_FROM_PARENT, w, rootWindow(), panelGeometry.x(), panelGeometry.y(), panelGeometry.width(), panelGeometry.height(), 0, XCB_WINDOW_CLASS_INPUT_OUTPUT, XCB_COPY_FROM_PARENT, 0, nullptr); xcb_size_hints_t hints; memset(&hints, 0, sizeof(hints)); xcb_icccm_size_hints_set_position(&hints, 1, panelGeometry.x(), panelGeometry.y()); xcb_icccm_size_hints_set_size(&hints, 1, panelGeometry.width(), panelGeometry.height()); xcb_icccm_set_wm_normal_hints(c.data(), w, &hints); NETWinInfo winInfo(c.data(), w, rootWindow(), NET::WMWindowType, NET::Properties2()); winInfo.setWindowType(NET::Dock); xcb_map_window(c.data(), w); xcb_flush(c.data()); QSignalSpy windowCreatedSpy(workspace(), &Workspace::clientAdded); QVERIFY(windowCreatedSpy.isValid()); QVERIFY(windowCreatedSpy.wait()); X11Client *panel = windowCreatedSpy.first().first().value(); QVERIFY(panel); QCOMPARE(panel->window(), w); QCOMPARE(panel->frameGeometry(), panelGeometry); QVERIFY(panel->isDock()); // let's create a window using namespace KWayland::Client; QScopedPointer surface(Test::createSurface()); QVERIFY(!surface.isNull()); QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); QVERIFY(!shellSurface.isNull()); auto testWindow = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); QVERIFY(testWindow); QVERIFY(testWindow->isMovable()); // panel is not yet hidden, we should snap against it QFETCH(QPoint, targetPoint); QTEST(Workspace::self()->adjustClientPosition(testWindow, targetPoint, false), "expectedAdjustedPoint"); // now let's hide the panel QSignalSpy panelHiddenSpy(panel, &AbstractClient::windowHidden); QVERIFY(panelHiddenSpy.isValid()); QFETCH(quint32, hideLocation); xcb_change_property(c.data(), XCB_PROP_MODE_REPLACE, w, atoms->kde_screen_edge_show, XCB_ATOM_CARDINAL, 32, 1, &hideLocation); xcb_flush(c.data()); QVERIFY(panelHiddenSpy.wait()); // now try to snap again QCOMPARE(Workspace::self()->adjustClientPosition(testWindow, targetPoint, false), targetPoint); // and destroy the panel again xcb_unmap_window(c.data(), w); xcb_destroy_window(c.data(), w); xcb_flush(c.data()); c.reset(); QSignalSpy panelClosedSpy(panel, &X11Client::windowClosed); QVERIFY(panelClosedSpy.isValid()); QVERIFY(panelClosedSpy.wait()); // snap once more QCOMPARE(Workspace::self()->adjustClientPosition(testWindow, targetPoint, false), targetPoint); // and close QSignalSpy windowClosedSpy(testWindow, &XdgShellClient::windowClosed); QVERIFY(windowClosedSpy.isValid()); shellSurface.reset(); surface.reset(); QVERIFY(windowClosedSpy.wait()); } void MoveResizeWindowTest::testAdjustClientGeometryOfAutohidingWaylandPanel_data() { QTest::addColumn("panelGeometry"); QTest::addColumn("targetPoint"); QTest::addColumn("expectedAdjustedPoint"); QTest::newRow("top") << QRect(0, 0, 100, 20) << QPoint(50, 25) << QPoint(50, 20); QTest::newRow("bottom") << QRect(0, 1024-20, 100, 20) << QPoint(50, 1024 - 25 - 50) << QPoint(50, 1024 - 20 - 50); QTest::newRow("left") << QRect(0, 0, 20, 100) << QPoint(25, 50) << QPoint(20, 50); QTest::newRow("right") << QRect(1280 - 20, 0, 20, 100) << QPoint(1280 - 25 - 100, 50) << QPoint(1280 - 20 - 100, 50); } void MoveResizeWindowTest::testAdjustClientGeometryOfAutohidingWaylandPanel() { // this test verifies that auto hiding panels are ignored when adjusting client geometry // see BUG 365892 // first create our panel using namespace KWayland::Client; QScopedPointer panelSurface(Test::createSurface()); QVERIFY(!panelSurface.isNull()); QScopedPointer panelShellSurface(Test::createXdgShellStableSurface(panelSurface.data())); QVERIFY(!panelShellSurface.isNull()); QScopedPointer plasmaSurface(Test::waylandPlasmaShell()->createSurface(panelSurface.data())); QVERIFY(!plasmaSurface.isNull()); plasmaSurface->setRole(PlasmaShellSurface::Role::Panel); plasmaSurface->setPanelBehavior(PlasmaShellSurface::PanelBehavior::AutoHide); QFETCH(QRect, panelGeometry); plasmaSurface->setPosition(panelGeometry.topLeft()); // let's render auto panel = Test::renderAndWaitForShown(panelSurface.data(), panelGeometry.size(), Qt::blue); QVERIFY(panel); QCOMPARE(panel->frameGeometry(), panelGeometry); QVERIFY(panel->isDock()); // let's create a window QScopedPointer surface(Test::createSurface()); QVERIFY(!surface.isNull()); QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); QVERIFY(!shellSurface.isNull()); auto testWindow = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); QVERIFY(testWindow); QVERIFY(testWindow->isMovable()); // panel is not yet hidden, we should snap against it QFETCH(QPoint, targetPoint); QTEST(Workspace::self()->adjustClientPosition(testWindow, targetPoint, false), "expectedAdjustedPoint"); // now let's hide the panel QSignalSpy panelHiddenSpy(panel, &AbstractClient::windowHidden); QVERIFY(panelHiddenSpy.isValid()); plasmaSurface->requestHideAutoHidingPanel(); QVERIFY(panelHiddenSpy.wait()); // now try to snap again QCOMPARE(Workspace::self()->adjustClientPosition(testWindow, targetPoint, false), targetPoint); // and destroy the panel again QSignalSpy panelClosedSpy(panel, &XdgShellClient::windowClosed); QVERIFY(panelClosedSpy.isValid()); plasmaSurface.reset(); panelShellSurface.reset(); panelSurface.reset(); QVERIFY(panelClosedSpy.wait()); // snap once more QCOMPARE(Workspace::self()->adjustClientPosition(testWindow, targetPoint, false), targetPoint); // and close QSignalSpy windowClosedSpy(testWindow, &XdgShellClient::windowClosed); QVERIFY(windowClosedSpy.isValid()); shellSurface.reset(); surface.reset(); QVERIFY(windowClosedSpy.wait()); } void MoveResizeWindowTest::testResizeForVirtualKeyboard() { using namespace KWayland::Client; QScopedPointer surface(Test::createSurface()); QVERIFY(!surface.isNull()); QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); QVERIFY(!shellSurface.isNull()); // let's render auto client = Test::renderAndWaitForShown(surface.data(), QSize(500, 800), Qt::blue); QVERIFY(client); // The client should receive a configure event upon becoming active. QSignalSpy configureRequestedSpy(shellSurface.data(), &XdgShellSurface::configureRequested); QVERIFY(configureRequestedSpy.isValid()); QVERIFY(configureRequestedSpy.wait()); client->move(100, 300); - QSignalSpy geometryChangedSpy(client, &XdgShellClient::geometryChanged); - QVERIFY(geometryChangedSpy.isValid()); + QSignalSpy frameGeometryChangedSpy(client, &XdgShellClient::frameGeometryChanged); + QVERIFY(frameGeometryChangedSpy.isValid()); QCOMPARE(client->frameGeometry(), QRect(100, 300, 500, 800)); client->setVirtualKeyboardGeometry(QRect(0, 100, 1280, 500)); QVERIFY(configureRequestedSpy.wait()); shellSurface->ackConfigure(configureRequestedSpy.last()[2].toInt()); // render at the new size Test::render(surface.data(), configureRequestedSpy.last().first().toSize(), Qt::blue); - QVERIFY(geometryChangedSpy.wait()); + QVERIFY(frameGeometryChangedSpy.wait()); QCOMPARE(client->frameGeometry(), QRect(100, 0, 500, 101)); client->setVirtualKeyboardGeometry(QRect()); QVERIFY(configureRequestedSpy.wait()); shellSurface->ackConfigure(configureRequestedSpy.last()[2].toInt()); // render at the new size Test::render(surface.data(), configureRequestedSpy.last().first().toSize(), Qt::blue); - QVERIFY(geometryChangedSpy.wait()); + QVERIFY(frameGeometryChangedSpy.wait()); QCOMPARE(client->frameGeometry(), QRect(100, 300, 500, 800)); } void MoveResizeWindowTest::testResizeForVirtualKeyboardWithMaximize() { using namespace KWayland::Client; QScopedPointer surface(Test::createSurface()); QVERIFY(!surface.isNull()); QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); QVERIFY(!shellSurface.isNull()); // let's render auto client = Test::renderAndWaitForShown(surface.data(), QSize(500, 800), Qt::blue); QVERIFY(client); // The client should receive a configure event upon becoming active. QSignalSpy configureRequestedSpy(shellSurface.data(), &XdgShellSurface::configureRequested); QVERIFY(configureRequestedSpy.isValid()); QVERIFY(configureRequestedSpy.wait()); client->move(100, 300); - QSignalSpy geometryChangedSpy(client, &XdgShellClient::geometryChanged); - QVERIFY(geometryChangedSpy.isValid()); + QSignalSpy frameGeometryChangedSpy(client, &XdgShellClient::frameGeometryChanged); + QVERIFY(frameGeometryChangedSpy.isValid()); QCOMPARE(client->frameGeometry(), QRect(100, 300, 500, 800)); client->setVirtualKeyboardGeometry(QRect(0, 100, 1280, 500)); QVERIFY(configureRequestedSpy.wait()); shellSurface->ackConfigure(configureRequestedSpy.last()[2].toInt()); // render at the new size Test::render(surface.data(), configureRequestedSpy.last().first().toSize(), Qt::blue); - QVERIFY(geometryChangedSpy.wait()); + QVERIFY(frameGeometryChangedSpy.wait()); QCOMPARE(client->frameGeometry(), QRect(100, 0, 500, 101)); client->setMaximize(true, true); QVERIFY(configureRequestedSpy.wait()); shellSurface->ackConfigure(configureRequestedSpy.last()[2].toInt()); Test::render(surface.data(), configureRequestedSpy.last().first().toSize(), Qt::blue); - QVERIFY(geometryChangedSpy.wait()); + QVERIFY(frameGeometryChangedSpy.wait()); QCOMPARE(client->frameGeometry(), QRect(0, 0, 1280, 1024)); client->setVirtualKeyboardGeometry(QRect()); QVERIFY(!configureRequestedSpy.wait(10)); // render at the size of the configureRequested.. it won't have changed Test::render(surface.data(), configureRequestedSpy.last().first().toSize(), Qt::blue); - QVERIFY(!geometryChangedSpy.wait(10)); + QVERIFY(!frameGeometryChangedSpy.wait(10)); // Size will NOT be restored QCOMPARE(client->frameGeometry(), QRect(0, 0, 1280, 1024)); } void MoveResizeWindowTest::testResizeForVirtualKeyboardWithFullScreen() { using namespace KWayland::Client; QScopedPointer surface(Test::createSurface()); QVERIFY(!surface.isNull()); QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); QVERIFY(!shellSurface.isNull()); // let's render auto client = Test::renderAndWaitForShown(surface.data(), QSize(500, 800), Qt::blue); QVERIFY(client); // The client should receive a configure event upon becoming active. QSignalSpy configureRequestedSpy(shellSurface.data(), &XdgShellSurface::configureRequested); QVERIFY(configureRequestedSpy.isValid()); QVERIFY(configureRequestedSpy.wait()); client->move(100, 300); - QSignalSpy geometryChangedSpy(client, &XdgShellClient::geometryChanged); - QVERIFY(geometryChangedSpy.isValid()); + QSignalSpy frameGeometryChangedSpy(client, &XdgShellClient::frameGeometryChanged); + QVERIFY(frameGeometryChangedSpy.isValid()); QCOMPARE(client->frameGeometry(), QRect(100, 300, 500, 800)); client->setVirtualKeyboardGeometry(QRect(0, 100, 1280, 500)); QVERIFY(configureRequestedSpy.wait()); shellSurface->ackConfigure(configureRequestedSpy.last()[2].toInt()); // render at the new size Test::render(surface.data(), configureRequestedSpy.last().first().toSize(), Qt::blue); - QVERIFY(geometryChangedSpy.wait()); + QVERIFY(frameGeometryChangedSpy.wait()); QCOMPARE(client->frameGeometry(), QRect(100, 0, 500, 101)); client->setFullScreen(true, true); QVERIFY(configureRequestedSpy.wait()); shellSurface->ackConfigure(configureRequestedSpy.last()[2].toInt()); Test::render(surface.data(), configureRequestedSpy.last().first().toSize(), Qt::blue); - QVERIFY(geometryChangedSpy.wait()); + QVERIFY(frameGeometryChangedSpy.wait()); QCOMPARE(client->frameGeometry(), QRect(0, 0, 1280, 1024)); client->setVirtualKeyboardGeometry(QRect()); QVERIFY(!configureRequestedSpy.wait(10)); // render at the size of the configureRequested.. it won't have changed Test::render(surface.data(), configureRequestedSpy.last().first().toSize(), Qt::blue); - QVERIFY(!geometryChangedSpy.wait(10)); + QVERIFY(!frameGeometryChangedSpy.wait(10)); // Size will NOT be restored QCOMPARE(client->frameGeometry(), QRect(0, 0, 1280, 1024)); } void MoveResizeWindowTest::testDestroyMoveClient() { // This test verifies that active move operation gets finished when // the associated client is destroyed. // Create the test client. using namespace KWayland::Client; QScopedPointer surface(Test::createSurface()); QVERIFY(!surface.isNull()); QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); QVERIFY(!shellSurface.isNull()); XdgShellClient *client = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); QVERIFY(client); // Start moving the client. QSignalSpy clientStartMoveResizedSpy(client, &AbstractClient::clientStartUserMovedResized); QVERIFY(clientStartMoveResizedSpy.isValid()); QSignalSpy clientFinishUserMovedResizedSpy(client, &AbstractClient::clientFinishUserMovedResized); QVERIFY(clientFinishUserMovedResizedSpy.isValid()); QCOMPARE(workspace()->moveResizeClient(), nullptr); QCOMPARE(client->isMove(), false); QCOMPARE(client->isResize(), false); workspace()->slotWindowMove(); QCOMPARE(clientStartMoveResizedSpy.count(), 1); QCOMPARE(workspace()->moveResizeClient(), client); QCOMPARE(client->isMove(), true); QCOMPARE(client->isResize(), false); // Let's pretend that the client crashed. shellSurface.reset(); surface.reset(); QVERIFY(Test::waitForWindowDestroyed(client)); QCOMPARE(clientFinishUserMovedResizedSpy.count(), 0); QCOMPARE(workspace()->moveResizeClient(), nullptr); } void MoveResizeWindowTest::testDestroyResizeClient() { // This test verifies that active resize operation gets finished when // the associated client is destroyed. // Create the test client. using namespace KWayland::Client; QScopedPointer surface(Test::createSurface()); QVERIFY(!surface.isNull()); QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); QVERIFY(!shellSurface.isNull()); XdgShellClient *client = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); QVERIFY(client); // Start resizing the client. QSignalSpy clientStartMoveResizedSpy(client, &AbstractClient::clientStartUserMovedResized); QVERIFY(clientStartMoveResizedSpy.isValid()); QSignalSpy clientFinishUserMovedResizedSpy(client, &AbstractClient::clientFinishUserMovedResized); QVERIFY(clientFinishUserMovedResizedSpy.isValid()); QCOMPARE(workspace()->moveResizeClient(), nullptr); QCOMPARE(client->isMove(), false); QCOMPARE(client->isResize(), false); workspace()->slotWindowResize(); QCOMPARE(clientStartMoveResizedSpy.count(), 1); QCOMPARE(workspace()->moveResizeClient(), client); QCOMPARE(client->isMove(), false); QCOMPARE(client->isResize(), true); // Let's pretend that the client crashed. shellSurface.reset(); surface.reset(); QVERIFY(Test::waitForWindowDestroyed(client)); QCOMPARE(clientFinishUserMovedResizedSpy.count(), 0); QCOMPARE(workspace()->moveResizeClient(), nullptr); } void MoveResizeWindowTest::testUnmapMoveClient() { // This test verifies that active move operation gets cancelled when // the associated client is unmapped. // Create the test client. using namespace KWayland::Client; QScopedPointer surface(Test::createSurface()); QVERIFY(!surface.isNull()); QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); QVERIFY(!shellSurface.isNull()); XdgShellClient *client = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); QVERIFY(client); // Start resizing the client. QSignalSpy clientStartMoveResizedSpy(client, &AbstractClient::clientStartUserMovedResized); QVERIFY(clientStartMoveResizedSpy.isValid()); QSignalSpy clientFinishUserMovedResizedSpy(client, &AbstractClient::clientFinishUserMovedResized); QVERIFY(clientFinishUserMovedResizedSpy.isValid()); QCOMPARE(workspace()->moveResizeClient(), nullptr); QCOMPARE(client->isMove(), false); QCOMPARE(client->isResize(), false); workspace()->slotWindowMove(); QCOMPARE(clientStartMoveResizedSpy.count(), 1); QCOMPARE(workspace()->moveResizeClient(), client); QCOMPARE(client->isMove(), true); QCOMPARE(client->isResize(), false); // Unmap the client while we're moving it. QSignalSpy hiddenSpy(client, &XdgShellClient::windowHidden); QVERIFY(hiddenSpy.isValid()); surface->attachBuffer(Buffer::Ptr()); surface->commit(Surface::CommitFlag::None); QVERIFY(hiddenSpy.wait()); QCOMPARE(clientFinishUserMovedResizedSpy.count(), 0); QCOMPARE(workspace()->moveResizeClient(), nullptr); QCOMPARE(client->isMove(), false); QCOMPARE(client->isResize(), false); // Destroy the client. shellSurface.reset(); QVERIFY(Test::waitForWindowDestroyed(client)); QCOMPARE(clientFinishUserMovedResizedSpy.count(), 0); } void MoveResizeWindowTest::testUnmapResizeClient() { // This test verifies that active resize operation gets cancelled when // the associated client is unmapped. // Create the test client. using namespace KWayland::Client; QScopedPointer surface(Test::createSurface()); QVERIFY(!surface.isNull()); QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); QVERIFY(!shellSurface.isNull()); XdgShellClient *client = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); QVERIFY(client); // Start resizing the client. QSignalSpy clientStartMoveResizedSpy(client, &AbstractClient::clientStartUserMovedResized); QVERIFY(clientStartMoveResizedSpy.isValid()); QSignalSpy clientFinishUserMovedResizedSpy(client, &AbstractClient::clientFinishUserMovedResized); QVERIFY(clientFinishUserMovedResizedSpy.isValid()); QCOMPARE(workspace()->moveResizeClient(), nullptr); QCOMPARE(client->isMove(), false); QCOMPARE(client->isResize(), false); workspace()->slotWindowResize(); QCOMPARE(clientStartMoveResizedSpy.count(), 1); QCOMPARE(workspace()->moveResizeClient(), client); QCOMPARE(client->isMove(), false); QCOMPARE(client->isResize(), true); // Unmap the client while we're resizing it. QSignalSpy hiddenSpy(client, &XdgShellClient::windowHidden); QVERIFY(hiddenSpy.isValid()); surface->attachBuffer(Buffer::Ptr()); surface->commit(Surface::CommitFlag::None); QVERIFY(hiddenSpy.wait()); QCOMPARE(clientFinishUserMovedResizedSpy.count(), 0); QCOMPARE(workspace()->moveResizeClient(), nullptr); QCOMPARE(client->isMove(), false); QCOMPARE(client->isResize(), false); // Destroy the client. shellSurface.reset(); QVERIFY(Test::waitForWindowDestroyed(client)); QCOMPARE(clientFinishUserMovedResizedSpy.count(), 0); } } WAYLANDTEST_MAIN(KWin::MoveResizeWindowTest) #include "move_resize_window_test.moc" diff --git a/autotests/integration/plasma_surface_test.cpp b/autotests/integration/plasma_surface_test.cpp index faf2e905e..60aef48fc 100644 --- a/autotests/integration/plasma_surface_test.cpp +++ b/autotests/integration/plasma_surface_test.cpp @@ -1,439 +1,439 @@ /******************************************************************** 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 "xdgshellclient.h" #include "screens.h" #include "wayland_server.h" #include "workspace.h" #include #include #include #include #include #include #include using namespace KWin; using namespace KWayland::Client; Q_DECLARE_METATYPE(KWin::Layer) static const QString s_socketName = QStringLiteral("wayland_test_kwin_plasma_surface-0"); class PlasmaSurfaceTest : public QObject { Q_OBJECT private Q_SLOTS: void initTestCase(); void init(); void cleanup(); void testRoleOnAllDesktops_data(); void testRoleOnAllDesktops(); void testAcceptsFocus_data(); void testAcceptsFocus(); void testDesktopIsOpaque(); void testPanelWindowsCanCover_data(); void testPanelWindowsCanCover(); void testOSDPlacement(); void testOSDPlacementManualPosition_data(); void testOSDPlacementManualPosition(); void testPanelTypeHasStrut_data(); void testPanelTypeHasStrut(); void testPanelActivate_data(); void testPanelActivate(); private: KWayland::Client::Compositor *m_compositor = nullptr; PlasmaShell *m_plasmaShell = nullptr; }; void PlasmaSurfaceTest::initTestCase() { qRegisterMetaType(); QSignalSpy workspaceCreatedSpy(kwinApp(), &Application::workspaceCreated); QVERIFY(workspaceCreatedSpy.isValid()); kwinApp()->platform()->setInitialWindowSize(QSize(1280, 1024)); QVERIFY(waylandServer()->init(s_socketName.toLocal8Bit())); kwinApp()->start(); QVERIFY(workspaceCreatedSpy.wait()); } void PlasmaSurfaceTest::init() { QVERIFY(Test::setupWaylandConnection(Test::AdditionalWaylandInterface::PlasmaShell)); m_compositor = Test::waylandCompositor(); m_plasmaShell = Test::waylandPlasmaShell(); KWin::Cursor::setPos(640, 512); } void PlasmaSurfaceTest::cleanup() { Test::destroyWaylandConnection(); } void PlasmaSurfaceTest::testRoleOnAllDesktops_data() { QTest::addColumn("role"); QTest::addColumn("expectedOnAllDesktops"); QTest::newRow("Desktop") << PlasmaShellSurface::Role::Desktop << true; QTest::newRow("Panel") << PlasmaShellSurface::Role::Panel << true; QTest::newRow("OSD") << PlasmaShellSurface::Role::OnScreenDisplay << true; QTest::newRow("Normal") << PlasmaShellSurface::Role::Normal << false; QTest::newRow("Notification") << PlasmaShellSurface::Role::Notification << true; QTest::newRow("ToolTip") << PlasmaShellSurface::Role::ToolTip << true; QTest::newRow("CriticalNotification") << PlasmaShellSurface::Role::CriticalNotification << true; } void PlasmaSurfaceTest::testRoleOnAllDesktops() { // this test verifies that a XdgShellClient is set on all desktops when the role changes QScopedPointer surface(Test::createSurface()); QVERIFY(!surface.isNull()); QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); QVERIFY(!shellSurface.isNull()); QScopedPointer plasmaSurface(m_plasmaShell->createSurface(surface.data())); QVERIFY(!plasmaSurface.isNull()); // now render to map the window AbstractClient *c = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); QVERIFY(c); QCOMPARE(workspace()->activeClient(), c); // currently the role is not yet set, so the window should not be on all desktops QCOMPARE(c->isOnAllDesktops(), false); // now let's try to change that QSignalSpy onAllDesktopsSpy(c, &AbstractClient::desktopChanged); QVERIFY(onAllDesktopsSpy.isValid()); QFETCH(PlasmaShellSurface::Role, role); plasmaSurface->setRole(role); QFETCH(bool, expectedOnAllDesktops); QCOMPARE(onAllDesktopsSpy.wait(), expectedOnAllDesktops); QCOMPARE(c->isOnAllDesktops(), expectedOnAllDesktops); // let's create a second window where we init a little bit different // first creating the PlasmaSurface then the Shell Surface QScopedPointer surface2(Test::createSurface()); QVERIFY(!surface2.isNull()); QScopedPointer plasmaSurface2(m_plasmaShell->createSurface(surface2.data())); QVERIFY(!plasmaSurface2.isNull()); plasmaSurface2->setRole(role); QScopedPointer shellSurface2(Test::createXdgShellStableSurface(surface2.data())); QVERIFY(!shellSurface2.isNull()); auto c2 = Test::renderAndWaitForShown(surface2.data(), QSize(100, 50), Qt::blue); QVERIFY(c2); QVERIFY(c != c2); QCOMPARE(c2->isOnAllDesktops(), expectedOnAllDesktops); } void PlasmaSurfaceTest::testAcceptsFocus_data() { QTest::addColumn("role"); QTest::addColumn("wantsInput"); QTest::addColumn("active"); QTest::newRow("Desktop") << PlasmaShellSurface::Role::Desktop << true << true; QTest::newRow("Panel") << PlasmaShellSurface::Role::Panel << true << false; QTest::newRow("OSD") << PlasmaShellSurface::Role::OnScreenDisplay << false << false; QTest::newRow("Normal") << PlasmaShellSurface::Role::Normal << true << true; QTest::newRow("Notification") << PlasmaShellSurface::Role::Notification << false << false; QTest::newRow("ToolTip") << PlasmaShellSurface::Role::ToolTip << false << false; QTest::newRow("CriticalNotification") << PlasmaShellSurface::Role::CriticalNotification << false << false; } void PlasmaSurfaceTest::testAcceptsFocus() { // this test verifies that some surface roles don't get focus QScopedPointer surface(Test::createSurface()); QVERIFY(!surface.isNull()); QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); QVERIFY(!shellSurface.isNull()); QScopedPointer plasmaSurface(m_plasmaShell->createSurface(surface.data())); QVERIFY(!plasmaSurface.isNull()); QFETCH(PlasmaShellSurface::Role, role); plasmaSurface->setRole(role); // now render to map the window auto c = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); QVERIFY(c); QTEST(c->wantsInput(), "wantsInput"); QTEST(c->isActive(), "active"); } void PlasmaSurfaceTest::testDesktopIsOpaque() { QScopedPointer surface(Test::createSurface()); QVERIFY(!surface.isNull()); QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); QVERIFY(!shellSurface.isNull()); QScopedPointer plasmaSurface(m_plasmaShell->createSurface(surface.data())); QVERIFY(!plasmaSurface.isNull()); plasmaSurface->setRole(PlasmaShellSurface::Role::Desktop); // now render to map the window auto c = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); QVERIFY(c); QCOMPARE(c->windowType(), NET::Desktop); QVERIFY(c->isDesktop()); QVERIFY(!c->hasAlpha()); QCOMPARE(c->depth(), 24); } void PlasmaSurfaceTest::testOSDPlacement() { QScopedPointer surface(Test::createSurface()); QVERIFY(!surface.isNull()); QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); QVERIFY(!shellSurface.isNull()); QScopedPointer plasmaSurface(m_plasmaShell->createSurface(surface.data())); QVERIFY(!plasmaSurface.isNull()); plasmaSurface->setRole(PlasmaShellSurface::Role::OnScreenDisplay); // now render and map the window auto c = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); QVERIFY(c); QCOMPARE(c->windowType(), NET::OnScreenDisplay); QVERIFY(c->isOnScreenDisplay()); QCOMPARE(c->frameGeometry(), QRect(590, 649, 100, 50)); // change the screen size QSignalSpy screensChangedSpy(screens(), &Screens::changed); QVERIFY(screensChangedSpy.isValid()); const QVector geometries{QRect(0, 0, 1280, 1024), QRect(1280, 0, 1280, 1024)}; QMetaObject::invokeMethod(kwinApp()->platform(), "setVirtualOutputs", Qt::DirectConnection, Q_ARG(int, 2), Q_ARG(QVector, geometries)); QVERIFY(screensChangedSpy.wait()); QCOMPARE(screensChangedSpy.count(), 1); QCOMPARE(screens()->count(), 2); QCOMPARE(screens()->geometry(0), geometries.at(0)); QCOMPARE(screens()->geometry(1), geometries.at(1)); QCOMPARE(c->frameGeometry(), QRect(590, 649, 100, 50)); // change size of window - QSignalSpy geometryChangedSpy(c, &AbstractClient::geometryShapeChanged); - QVERIFY(geometryChangedSpy.isValid()); + QSignalSpy frameGeometryChangedSpy(c, &AbstractClient::frameGeometryChanged); + QVERIFY(frameGeometryChangedSpy.isValid()); Test::render(surface.data(), QSize(200, 100), Qt::red); - QVERIFY(geometryChangedSpy.wait()); + QVERIFY(frameGeometryChangedSpy.wait()); QCOMPARE(c->frameGeometry(), QRect(540, 616, 200, 100)); } void PlasmaSurfaceTest::testOSDPlacementManualPosition_data() { QTest::addColumn("type"); QTest::newRow("xdgv6") << Test::XdgShellSurfaceType::XdgShellV6; QTest::newRow("xdgWmBase") << Test::XdgShellSurfaceType::XdgShellStable; } void PlasmaSurfaceTest::testOSDPlacementManualPosition() { QScopedPointer surface(Test::createSurface()); QVERIFY(!surface.isNull()); QScopedPointer plasmaSurface(m_plasmaShell->createSurface(surface.data())); QVERIFY(!plasmaSurface.isNull()); plasmaSurface->setRole(PlasmaShellSurface::Role::OnScreenDisplay); plasmaSurface->setPosition(QPoint(50, 70)); QFETCH(Test::XdgShellSurfaceType, type); QScopedPointer shellSurface(Test::createXdgShellSurface(type, surface.data())); QVERIFY(!shellSurface.isNull()); // now render and map the window auto c = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); QVERIFY(c); QVERIFY(c->isInitialPositionSet()); QCOMPARE(c->windowType(), NET::OnScreenDisplay); QVERIFY(c->isOnScreenDisplay()); QCOMPARE(c->frameGeometry(), QRect(50, 70, 100, 50)); } void PlasmaSurfaceTest::testPanelTypeHasStrut_data() { QTest::addColumn("type"); QTest::addColumn("panelBehavior"); QTest::addColumn("expectedStrut"); QTest::addColumn("expectedMaxArea"); QTest::addColumn("expectedLayer"); QTest::newRow("always visible - xdgShellV6") << Test::XdgShellSurfaceType::XdgShellV6 << PlasmaShellSurface::PanelBehavior::AlwaysVisible << true << QRect(0, 50, 1280, 974) << KWin::DockLayer; QTest::newRow("always visible - xdgWmBase") << Test::XdgShellSurfaceType::XdgShellStable << PlasmaShellSurface::PanelBehavior::AlwaysVisible << true << QRect(0, 50, 1280, 974) << KWin::DockLayer; QTest::newRow("autohide - xdgShellV6") << Test::XdgShellSurfaceType::XdgShellV6 << PlasmaShellSurface::PanelBehavior::AutoHide << false << QRect(0, 0, 1280, 1024) << KWin::AboveLayer; QTest::newRow("autohide - xdgWmBase") << Test::XdgShellSurfaceType::XdgShellStable << PlasmaShellSurface::PanelBehavior::AutoHide << false << QRect(0, 0, 1280, 1024) << KWin::AboveLayer; QTest::newRow("windows can cover - xdgShellV6") << Test::XdgShellSurfaceType::XdgShellV6 << PlasmaShellSurface::PanelBehavior::WindowsCanCover << false << QRect(0, 0, 1280, 1024) << KWin::NormalLayer; QTest::newRow("windows can cover - xdgWmBase") << Test::XdgShellSurfaceType::XdgShellStable << PlasmaShellSurface::PanelBehavior::WindowsCanCover << false << QRect(0, 0, 1280, 1024) << KWin::NormalLayer; QTest::newRow("windows go below - xdgShellV6") << Test::XdgShellSurfaceType::XdgShellV6 << PlasmaShellSurface::PanelBehavior::WindowsGoBelow << false << QRect(0, 0, 1280, 1024) << KWin::DockLayer; QTest::newRow("windows go below - xdgWmBase") << Test::XdgShellSurfaceType::XdgShellStable << PlasmaShellSurface::PanelBehavior::WindowsGoBelow << false << QRect(0, 0, 1280, 1024) << KWin::DockLayer; } void PlasmaSurfaceTest::testPanelTypeHasStrut() { QScopedPointer surface(Test::createSurface()); QVERIFY(!surface.isNull()); QFETCH(Test::XdgShellSurfaceType, type); QScopedPointer shellSurface(Test::createXdgShellSurface(type, surface.data())); QVERIFY(!shellSurface.isNull()); QScopedPointer plasmaSurface(m_plasmaShell->createSurface(surface.data())); QVERIFY(!plasmaSurface.isNull()); plasmaSurface->setRole(PlasmaShellSurface::Role::Panel); plasmaSurface->setPosition(QPoint(0, 0)); QFETCH(PlasmaShellSurface::PanelBehavior, panelBehavior); plasmaSurface->setPanelBehavior(panelBehavior); // now render and map the window auto c = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); QVERIFY(c); QCOMPARE(c->windowType(), NET::Dock); QVERIFY(c->isDock()); QCOMPARE(c->frameGeometry(), QRect(0, 0, 100, 50)); QTEST(c->hasStrut(), "expectedStrut"); QTEST(workspace()->clientArea(MaximizeArea, 0, 0), "expectedMaxArea"); QTEST(c->layer(), "expectedLayer"); } void PlasmaSurfaceTest::testPanelWindowsCanCover_data() { QTest::addColumn("panelGeometry"); QTest::addColumn("windowGeometry"); QTest::addColumn("triggerPoint"); QTest::newRow("top-full-edge") << QRect(0, 0, 1280, 30) << QRect(0, 0, 200, 300) << QPoint(100, 0); QTest::newRow("top-left-edge") << QRect(0, 0, 1000, 30) << QRect(0, 0, 200, 300) << QPoint(100, 0); QTest::newRow("top-right-edge") << QRect(280, 0, 1000, 30) << QRect(1000, 0, 200, 300) << QPoint(1000, 0); QTest::newRow("bottom-full-edge") << QRect(0, 994, 1280, 30) << QRect(0, 724, 200, 300) << QPoint(100, 1023); QTest::newRow("bottom-left-edge") << QRect(0, 994, 1000, 30) << QRect(0, 724, 200, 300) << QPoint(100, 1023); QTest::newRow("bottom-right-edge") << QRect(280, 994, 1000, 30) << QRect(1000, 724, 200, 300) << QPoint(1000, 1023); QTest::newRow("left-full-edge") << QRect(0, 0, 30, 1024) << QRect(0, 0, 200, 300) << QPoint(0, 100); QTest::newRow("left-top-edge") << QRect(0, 0, 30, 800) << QRect(0, 0, 200, 300) << QPoint(0, 100); QTest::newRow("left-bottom-edge") << QRect(0, 200, 30, 824) << QRect(0, 0, 200, 300) << QPoint(0, 250); QTest::newRow("right-full-edge") << QRect(1250, 0, 30, 1024) << QRect(1080, 0, 200, 300) << QPoint(1279, 100); QTest::newRow("right-top-edge") << QRect(1250, 0, 30, 800) << QRect(1080, 0, 200, 300) << QPoint(1279, 100); QTest::newRow("right-bottom-edge") << QRect(1250, 200, 30, 824) << QRect(1080, 0, 200, 300) << QPoint(1279, 250); } void PlasmaSurfaceTest::testPanelWindowsCanCover() { // this test verifies the behavior of a panel with windows can cover // triggering the screen edge should raise the panel. QScopedPointer surface(Test::createSurface()); QVERIFY(!surface.isNull()); QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); QVERIFY(!shellSurface.isNull()); QScopedPointer plasmaSurface(m_plasmaShell->createSurface(surface.data())); QVERIFY(!plasmaSurface.isNull()); plasmaSurface->setRole(PlasmaShellSurface::Role::Panel); QFETCH(QRect, panelGeometry); plasmaSurface->setPosition(panelGeometry.topLeft()); plasmaSurface->setPanelBehavior(PlasmaShellSurface::PanelBehavior::WindowsCanCover); // now render and map the window auto panel = Test::renderAndWaitForShown(surface.data(), panelGeometry.size(), Qt::blue); QVERIFY(panel); QCOMPARE(panel->windowType(), NET::Dock); QVERIFY(panel->isDock()); QCOMPARE(panel->frameGeometry(), panelGeometry); QCOMPARE(panel->hasStrut(), false); QCOMPARE(workspace()->clientArea(MaximizeArea, 0, 0), QRect(0, 0, 1280, 1024)); QCOMPARE(panel->layer(), KWin::NormalLayer); // create a Window QScopedPointer surface2(Test::createSurface()); QVERIFY(!surface2.isNull()); QScopedPointer shellSurface2(Test::createXdgShellStableSurface(surface2.data())); QVERIFY(!shellSurface2.isNull()); QFETCH(QRect, windowGeometry); auto c = Test::renderAndWaitForShown(surface2.data(), windowGeometry.size(), Qt::red); QVERIFY(c); QCOMPARE(c->windowType(), NET::Normal); QVERIFY(c->isActive()); QCOMPARE(c->layer(), KWin::NormalLayer); c->move(windowGeometry.topLeft()); QCOMPARE(c->frameGeometry(), windowGeometry); auto stackingOrder = workspace()->stackingOrder(); QCOMPARE(stackingOrder.count(), 2); QCOMPARE(stackingOrder.first(), panel); QCOMPARE(stackingOrder.last(), c); QSignalSpy stackingOrderChangedSpy(workspace(), &Workspace::stackingOrderChanged); QVERIFY(stackingOrderChangedSpy.isValid()); // trigger screenedge QFETCH(QPoint, triggerPoint); KWin::Cursor::setPos(triggerPoint); QCOMPARE(stackingOrderChangedSpy.count(), 1); stackingOrder = workspace()->stackingOrder(); QCOMPARE(stackingOrder.count(), 2); QCOMPARE(stackingOrder.first(), c); QCOMPARE(stackingOrder.last(), panel); } void PlasmaSurfaceTest::testPanelActivate_data() { QTest::addColumn("wantsFocus"); QTest::addColumn("active"); QTest::newRow("no focus") << false << false; QTest::newRow("focus") << true << true; } void PlasmaSurfaceTest::testPanelActivate() { QScopedPointer surface(Test::createSurface()); QVERIFY(!surface.isNull()); QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); QVERIFY(!shellSurface.isNull()); QScopedPointer plasmaSurface(m_plasmaShell->createSurface(surface.data())); QVERIFY(!plasmaSurface.isNull()); plasmaSurface->setRole(PlasmaShellSurface::Role::Panel); QFETCH(bool, wantsFocus); plasmaSurface->setPanelTakesFocus(wantsFocus); auto panel = Test::renderAndWaitForShown(surface.data(), QSize(100, 200), Qt::blue); QVERIFY(panel); QCOMPARE(panel->windowType(), NET::Dock); QVERIFY(panel->isDock()); QFETCH(bool, active); QCOMPARE(panel->dockWantsInput(), active); QCOMPARE(panel->isActive(), active); } WAYLANDTEST_MAIN(PlasmaSurfaceTest) #include "plasma_surface_test.moc" diff --git a/autotests/integration/quick_tiling_test.cpp b/autotests/integration/quick_tiling_test.cpp index 15016c570..7bbc0c7ee 100644 --- a/autotests/integration/quick_tiling_test.cpp +++ b/autotests/integration/quick_tiling_test.cpp @@ -1,893 +1,892 @@ /******************************************************************** 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 "kwin_wayland_test.h" #include "platform.h" #include "abstract_client.h" #include "x11client.h" #include "cursor.h" #include "decorations/decorationbridge.h" #include "decorations/settings.h" #include "screens.h" #include "wayland_server.h" #include "workspace.h" #include "xdgshellclient.h" #include "scripting/scripting.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include Q_DECLARE_METATYPE(KWin::QuickTileMode) Q_DECLARE_METATYPE(KWin::MaximizeMode) namespace KWin { static const QString s_socketName = QStringLiteral("wayland_test_kwin_quick_tiling-0"); class QuickTilingTest : public QObject { Q_OBJECT private Q_SLOTS: void initTestCase(); void init(); void cleanup(); void testQuickTiling_data(); void testQuickTiling(); void testQuickMaximizing_data(); void testQuickMaximizing(); void testQuickTilingKeyboardMove_data(); void testQuickTilingKeyboardMove(); void testQuickTilingPointerMove_data(); void testQuickTilingPointerMove(); void testQuickTilingTouchMove_data(); void testQuickTilingTouchMove(); void testX11QuickTiling_data(); void testX11QuickTiling(); void testX11QuickTilingAfterVertMaximize_data(); void testX11QuickTilingAfterVertMaximize(); void testShortcut_data(); void testShortcut(); void testScript_data(); void testScript(); private: KWayland::Client::ConnectionThread *m_connection = nullptr; KWayland::Client::Compositor *m_compositor = nullptr; }; void QuickTilingTest::initTestCase() { qRegisterMetaType(); qRegisterMetaType(); qRegisterMetaType("MaximizeMode"); QSignalSpy workspaceCreatedSpy(kwinApp(), &Application::workspaceCreated); QVERIFY(workspaceCreatedSpy.isValid()); kwinApp()->platform()->setInitialWindowSize(QSize(1280, 1024)); QVERIFY(waylandServer()->init(s_socketName.toLocal8Bit())); QMetaObject::invokeMethod(kwinApp()->platform(), "setVirtualOutputs", Qt::DirectConnection, Q_ARG(int, 2)); // set custom config which disables the Outline KSharedConfig::Ptr config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); KConfigGroup group = config->group("Outline"); group.writeEntry(QStringLiteral("QmlPath"), QString("/does/not/exist.qml")); group.sync(); kwinApp()->setConfig(config); qputenv("XKB_DEFAULT_RULES", "evdev"); 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)); } void QuickTilingTest::init() { QVERIFY(Test::setupWaylandConnection(Test::AdditionalWaylandInterface::Decoration)); m_connection = Test::waylandConnection(); m_compositor = Test::waylandCompositor(); screens()->setCurrent(0); } void QuickTilingTest::cleanup() { Test::destroyWaylandConnection(); } void QuickTilingTest::testQuickTiling_data() { QTest::addColumn("mode"); QTest::addColumn("expectedGeometry"); QTest::addColumn("secondScreen"); QTest::addColumn("expectedModeAfterToggle"); #define FLAG(name) QuickTileMode(QuickTileFlag::name) QTest::newRow("left") << FLAG(Left) << QRect(0, 0, 640, 1024) << QRect(1280, 0, 640, 1024) << FLAG(Right); QTest::newRow("top") << FLAG(Top) << QRect(0, 0, 1280, 512) << QRect(1280, 0, 1280, 512) << FLAG(Top); QTest::newRow("right") << FLAG(Right) << QRect(640, 0, 640, 1024) << QRect(1920, 0, 640, 1024) << QuickTileMode(); QTest::newRow("bottom") << FLAG(Bottom) << QRect(0, 512, 1280, 512) << QRect(1280, 512, 1280, 512) << FLAG(Bottom); QTest::newRow("top left") << (FLAG(Left) | FLAG(Top)) << QRect(0, 0, 640, 512) << QRect(1280, 0, 640, 512) << (FLAG(Right) | FLAG(Top)); QTest::newRow("top right") << (FLAG(Right) | FLAG(Top)) << QRect(640, 0, 640, 512) << QRect(1920, 0, 640, 512) << QuickTileMode(); QTest::newRow("bottom left") << (FLAG(Left) | FLAG(Bottom)) << QRect(0, 512, 640, 512) << QRect(1280, 512, 640, 512) << (FLAG(Right) | FLAG(Bottom)); QTest::newRow("bottom right") << (FLAG(Right) | FLAG(Bottom)) << QRect(640, 512, 640, 512) << QRect(1920, 512, 640, 512) << QuickTileMode(); QTest::newRow("maximize") << FLAG(Maximize) << QRect(0, 0, 1280, 1024) << QRect(1280, 0, 1280, 1024) << QuickTileMode(); #undef FLAG } void QuickTilingTest::testQuickTiling() { using namespace KWayland::Client; QScopedPointer surface(Test::createSurface()); QVERIFY(!surface.isNull()); QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); QVERIFY(!shellSurface.isNull()); // Map the client. auto c = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); QVERIFY(c); QCOMPARE(workspace()->activeClient(), c); QCOMPARE(c->frameGeometry(), QRect(0, 0, 100, 50)); QCOMPARE(c->quickTileMode(), QuickTileMode(QuickTileFlag::None)); // We have to receive a configure event when the client becomes active. QSignalSpy configureRequestedSpy(shellSurface.data(), &XdgShellSurface::configureRequested); QVERIFY(configureRequestedSpy.isValid()); QVERIFY(configureRequestedSpy.wait()); QCOMPARE(configureRequestedSpy.count(), 1); QSignalSpy quickTileChangedSpy(c, &AbstractClient::quickTileModeChanged); QVERIFY(quickTileChangedSpy.isValid()); - QSignalSpy geometryChangedSpy(c, &AbstractClient::geometryChanged); - QVERIFY(geometryChangedSpy.isValid()); + QSignalSpy frameGeometryChangedSpy(c, &AbstractClient::frameGeometryChanged); + QVERIFY(frameGeometryChangedSpy.isValid()); QFETCH(QuickTileMode, mode); QFETCH(QRect, expectedGeometry); c->setQuickTileMode(mode, true); QCOMPARE(quickTileChangedSpy.count(), 1); // at this point the geometry did not yet change QCOMPARE(c->frameGeometry(), QRect(0, 0, 100, 50)); // but quick tile mode already changed QCOMPARE(c->quickTileMode(), mode); // but we got requested a new geometry QVERIFY(configureRequestedSpy.wait()); QEXPECT_FAIL("maximize", "Two configure events are sent for maximized", Continue); QCOMPARE(configureRequestedSpy.count(), 2); QCOMPARE(configureRequestedSpy.last().at(0).toSize(), expectedGeometry.size()); // attach a new image shellSurface->ackConfigure(configureRequestedSpy.last().at(2).value()); Test::render(surface.data(), expectedGeometry.size(), Qt::red); - QVERIFY(geometryChangedSpy.wait()); - QEXPECT_FAIL("maximize", "Geometry changed called twice for maximize", Continue); - QCOMPARE(geometryChangedSpy.count(), 1); + QVERIFY(frameGeometryChangedSpy.wait()); + QCOMPARE(frameGeometryChangedSpy.count(), 1); QCOMPARE(c->frameGeometry(), expectedGeometry); // send window to other screen QCOMPARE(c->screen(), 0); c->sendToScreen(1); QCOMPARE(c->screen(), 1); // quick tile should not be changed QCOMPARE(c->quickTileMode(), mode); QTEST(c->frameGeometry(), "secondScreen"); // now try to toggle again c->setQuickTileMode(mode, true); QTEST(c->quickTileMode(), "expectedModeAfterToggle"); } void QuickTilingTest::testQuickMaximizing_data() { QTest::addColumn("mode"); #define FLAG(name) QuickTileMode(QuickTileFlag::name) QTest::newRow("maximize") << FLAG(Maximize); QTest::newRow("none") << FLAG(None); #undef FLAG } void QuickTilingTest::testQuickMaximizing() { using namespace KWayland::Client; QScopedPointer surface(Test::createSurface()); QVERIFY(!surface.isNull()); QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); QVERIFY(!shellSurface.isNull()); // Map the client. auto c = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); QVERIFY(c); QCOMPARE(workspace()->activeClient(), c); QCOMPARE(c->frameGeometry(), QRect(0, 0, 100, 50)); QCOMPARE(c->quickTileMode(), QuickTileMode(QuickTileFlag::None)); QCOMPARE(c->maximizeMode(), MaximizeRestore); // We have to receive a configure event upon becoming active. QSignalSpy configureRequestedSpy(shellSurface.data(), &XdgShellSurface::configureRequested); QVERIFY(configureRequestedSpy.isValid()); QVERIFY(configureRequestedSpy.wait()); QCOMPARE(configureRequestedSpy.count(), 1); QSignalSpy quickTileChangedSpy(c, &AbstractClient::quickTileModeChanged); QVERIFY(quickTileChangedSpy.isValid()); - QSignalSpy geometryChangedSpy(c, &AbstractClient::geometryChanged); - QVERIFY(geometryChangedSpy.isValid()); + QSignalSpy frameGeometryChangedSpy(c, &AbstractClient::frameGeometryChanged); + QVERIFY(frameGeometryChangedSpy.isValid()); QSignalSpy maximizeChangedSpy1(c, qOverload(&AbstractClient::clientMaximizedStateChanged)); QVERIFY(maximizeChangedSpy1.isValid()); QSignalSpy maximizeChangedSpy2(c, qOverload(&AbstractClient::clientMaximizedStateChanged)); QVERIFY(maximizeChangedSpy2.isValid()); c->setQuickTileMode(QuickTileFlag::Maximize, true); QCOMPARE(quickTileChangedSpy.count(), 1); // at this point the geometry did not yet change QCOMPARE(c->frameGeometry(), QRect(0, 0, 100, 50)); // but quick tile mode already changed QCOMPARE(c->quickTileMode(), QuickTileFlag::Maximize); QCOMPARE(c->geometryRestore(), QRect(0, 0, 100, 50)); // but we got requested a new geometry QVERIFY(configureRequestedSpy.wait()); QEXPECT_FAIL("", "Two configure events are sent for maximized", Continue); QCOMPARE(configureRequestedSpy.count(), 2); QCOMPARE(configureRequestedSpy.last().at(0).toSize(), QSize(1280, 1024)); // attach a new image shellSurface->ackConfigure(configureRequestedSpy.last().at(2).value()); Test::render(surface.data(), QSize(1280, 1024), Qt::red); - QVERIFY(geometryChangedSpy.wait()); - QCOMPARE(geometryChangedSpy.count(), 2); + QVERIFY(frameGeometryChangedSpy.wait()); + QCOMPARE(frameGeometryChangedSpy.count(), 1); QCOMPARE(c->frameGeometry(), QRect(0, 0, 1280, 1024)); QCOMPARE(c->geometryRestore(), QRect(0, 0, 100, 50)); // client is now set to maximised QCOMPARE(maximizeChangedSpy1.count(), 1); QCOMPARE(maximizeChangedSpy1.first().first().value(), c); QCOMPARE(maximizeChangedSpy1.first().last().value(), MaximizeFull); QCOMPARE(maximizeChangedSpy2.count(), 1); QCOMPARE(maximizeChangedSpy2.first().first().value(), c); QCOMPARE(maximizeChangedSpy2.first().at(1).toBool(), true); QCOMPARE(maximizeChangedSpy2.first().at(2).toBool(), true); QCOMPARE(c->maximizeMode(), MaximizeFull); // go back to quick tile none QFETCH(QuickTileMode, mode); c->setQuickTileMode(mode, true); QCOMPARE(c->quickTileMode(), QuickTileMode(QuickTileFlag::None)); QCOMPARE(quickTileChangedSpy.count(), 2); // geometry not yet changed QCOMPARE(c->frameGeometry(), QRect(0, 0, 1280, 1024)); QCOMPARE(c->geometryRestore(), QRect(0, 0, 100, 50)); // we got requested a new geometry QVERIFY(configureRequestedSpy.wait()); QEXPECT_FAIL("", "Two configure events are sent for maximized", Continue); QCOMPARE(configureRequestedSpy.count(), 3); QCOMPARE(configureRequestedSpy.last().at(0).toSize(), QSize(100, 50)); // render again shellSurface->ackConfigure(configureRequestedSpy.last().at(2).value()); Test::render(surface.data(), QSize(100, 50), Qt::yellow); - QVERIFY(geometryChangedSpy.wait()); - QCOMPARE(geometryChangedSpy.count(), 4); + QVERIFY(frameGeometryChangedSpy.wait()); + QCOMPARE(frameGeometryChangedSpy.count(), 2); QCOMPARE(c->frameGeometry(), QRect(0, 0, 100, 50)); QCOMPARE(c->geometryRestore(), QRect(0, 0, 100, 50)); QCOMPARE(maximizeChangedSpy1.count(), 2); QCOMPARE(maximizeChangedSpy1.last().first().value(), c); QCOMPARE(maximizeChangedSpy1.last().last().value(), MaximizeRestore); QCOMPARE(maximizeChangedSpy2.count(), 2); QCOMPARE(maximizeChangedSpy2.last().first().value(), c); QCOMPARE(maximizeChangedSpy2.last().at(1).toBool(), false); QCOMPARE(maximizeChangedSpy2.last().at(2).toBool(), false); } void QuickTilingTest::testQuickTilingKeyboardMove_data() { QTest::addColumn("targetPos"); QTest::addColumn("expectedMode"); QTest::newRow("topRight") << QPoint(2559, 24) << QuickTileMode(QuickTileFlag::Top | QuickTileFlag::Right); QTest::newRow("right") << QPoint(2559, 512) << QuickTileMode(QuickTileFlag::Right); QTest::newRow("bottomRight") << QPoint(2559, 1023) << QuickTileMode(QuickTileFlag::Bottom | QuickTileFlag::Right); QTest::newRow("bottomLeft") << QPoint(0, 1023) << QuickTileMode(QuickTileFlag::Bottom | QuickTileFlag::Left); QTest::newRow("Left") << QPoint(0, 512) << QuickTileMode(QuickTileFlag::Left); QTest::newRow("topLeft") << QPoint(0, 24) << QuickTileMode(QuickTileFlag::Top | QuickTileFlag::Left); } void QuickTilingTest::testQuickTilingKeyboardMove() { using namespace KWayland::Client; QScopedPointer surface(Test::createSurface()); QVERIFY(!surface.isNull()); QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); QVERIFY(!shellSurface.isNull()); QSignalSpy sizeChangeSpy(shellSurface.data(), &XdgShellSurface::sizeChanged); QVERIFY(sizeChangeSpy.isValid()); // let's render auto c = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); QVERIFY(c); QCOMPARE(workspace()->activeClient(), c); QCOMPARE(c->frameGeometry(), QRect(0, 0, 100, 50)); QCOMPARE(c->quickTileMode(), QuickTileMode(QuickTileFlag::None)); QCOMPARE(c->maximizeMode(), MaximizeRestore); QSignalSpy quickTileChangedSpy(c, &AbstractClient::quickTileModeChanged); QVERIFY(quickTileChangedSpy.isValid()); workspace()->performWindowOperation(c, Options::UnrestrictedMoveOp); QCOMPARE(c, workspace()->moveResizeClient()); QCOMPARE(Cursor::pos(), QPoint(49, 24)); QFETCH(QPoint, targetPos); quint32 timestamp = 1; kwinApp()->platform()->keyboardKeyPressed(KEY_LEFTCTRL, timestamp++); while (Cursor::pos().x() > targetPos.x()) { kwinApp()->platform()->keyboardKeyPressed(KEY_LEFT, timestamp++); kwinApp()->platform()->keyboardKeyReleased(KEY_LEFT, timestamp++); } while (Cursor::pos().x() < targetPos.x()) { kwinApp()->platform()->keyboardKeyPressed(KEY_RIGHT, timestamp++); kwinApp()->platform()->keyboardKeyReleased(KEY_RIGHT, timestamp++); } while (Cursor::pos().y() < targetPos.y()) { kwinApp()->platform()->keyboardKeyPressed(KEY_DOWN, timestamp++); kwinApp()->platform()->keyboardKeyReleased(KEY_DOWN, timestamp++); } while (Cursor::pos().y() > targetPos.y()) { kwinApp()->platform()->keyboardKeyPressed(KEY_UP, timestamp++); kwinApp()->platform()->keyboardKeyReleased(KEY_UP, timestamp++); } kwinApp()->platform()->keyboardKeyReleased(KEY_LEFTCTRL, timestamp++); kwinApp()->platform()->keyboardKeyPressed(KEY_ENTER, timestamp++); kwinApp()->platform()->keyboardKeyReleased(KEY_ENTER, timestamp++); QCOMPARE(Cursor::pos(), targetPos); QVERIFY(!workspace()->moveResizeClient()); QCOMPARE(quickTileChangedSpy.count(), 1); QTEST(c->quickTileMode(), "expectedMode"); } void QuickTilingTest::testQuickTilingPointerMove_data() { QTest::addColumn("targetPos"); QTest::addColumn("expectedMode"); QTest::newRow("topRight") << QPoint(2559, 24) << QuickTileMode(QuickTileFlag::Top | QuickTileFlag::Right); QTest::newRow("right") << QPoint(2559, 512) << QuickTileMode(QuickTileFlag::Right); QTest::newRow("bottomRight") << QPoint(2559, 1023) << QuickTileMode(QuickTileFlag::Bottom | QuickTileFlag::Right); QTest::newRow("bottomLeft") << QPoint(0, 1023) << QuickTileMode(QuickTileFlag::Bottom | QuickTileFlag::Left); QTest::newRow("Left") << QPoint(0, 512) << QuickTileMode(QuickTileFlag::Left); QTest::newRow("topLeft") << QPoint(0, 24) << QuickTileMode(QuickTileFlag::Top | QuickTileFlag::Left); } void QuickTilingTest::testQuickTilingPointerMove() { using namespace KWayland::Client; QScopedPointer surface(Test::createSurface()); QVERIFY(!surface.isNull()); QScopedPointer shellSurface(Test::createXdgShellV6Surface( surface.data(), surface.data(), Test::CreationSetup::CreateOnly)); QVERIFY(!shellSurface.isNull()); // wait for the initial configure event QSignalSpy configureRequestedSpy(shellSurface.data(), &XdgShellSurface::configureRequested); QVERIFY(configureRequestedSpy.isValid()); surface->commit(Surface::CommitFlag::None); QVERIFY(configureRequestedSpy.wait()); QCOMPARE(configureRequestedSpy.count(), 1); // let's render shellSurface->ackConfigure(configureRequestedSpy.last().at(2).value()); auto c = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); QVERIFY(c); QCOMPARE(workspace()->activeClient(), c); QCOMPARE(c->frameGeometry(), QRect(0, 0, 100, 50)); QCOMPARE(c->quickTileMode(), QuickTileMode(QuickTileFlag::None)); QCOMPARE(c->maximizeMode(), MaximizeRestore); // we have to receive a configure event when the client becomes active QVERIFY(configureRequestedSpy.wait()); QTRY_COMPARE(configureRequestedSpy.count(), 2); QSignalSpy quickTileChangedSpy(c, &AbstractClient::quickTileModeChanged); QVERIFY(quickTileChangedSpy.isValid()); workspace()->performWindowOperation(c, Options::UnrestrictedMoveOp); QCOMPARE(c, workspace()->moveResizeClient()); QCOMPARE(Cursor::pos(), QPoint(49, 24)); QVERIFY(configureRequestedSpy.wait()); QCOMPARE(configureRequestedSpy.count(), 3); QFETCH(QPoint, targetPos); quint32 timestamp = 1; kwinApp()->platform()->pointerMotion(targetPos, timestamp++); kwinApp()->platform()->pointerButtonPressed(BTN_LEFT, timestamp++); kwinApp()->platform()->pointerButtonReleased(BTN_LEFT, timestamp++); QCOMPARE(Cursor::pos(), targetPos); QVERIFY(!workspace()->moveResizeClient()); QCOMPARE(quickTileChangedSpy.count(), 1); QTEST(c->quickTileMode(), "expectedMode"); QVERIFY(configureRequestedSpy.wait()); QCOMPARE(configureRequestedSpy.count(), 4); QCOMPARE(false, configureRequestedSpy.last().first().toSize().isEmpty()); } void QuickTilingTest::testQuickTilingTouchMove_data() { QTest::addColumn("targetPos"); QTest::addColumn("expectedMode"); QTest::newRow("topRight") << QPoint(2559, 24) << QuickTileMode(QuickTileFlag::Top | QuickTileFlag::Right); QTest::newRow("right") << QPoint(2559, 512) << QuickTileMode(QuickTileFlag::Right); QTest::newRow("bottomRight") << QPoint(2559, 1023) << QuickTileMode(QuickTileFlag::Bottom | QuickTileFlag::Right); QTest::newRow("bottomLeft") << QPoint(0, 1023) << QuickTileMode(QuickTileFlag::Bottom | QuickTileFlag::Left); QTest::newRow("Left") << QPoint(0, 512) << QuickTileMode(QuickTileFlag::Left); QTest::newRow("topLeft") << QPoint(0, 24) << QuickTileMode(QuickTileFlag::Top | QuickTileFlag::Left); } void QuickTilingTest::testQuickTilingTouchMove() { // test verifies that touch on decoration also allows quick tiling // see BUG: 390113 using namespace KWayland::Client; QScopedPointer surface(Test::createSurface()); QVERIFY(!surface.isNull()); QScopedPointer deco(Test::waylandServerSideDecoration()->create(surface.data())); QScopedPointer shellSurface(Test::createXdgShellV6Surface( surface.data(), surface.data(), Test::CreationSetup::CreateOnly)); QVERIFY(!shellSurface.isNull()); // wait for the initial configure event QSignalSpy configureRequestedSpy(shellSurface.data(), &XdgShellSurface::configureRequested); QVERIFY(configureRequestedSpy.isValid()); surface->commit(Surface::CommitFlag::None); QVERIFY(configureRequestedSpy.wait()); QCOMPARE(configureRequestedSpy.count(), 1); // let's render shellSurface->ackConfigure(configureRequestedSpy.last().at(2).value()); auto c = Test::renderAndWaitForShown(surface.data(), QSize(1000, 50), Qt::blue); QVERIFY(c); QVERIFY(c->isDecorated()); const auto decoration = c->decoration(); QCOMPARE(workspace()->activeClient(), c); QCOMPARE(c->frameGeometry(), QRect(-decoration->borderLeft(), 0, 1000 + decoration->borderLeft() + decoration->borderRight(), 50 + decoration->borderTop() + decoration->borderBottom())); QCOMPARE(c->quickTileMode(), QuickTileMode(QuickTileFlag::None)); QCOMPARE(c->maximizeMode(), MaximizeRestore); // we have to receive a configure event when the client becomes active QVERIFY(configureRequestedSpy.wait()); QTRY_COMPARE(configureRequestedSpy.count(), 2); QSignalSpy quickTileChangedSpy(c, &AbstractClient::quickTileModeChanged); QVERIFY(quickTileChangedSpy.isValid()); quint32 timestamp = 1; kwinApp()->platform()->touchDown(0, QPointF(c->frameGeometry().center().x(), c->frameGeometry().y() + decoration->borderTop() / 2), timestamp++); QVERIFY(configureRequestedSpy.wait()); QCOMPARE(c, workspace()->moveResizeClient()); QCOMPARE(configureRequestedSpy.count(), 3); QFETCH(QPoint, targetPos); kwinApp()->platform()->touchMotion(0, targetPos, timestamp++); kwinApp()->platform()->touchUp(0, timestamp++); QVERIFY(!workspace()->moveResizeClient()); // When there are no borders, there is no change to them when quick-tiling. // TODO: we should test both cases with fixed fake decoration for autotests. const bool hasBorders = Decoration::DecorationBridge::self()->settings()->borderSize() != KDecoration2::BorderSize::None; QCOMPARE(quickTileChangedSpy.count(), 1); QTEST(c->quickTileMode(), "expectedMode"); QVERIFY(configureRequestedSpy.wait()); QTRY_COMPARE(configureRequestedSpy.count(), hasBorders ? 5 : 4); QCOMPARE(false, configureRequestedSpy.last().first().toSize().isEmpty()); } struct XcbConnectionDeleter { static inline void cleanup(xcb_connection_t *pointer) { xcb_disconnect(pointer); } }; void QuickTilingTest::testX11QuickTiling_data() { QTest::addColumn("mode"); QTest::addColumn("expectedGeometry"); QTest::addColumn("screen"); QTest::addColumn("modeAfterToggle"); #define FLAG(name) QuickTileMode(QuickTileFlag::name) QTest::newRow("left") << FLAG(Left) << QRect(0, 0, 640, 1024) << 0 << QuickTileMode(); QTest::newRow("top") << FLAG(Top) << QRect(0, 0, 1280, 512) << 1 << FLAG(Top); QTest::newRow("right") << FLAG(Right) << QRect(640, 0, 640, 1024) << 1 << FLAG(Left); QTest::newRow("bottom") << FLAG(Bottom) << QRect(0, 512, 1280, 512) << 1 << FLAG(Bottom); QTest::newRow("top left") << (FLAG(Left) | FLAG(Top)) << QRect(0, 0, 640, 512) << 0 << QuickTileMode(); QTest::newRow("top right") << (FLAG(Right) | FLAG(Top)) << QRect(640, 0, 640, 512) << 1 << (FLAG(Left) | FLAG(Top)); QTest::newRow("bottom left") << (FLAG(Left) | FLAG(Bottom)) << QRect(0, 512, 640, 512) << 0 << QuickTileMode(); QTest::newRow("bottom right") << (FLAG(Right) | FLAG(Bottom)) << QRect(640, 512, 640, 512) << 1 << (FLAG(Left) | FLAG(Bottom)); QTest::newRow("maximize") << FLAG(Maximize) << QRect(0, 0, 1280, 1024) << 0 << QuickTileMode(); #undef FLAG } void QuickTilingTest::testX11QuickTiling() { QScopedPointer c(xcb_connect(nullptr, nullptr)); QVERIFY(!xcb_connection_has_error(c.data())); const QRect windowGeometry(0, 0, 100, 200); xcb_window_t w = xcb_generate_id(c.data()); xcb_create_window(c.data(), XCB_COPY_FROM_PARENT, w, rootWindow(), windowGeometry.x(), windowGeometry.y(), windowGeometry.width(), windowGeometry.height(), 0, XCB_WINDOW_CLASS_INPUT_OUTPUT, XCB_COPY_FROM_PARENT, 0, nullptr); xcb_size_hints_t hints; memset(&hints, 0, sizeof(hints)); xcb_icccm_size_hints_set_position(&hints, 1, windowGeometry.x(), windowGeometry.y()); xcb_icccm_size_hints_set_size(&hints, 1, windowGeometry.width(), windowGeometry.height()); xcb_icccm_set_wm_normal_hints(c.data(), w, &hints); xcb_map_window(c.data(), w); xcb_flush(c.data()); // we should get a client for it QSignalSpy windowCreatedSpy(workspace(), &Workspace::clientAdded); QVERIFY(windowCreatedSpy.isValid()); QVERIFY(windowCreatedSpy.wait()); X11Client *client = windowCreatedSpy.first().first().value(); QVERIFY(client); QCOMPARE(client->window(), w); // now quick tile QSignalSpy quickTileChangedSpy(client, &AbstractClient::quickTileModeChanged); QVERIFY(quickTileChangedSpy.isValid()); const QRect origGeo = client->frameGeometry(); QFETCH(QuickTileMode, mode); client->setQuickTileMode(mode, true); QCOMPARE(client->quickTileMode(), mode); QTEST(client->frameGeometry(), "expectedGeometry"); QCOMPARE(client->geometryRestore(), origGeo); QEXPECT_FAIL("maximize", "For maximize we get two changed signals", Continue); QCOMPARE(quickTileChangedSpy.count(), 1); // quick tile to same edge again should also act like send to screen QCOMPARE(client->screen(), 0); client->setQuickTileMode(mode, true); QTEST(client->screen(), "screen"); QTEST(client->quickTileMode(), "modeAfterToggle"); QCOMPARE(client->geometryRestore(), origGeo); // and destroy the window again xcb_unmap_window(c.data(), w); xcb_destroy_window(c.data(), w); xcb_flush(c.data()); c.reset(); QSignalSpy windowClosedSpy(client, &X11Client::windowClosed); QVERIFY(windowClosedSpy.isValid()); QVERIFY(windowClosedSpy.wait()); } void QuickTilingTest::testX11QuickTilingAfterVertMaximize_data() { QTest::addColumn("mode"); QTest::addColumn("expectedGeometry"); #define FLAG(name) QuickTileMode(QuickTileFlag::name) QTest::newRow("left") << FLAG(Left) << QRect(0, 0, 640, 1024); QTest::newRow("top") << FLAG(Top) << QRect(0, 0, 1280, 512); QTest::newRow("right") << FLAG(Right) << QRect(640, 0, 640, 1024); QTest::newRow("bottom") << FLAG(Bottom) << QRect(0, 512, 1280, 512); QTest::newRow("top left") << (FLAG(Left) | FLAG(Top)) << QRect(0, 0, 640, 512); QTest::newRow("top right") << (FLAG(Right) | FLAG(Top)) << QRect(640, 0, 640, 512); QTest::newRow("bottom left") << (FLAG(Left) | FLAG(Bottom)) << QRect(0, 512, 640, 512); QTest::newRow("bottom right") << (FLAG(Right) | FLAG(Bottom)) << QRect(640, 512, 640, 512); QTest::newRow("maximize") << FLAG(Maximize) << QRect(0, 0, 1280, 1024); #undef FLAG } void QuickTilingTest::testX11QuickTilingAfterVertMaximize() { QScopedPointer c(xcb_connect(nullptr, nullptr)); QVERIFY(!xcb_connection_has_error(c.data())); const QRect windowGeometry(0, 0, 100, 200); xcb_window_t w = xcb_generate_id(c.data()); xcb_create_window(c.data(), XCB_COPY_FROM_PARENT, w, rootWindow(), windowGeometry.x(), windowGeometry.y(), windowGeometry.width(), windowGeometry.height(), 0, XCB_WINDOW_CLASS_INPUT_OUTPUT, XCB_COPY_FROM_PARENT, 0, nullptr); xcb_size_hints_t hints; memset(&hints, 0, sizeof(hints)); xcb_icccm_size_hints_set_position(&hints, 1, windowGeometry.x(), windowGeometry.y()); xcb_icccm_size_hints_set_size(&hints, 1, windowGeometry.width(), windowGeometry.height()); xcb_icccm_set_wm_normal_hints(c.data(), w, &hints); xcb_map_window(c.data(), w); xcb_flush(c.data()); // we should get a client for it QSignalSpy windowCreatedSpy(workspace(), &Workspace::clientAdded); QVERIFY(windowCreatedSpy.isValid()); QVERIFY(windowCreatedSpy.wait()); X11Client *client = windowCreatedSpy.first().first().value(); QVERIFY(client); QCOMPARE(client->window(), w); const QRect origGeo = client->frameGeometry(); QCOMPARE(client->maximizeMode(), MaximizeRestore); // vertically maximize the window client->maximize(client->maximizeMode() ^ MaximizeVertical); QCOMPARE(client->frameGeometry().width(), origGeo.width()); QCOMPARE(client->height(), screens()->size(client->screen()).height()); QCOMPARE(client->geometryRestore(), origGeo); // now quick tile QSignalSpy quickTileChangedSpy(client, &AbstractClient::quickTileModeChanged); QVERIFY(quickTileChangedSpy.isValid()); QFETCH(QuickTileMode, mode); client->setQuickTileMode(mode, true); QCOMPARE(client->quickTileMode(), mode); QTEST(client->frameGeometry(), "expectedGeometry"); QEXPECT_FAIL("", "We get two changed events", Continue); QCOMPARE(quickTileChangedSpy.count(), 1); // and destroy the window again xcb_unmap_window(c.data(), w); xcb_destroy_window(c.data(), w); xcb_flush(c.data()); c.reset(); QSignalSpy windowClosedSpy(client, &X11Client::windowClosed); QVERIFY(windowClosedSpy.isValid()); QVERIFY(windowClosedSpy.wait()); } void QuickTilingTest::testShortcut_data() { QTest::addColumn("shortcut"); QTest::addColumn("expectedMode"); QTest::addColumn("expectedGeometry"); #define FLAG(name) QuickTileMode(QuickTileFlag::name) QTest::newRow("top") << QStringLiteral("Window Quick Tile Top") << FLAG(Top) << QRect(0, 0, 1280, 512); QTest::newRow("left") << QStringLiteral("Window Quick Tile Left") << FLAG(Left) << QRect(0, 0, 640, 1024); QTest::newRow("bottom") << QStringLiteral("Window Quick Tile Bottom") << FLAG(Bottom) << QRect(0, 512, 1280, 512); QTest::newRow("right") << QStringLiteral("Window Quick Tile Right") << FLAG(Right) << QRect(640, 0, 640, 1024); QTest::newRow("top right") << QStringLiteral("Window Quick Tile Top Right") << (FLAG(Top) | FLAG(Right)) << QRect(640, 0, 640, 512); QTest::newRow("top left") << QStringLiteral("Window Quick Tile Top Left") << (FLAG(Top) | FLAG(Left)) << QRect(0, 0, 640, 512); QTest::newRow("bottom right") << QStringLiteral("Window Quick Tile Bottom Right") << (FLAG(Bottom) | FLAG(Right)) << QRect(640, 512, 640, 512); QTest::newRow("bottom left") << QStringLiteral("Window Quick Tile Bottom Left") << (FLAG(Bottom) | FLAG(Left)) << QRect(0, 512, 640, 512); #undef FLAG } void QuickTilingTest::testShortcut() { using namespace KWayland::Client; QScopedPointer surface(Test::createSurface()); QVERIFY(!surface.isNull()); QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); QVERIFY(!shellSurface.isNull()); // Map the client. auto c = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); QVERIFY(c); QCOMPARE(workspace()->activeClient(), c); QCOMPARE(c->frameGeometry(), QRect(0, 0, 100, 50)); QCOMPARE(c->quickTileMode(), QuickTileMode(QuickTileFlag::None)); // We have to receive a configure event when the client becomes active. QSignalSpy configureRequestedSpy(shellSurface.data(), &XdgShellSurface::configureRequested); QVERIFY(configureRequestedSpy.isValid()); QVERIFY(configureRequestedSpy.wait()); QCOMPARE(configureRequestedSpy.count(), 1); QFETCH(QString, shortcut); QFETCH(QRect, expectedGeometry); // invoke global shortcut through dbus auto msg = QDBusMessage::createMethodCall( QStringLiteral("org.kde.kglobalaccel"), QStringLiteral("/component/kwin"), QStringLiteral("org.kde.kglobalaccel.Component"), QStringLiteral("invokeShortcut")); msg.setArguments(QList{shortcut}); QDBusConnection::sessionBus().asyncCall(msg); QSignalSpy quickTileChangedSpy(c, &AbstractClient::quickTileModeChanged); QVERIFY(quickTileChangedSpy.isValid()); QVERIFY(quickTileChangedSpy.wait()); QCOMPARE(quickTileChangedSpy.count(), 1); // at this point the geometry did not yet change QCOMPARE(c->frameGeometry(), QRect(0, 0, 100, 50)); // but quick tile mode already changed QTEST(c->quickTileMode(), "expectedMode"); // but we got requested a new geometry QVERIFY(configureRequestedSpy.wait()); QCOMPARE(configureRequestedSpy.count(), 2); QCOMPARE(configureRequestedSpy.last().at(0).toSize(), expectedGeometry.size()); // attach a new image - QSignalSpy geometryChangedSpy(c, &AbstractClient::geometryChanged); - QVERIFY(geometryChangedSpy.isValid()); + QSignalSpy frameGeometryChangedSpy(c, &AbstractClient::frameGeometryChanged); + QVERIFY(frameGeometryChangedSpy.isValid()); shellSurface->ackConfigure(configureRequestedSpy.last().at(2).value()); Test::render(surface.data(), expectedGeometry.size(), Qt::red); - QVERIFY(geometryChangedSpy.wait()); + QVERIFY(frameGeometryChangedSpy.wait()); QEXPECT_FAIL("maximize", "Geometry changed called twice for maximize", Continue); - QCOMPARE(geometryChangedSpy.count(), 1); + QCOMPARE(frameGeometryChangedSpy.count(), 1); QCOMPARE(c->frameGeometry(), expectedGeometry); } void QuickTilingTest::testScript_data() { QTest::addColumn("action"); QTest::addColumn("expectedMode"); QTest::addColumn("expectedGeometry"); #define FLAG(name) QuickTileMode(QuickTileFlag::name) QTest::newRow("top") << QStringLiteral("Top") << FLAG(Top) << QRect(0, 0, 1280, 512); QTest::newRow("left") << QStringLiteral("Left") << FLAG(Left) << QRect(0, 0, 640, 1024); QTest::newRow("bottom") << QStringLiteral("Bottom") << FLAG(Bottom) << QRect(0, 512, 1280, 512); QTest::newRow("right") << QStringLiteral("Right") << FLAG(Right) << QRect(640, 0, 640, 1024); QTest::newRow("top right") << QStringLiteral("TopRight") << (FLAG(Top) | FLAG(Right)) << QRect(640, 0, 640, 512); QTest::newRow("top left") << QStringLiteral("TopLeft") << (FLAG(Top) | FLAG(Left)) << QRect(0, 0, 640, 512); QTest::newRow("bottom right") << QStringLiteral("BottomRight") << (FLAG(Bottom) | FLAG(Right)) << QRect(640, 512, 640, 512); QTest::newRow("bottom left") << QStringLiteral("BottomLeft") << (FLAG(Bottom) | FLAG(Left)) << QRect(0, 512, 640, 512); #undef FLAG } void QuickTilingTest::testScript() { using namespace KWayland::Client; QScopedPointer surface(Test::createSurface()); QVERIFY(!surface.isNull()); QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); QVERIFY(!shellSurface.isNull()); // Map the client. auto c = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); QVERIFY(c); QCOMPARE(workspace()->activeClient(), c); QCOMPARE(c->frameGeometry(), QRect(0, 0, 100, 50)); QCOMPARE(c->quickTileMode(), QuickTileMode(QuickTileFlag::None)); // We have to receive a configure event upon the client becoming active. QSignalSpy configureRequestedSpy(shellSurface.data(), &XdgShellSurface::configureRequested); QVERIFY(configureRequestedSpy.isValid()); QVERIFY(configureRequestedSpy.wait()); QCOMPARE(configureRequestedSpy.count(), 1); QSignalSpy quickTileChangedSpy(c, &AbstractClient::quickTileModeChanged); QVERIFY(quickTileChangedSpy.isValid()); - QSignalSpy geometryChangedSpy(c, &AbstractClient::geometryChanged); - QVERIFY(geometryChangedSpy.isValid()); + QSignalSpy frameGeometryChangedSpy(c, &AbstractClient::frameGeometryChanged); + QVERIFY(frameGeometryChangedSpy.isValid()); QVERIFY(Scripting::self()); QTemporaryFile tmpFile; QVERIFY(tmpFile.open()); QTextStream out(&tmpFile); QFETCH(QString, action); out << "workspace.slotWindowQuickTile" << action << "()"; out.flush(); QFETCH(QuickTileMode, expectedMode); QFETCH(QRect, expectedGeometry); const int id = Scripting::self()->loadScript(tmpFile.fileName()); QVERIFY(id != -1); QVERIFY(Scripting::self()->isScriptLoaded(tmpFile.fileName())); auto s = Scripting::self()->findScript(tmpFile.fileName()); QVERIFY(s); QSignalSpy runningChangedSpy(s, &AbstractScript::runningChanged); QVERIFY(runningChangedSpy.isValid()); s->run(); QVERIFY(quickTileChangedSpy.wait()); QCOMPARE(quickTileChangedSpy.count(), 1); QCOMPARE(runningChangedSpy.count(), 1); QCOMPARE(runningChangedSpy.first().first().toBool(), true); // at this point the geometry did not yet change QCOMPARE(c->frameGeometry(), QRect(0, 0, 100, 50)); // but quick tile mode already changed QCOMPARE(c->quickTileMode(), expectedMode); // but we got requested a new geometry QVERIFY(configureRequestedSpy.wait()); QCOMPARE(configureRequestedSpy.count(), 2); QCOMPARE(configureRequestedSpy.last().at(0).toSize(), expectedGeometry.size()); // attach a new image shellSurface->ackConfigure(configureRequestedSpy.last().at(2).value()); Test::render(surface.data(), expectedGeometry.size(), Qt::red); - QVERIFY(geometryChangedSpy.wait()); + QVERIFY(frameGeometryChangedSpy.wait()); QEXPECT_FAIL("maximize", "Geometry changed called twice for maximize", Continue); - QCOMPARE(geometryChangedSpy.count(), 1); + QCOMPARE(frameGeometryChangedSpy.count(), 1); QCOMPARE(c->frameGeometry(), expectedGeometry); } } WAYLANDTEST_MAIN(KWin::QuickTilingTest) #include "quick_tiling_test.moc" diff --git a/autotests/integration/struts_test.cpp b/autotests/integration/struts_test.cpp index a1b887ff9..8a2a7d990 100644 --- a/autotests/integration/struts_test.cpp +++ b/autotests/integration/struts_test.cpp @@ -1,967 +1,967 @@ /******************************************************************** 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 "x11client.h" #include "cursor.h" #include "deleted.h" #include "screenedge.h" #include "screens.h" #include "wayland_server.h" #include "workspace.h" #include "xdgshellclient.h" #include #include #include #include #include #include #include namespace KWin { static const QString s_socketName = QStringLiteral("wayland_test_kwin_struts-0"); class StrutsTest : public QObject { Q_OBJECT private Q_SLOTS: void initTestCase(); void init(); void cleanup(); void testWaylandStruts_data(); void testWaylandStruts(); void testMoveWaylandPanel(); void testWaylandMobilePanel(); void testX11Struts_data(); void testX11Struts(); void test363804(); void testLeftScreenSmallerBottomAligned(); void testWindowMoveWithPanelBetweenScreens(); private: KWayland::Client::Compositor *m_compositor = nullptr; KWayland::Client::PlasmaShell *m_plasmaShell = nullptr; }; void StrutsTest::initTestCase() { qRegisterMetaType(); qRegisterMetaType(); qRegisterMetaType(); QSignalSpy workspaceCreatedSpy(kwinApp(), &Application::workspaceCreated); QVERIFY(workspaceCreatedSpy.isValid()); kwinApp()->platform()->setInitialWindowSize(QSize(1280, 1024)); QVERIFY(waylandServer()->init(s_socketName.toLocal8Bit())); QMetaObject::invokeMethod(kwinApp()->platform(), "setVirtualOutputs", Qt::DirectConnection, Q_ARG(int, 2)); // set custom config which disables the Outline KSharedConfig::Ptr config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); KConfigGroup group = config->group("Outline"); group.writeEntry(QStringLiteral("QmlPath"), QString("/does/not/exist.qml")); group.sync(); kwinApp()->setConfig(config); 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)); setenv("QT_QPA_PLATFORM", "wayland", true); waylandServer()->initWorkspace(); } void StrutsTest::init() { QVERIFY(Test::setupWaylandConnection(Test::AdditionalWaylandInterface::PlasmaShell)); m_compositor = Test::waylandCompositor(); m_plasmaShell = Test::waylandPlasmaShell(); screens()->setCurrent(0); Cursor::setPos(QPoint(640, 512)); QVERIFY(waylandServer()->clients().isEmpty()); } void StrutsTest::cleanup() { Test::destroyWaylandConnection(); } void StrutsTest::testWaylandStruts_data() { QTest::addColumn>("windowGeometries"); QTest::addColumn("screen0Maximized"); QTest::addColumn("screen1Maximized"); QTest::addColumn("workArea"); QTest::addColumn("restrictedMoveArea"); QTest::newRow("bottom/0") << QVector{QRect(0, 992, 1280, 32)} << QRect(0, 0, 1280, 992) << QRect(1280, 0, 1280, 1024) << QRect(0, 0, 2560, 992) << QRegion(0, 992, 1280, 32); QTest::newRow("bottom/1") << QVector{QRect(1280, 992, 1280, 32)} << QRect(0, 0, 1280, 1024) << QRect(1280, 0, 1280, 992) << QRect(0, 0, 2560, 992) << QRegion(1280, 992, 1280, 32); QTest::newRow("top/0") << QVector{QRect(0, 0, 1280, 32)} << QRect(0, 32, 1280, 992) << QRect(1280, 0, 1280, 1024) << QRect(0, 32, 2560, 992) << QRegion(0, 0, 1280, 32); QTest::newRow("top/1") << QVector{QRect(1280, 0, 1280, 32)} << QRect(0, 0, 1280, 1024) << QRect(1280, 32, 1280, 992) << QRect(0, 32, 2560, 992) << QRegion(1280, 0, 1280, 32); QTest::newRow("left/0") << QVector{QRect(0, 0, 32, 1024)} << QRect(32, 0, 1248, 1024) << QRect(1280, 0, 1280, 1024) << QRect(32, 0, 2528, 1024) << QRegion(0, 0, 32, 1024); QTest::newRow("left/1") << QVector{QRect(1280, 0, 32, 1024)} << QRect(0, 0, 1280, 1024) << QRect(1312, 0, 1248, 1024) << QRect(0, 0, 2560, 1024) << QRegion(1280, 0, 32, 1024); QTest::newRow("right/0") << QVector{QRect(1248, 0, 32, 1024)} << QRect(0, 0, 1248, 1024) << QRect(1280, 0, 1280, 1024) << QRect(0, 0, 2560, 1024) << QRegion(1248, 0, 32, 1024); QTest::newRow("right/1") << QVector{QRect(2528, 0, 32, 1024)} << QRect(0, 0, 1280, 1024) << QRect(1280, 0, 1248, 1024) << QRect(0, 0, 2528, 1024) << QRegion(2528, 0, 32, 1024); // same with partial panels not covering the whole area QTest::newRow("part bottom/0") << QVector{QRect(100, 992, 1080, 32)} << QRect(0, 0, 1280, 992) << QRect(1280, 0, 1280, 1024) << QRect(0, 0, 2560, 992) << QRegion(100, 992, 1080, 32); QTest::newRow("part bottom/1") << QVector{QRect(1380, 992, 1080, 32)} << QRect(0, 0, 1280, 1024) << QRect(1280, 0, 1280, 992) << QRect(0, 0, 2560, 992) << QRegion(1380, 992, 1080, 32); QTest::newRow("part top/0") << QVector{QRect(100, 0, 1080, 32)} << QRect(0, 32, 1280, 992) << QRect(1280, 0, 1280, 1024) << QRect(0, 32, 2560, 992) << QRegion(100, 0, 1080, 32); QTest::newRow("part top/1") << QVector{QRect(1380, 0, 1080, 32)} << QRect(0, 0, 1280, 1024) << QRect(1280, 32, 1280, 992) << QRect(0, 32, 2560, 992) << QRegion(1380, 0, 1080, 32); QTest::newRow("part left/0") << QVector{QRect(0, 100, 32, 824)} << QRect(32, 0, 1248, 1024) << QRect(1280, 0, 1280, 1024) << QRect(32, 0, 2528, 1024) << QRegion(0, 100, 32, 824); QTest::newRow("part left/1") << QVector{QRect(1280, 100, 32, 824)} << QRect(0, 0, 1280, 1024) << QRect(1312, 0, 1248, 1024) << QRect(0, 0, 2560, 1024) << QRegion(1280, 100, 32, 824); QTest::newRow("part right/0") << QVector{QRect(1248, 100, 32, 824)} << QRect(0, 0, 1248, 1024) << QRect(1280, 0, 1280, 1024) << QRect(0, 0, 2560, 1024) << QRegion(1248, 100, 32, 824); QTest::newRow("part right/1") << QVector{QRect(2528, 100, 32, 824)} << QRect(0, 0, 1280, 1024) << QRect(1280, 0, 1248, 1024) << QRect(0, 0, 2528, 1024) << QRegion(2528, 100, 32, 824); // multiple panels QTest::newRow("two bottom panels") << QVector{QRect(100, 992, 1080, 32), QRect(1380, 984, 1080, 40)} << QRect(0, 0, 1280, 992) << QRect(1280, 0, 1280, 984) << QRect(0, 0, 2560, 984) << QRegion(100, 992, 1080, 32).united(QRegion(1380, 984, 1080, 40)); QTest::newRow("two left panels") << QVector{QRect(0, 10, 32, 390), QRect(0, 450, 40, 100)} << QRect(40, 0, 1240, 1024) << QRect(1280, 0, 1280, 1024) << QRect(40, 0, 2520, 1024) << QRegion(0, 10, 32, 390).united(QRegion(0, 450, 40, 100)); } void StrutsTest::testWaylandStruts() { // this test verifies that struts on Wayland panels are handled correctly using namespace KWayland::Client; // no, struts yet QVERIFY(waylandServer()->clients().isEmpty()); // first screen QCOMPARE(workspace()->clientArea(PlacementArea, 0, 1), QRect(0, 0, 1280, 1024)); QCOMPARE(workspace()->clientArea(MovementArea, 0, 1), QRect(0, 0, 1280, 1024)); QCOMPARE(workspace()->clientArea(MaximizeArea, 0, 1), QRect(0, 0, 1280, 1024)); QCOMPARE(workspace()->clientArea(MaximizeFullArea, 0, 1), QRect(0, 0, 1280, 1024)); QCOMPARE(workspace()->clientArea(FullScreenArea, 0, 1), QRect(0, 0, 1280, 1024)); QCOMPARE(workspace()->clientArea(ScreenArea, 0, 1), QRect(0, 0, 1280, 1024)); // second screen QCOMPARE(workspace()->clientArea(PlacementArea, 1, 1), QRect(1280, 0, 1280, 1024)); QCOMPARE(workspace()->clientArea(MovementArea, 1, 1), QRect(1280, 0, 1280, 1024)); QCOMPARE(workspace()->clientArea(MaximizeArea, 1, 1), QRect(1280, 0, 1280, 1024)); QCOMPARE(workspace()->clientArea(MaximizeFullArea, 1, 1), QRect(1280, 0, 1280, 1024)); QCOMPARE(workspace()->clientArea(FullScreenArea, 1, 1), QRect(1280, 0, 1280, 1024)); QCOMPARE(workspace()->clientArea(ScreenArea, 1, 1), QRect(1280, 0, 1280, 1024)); // combined QCOMPARE(workspace()->clientArea(WorkArea, 0, 1), QRect(0, 0, 2560, 1024)); QCOMPARE(workspace()->clientArea(FullArea, 0, 1), QRect(0, 0, 2560, 1024)); QCOMPARE(workspace()->restrictedMoveArea(-1), QRegion()); QFETCH(QVector, windowGeometries); // create the panels QHash clients; for (auto it = windowGeometries.constBegin(), end = windowGeometries.constEnd(); it != end; it++) { const QRect windowGeometry = *it; Surface *surface = Test::createSurface(m_compositor); XdgShellSurface *shellSurface = Test::createXdgShellStableSurface(surface, surface, Test::CreationSetup::CreateOnly); PlasmaShellSurface *plasmaSurface = m_plasmaShell->createSurface(surface, surface); plasmaSurface->setPosition(windowGeometry.topLeft()); plasmaSurface->setRole(PlasmaShellSurface::Role::Panel); Test::initXdgShellSurface(surface, shellSurface); // map the window auto c = Test::renderAndWaitForShown(surface, windowGeometry.size(), Qt::red, QImage::Format_RGB32); QVERIFY(c); QVERIFY(!c->isActive()); QCOMPARE(c->frameGeometry(), windowGeometry); QVERIFY(c->isDock()); QVERIFY(c->hasStrut()); clients.insert(surface, c); } // some props are independent of struts - those first // screen 0 QCOMPARE(workspace()->clientArea(MovementArea, 0, 1), QRect(0, 0, 1280, 1024)); QCOMPARE(workspace()->clientArea(MaximizeFullArea, 0, 1), QRect(0, 0, 1280, 1024)); QCOMPARE(workspace()->clientArea(FullScreenArea, 0, 1), QRect(0, 0, 1280, 1024)); QCOMPARE(workspace()->clientArea(ScreenArea, 0, 1), QRect(0, 0, 1280, 1024)); // screen 1 QCOMPARE(workspace()->clientArea(MovementArea, 1, 1), QRect(1280, 0, 1280, 1024)); QCOMPARE(workspace()->clientArea(MaximizeFullArea, 1, 1), QRect(1280, 0, 1280, 1024)); QCOMPARE(workspace()->clientArea(FullScreenArea, 1, 1), QRect(1280, 0, 1280, 1024)); QCOMPARE(workspace()->clientArea(ScreenArea, 1, 1), QRect(1280, 0, 1280, 1024)); // combined QCOMPARE(workspace()->clientArea(FullArea, 0, 1), QRect(0, 0, 2560, 1024)); // now verify the actual updated client areas QTEST(workspace()->clientArea(PlacementArea, 0, 1), "screen0Maximized"); QTEST(workspace()->clientArea(MaximizeArea, 0, 1), "screen0Maximized"); QTEST(workspace()->clientArea(PlacementArea, 1, 1), "screen1Maximized"); QTEST(workspace()->clientArea(MaximizeArea, 1, 1), "screen1Maximized"); QTEST(workspace()->clientArea(WorkArea, 0, 1), "workArea"); QTEST(workspace()->restrictedMoveArea(-1), "restrictedMoveArea"); // delete all surfaces for (auto it = clients.begin(); it != clients.end(); it++) { QSignalSpy destroyedSpy(it.value(), &QObject::destroyed); QVERIFY(destroyedSpy.isValid()); delete it.key(); QVERIFY(destroyedSpy.wait()); } QCOMPARE(workspace()->restrictedMoveArea(-1), QRegion()); } void StrutsTest::testMoveWaylandPanel() { // this test verifies that repositioning a Wayland panel updates the client area using namespace KWayland::Client; const QRect windowGeometry(0, 1000, 1280, 24); QScopedPointer surface(Test::createSurface()); QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data(), surface.data(), Test::CreationSetup::CreateOnly)); QScopedPointer plasmaSurface(m_plasmaShell->createSurface(surface.data())); plasmaSurface->setPosition(windowGeometry.topLeft()); plasmaSurface->setRole(PlasmaShellSurface::Role::Panel); Test::initXdgShellSurface(surface.data(), shellSurface.data()); // map the window auto c = Test::renderAndWaitForShown(surface.data(), windowGeometry.size(), Qt::red, QImage::Format_RGB32); QVERIFY(c); QVERIFY(!c->isActive()); QCOMPARE(c->frameGeometry(), windowGeometry); QVERIFY(c->isDock()); QVERIFY(c->hasStrut()); QCOMPARE(workspace()->clientArea(PlacementArea, 0, 1), QRect(0, 0, 1280, 1000)); QCOMPARE(workspace()->clientArea(MaximizeArea, 0, 1), QRect(0, 0, 1280, 1000)); QCOMPARE(workspace()->clientArea(PlacementArea, 1, 1), QRect(1280, 0, 1280, 1024)); QCOMPARE(workspace()->clientArea(MaximizeArea, 1, 1), QRect(1280, 0, 1280, 1024)); QCOMPARE(workspace()->clientArea(WorkArea, 0, 1), QRect(0, 0, 2560, 1000)); - QSignalSpy geometryChangedSpy(c, &XdgShellClient::geometryShapeChanged); - QVERIFY(geometryChangedSpy.isValid()); + QSignalSpy frameGeometryChangedSpy(c, &XdgShellClient::frameGeometryChanged); + QVERIFY(frameGeometryChangedSpy.isValid()); plasmaSurface->setPosition(QPoint(1280, 1000)); - QVERIFY(geometryChangedSpy.wait()); + QVERIFY(frameGeometryChangedSpy.wait()); QCOMPARE(c->frameGeometry(), QRect(1280, 1000, 1280, 24)); QCOMPARE(workspace()->clientArea(PlacementArea, 0, 1), QRect(0, 0, 1280, 1024)); QCOMPARE(workspace()->clientArea(MaximizeArea, 0, 1), QRect(0, 0, 1280, 1024)); QCOMPARE(workspace()->clientArea(PlacementArea, 1, 1), QRect(1280, 0, 1280, 1000)); QCOMPARE(workspace()->clientArea(MaximizeArea, 1, 1), QRect(1280, 0, 1280, 1000)); QCOMPARE(workspace()->clientArea(WorkArea, 0, 1), QRect(0, 0, 2560, 1000)); } void StrutsTest::testWaylandMobilePanel() { using namespace KWayland::Client; //First enable maxmizing policy KConfigGroup group = kwinApp()->config()->group("Windows"); group.writeEntry("Placement", "Maximizing"); group.sync(); workspace()->slotReconfigure(); // create first top panel const QRect windowGeometry(0, 0, 1280, 60); QScopedPointer surface(Test::createSurface()); QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data(), surface.data(), Test::CreationSetup::CreateOnly)); QScopedPointer plasmaSurface(m_plasmaShell->createSurface(surface.data())); plasmaSurface->setPosition(windowGeometry.topLeft()); plasmaSurface->setRole(PlasmaShellSurface::Role::Panel); Test::initXdgShellSurface(surface.data(), shellSurface.data()); // map the first panel auto c = Test::renderAndWaitForShown(surface.data(), windowGeometry.size(), Qt::red, QImage::Format_RGB32); QVERIFY(c); QVERIFY(!c->isActive()); QCOMPARE(c->frameGeometry(), windowGeometry); QVERIFY(c->isDock()); QVERIFY(c->hasStrut()); QCOMPARE(workspace()->clientArea(PlacementArea, 0, 1), QRect(0, 60, 1280, 964)); QCOMPARE(workspace()->clientArea(MaximizeArea, 0, 1), QRect(0, 60, 1280, 964)); QCOMPARE(workspace()->clientArea(PlacementArea, 1, 1), QRect(1280, 0, 1280, 1024)); QCOMPARE(workspace()->clientArea(MaximizeArea, 1, 1), QRect(1280, 0, 1280, 1024)); QCOMPARE(workspace()->clientArea(WorkArea, 0, 1), QRect(0, 60, 2560, 964)); // create another bottom panel const QRect windowGeometry2(0, 874, 1280, 150); QScopedPointer surface2(Test::createSurface()); QScopedPointer shellSurface2(Test::createXdgShellStableSurface(surface2.data(), surface2.data(), Test::CreationSetup::CreateOnly)); QScopedPointer plasmaSurface2(m_plasmaShell->createSurface(surface2.data())); plasmaSurface2->setPosition(windowGeometry2.topLeft()); plasmaSurface2->setRole(PlasmaShellSurface::Role::Panel); Test::initXdgShellSurface(surface2.data(), shellSurface2.data()); auto c1 = Test::renderAndWaitForShown(surface2.data(), windowGeometry2.size(), Qt::blue, QImage::Format_RGB32); QVERIFY(c1); QVERIFY(!c1->isActive()); QCOMPARE(c1->frameGeometry(), windowGeometry2); QVERIFY(c1->isDock()); QVERIFY(c1->hasStrut()); QCOMPARE(workspace()->clientArea(PlacementArea, 0, 1), QRect(0, 60, 1280, 814)); QCOMPARE(workspace()->clientArea(MaximizeArea, 0, 1), QRect(0, 60, 1280, 814)); QCOMPARE(workspace()->clientArea(PlacementArea, 1, 1), QRect(1280, 0, 1280, 1024)); QCOMPARE(workspace()->clientArea(MaximizeArea, 1, 1), QRect(1280, 0, 1280, 1024)); QCOMPARE(workspace()->clientArea(WorkArea, 0, 1), QRect(0, 60, 2560, 814)); // Destroy test clients. shellSurface.reset(); QVERIFY(Test::waitForWindowDestroyed(c)); shellSurface2.reset(); QVERIFY(Test::waitForWindowDestroyed(c1)); } void StrutsTest::testX11Struts_data() { QTest::addColumn("windowGeometry"); QTest::addColumn("leftStrut"); QTest::addColumn("rightStrut"); QTest::addColumn("topStrut"); QTest::addColumn("bottomStrut"); QTest::addColumn("leftStrutStart"); QTest::addColumn("leftStrutEnd"); QTest::addColumn("rightStrutStart"); QTest::addColumn("rightStrutEnd"); QTest::addColumn("topStrutStart"); QTest::addColumn("topStrutEnd"); QTest::addColumn("bottomStrutStart"); QTest::addColumn("bottomStrutEnd"); QTest::addColumn("screen0Maximized"); QTest::addColumn("screen1Maximized"); QTest::addColumn("workArea"); QTest::addColumn("restrictedMoveArea"); QTest::newRow("bottom panel/no strut") << QRect(0, 980, 1280, 44) << 0 << 0 << 0 << 0 << 0 << 0 << 0 << 0 << 0 << 0 << 0 << 0 << QRect(0, 0, 1280, 1024) << QRect(1280, 0, 1280, 1024) << QRect(0, 0, 2560, 1024) << QRegion(); QTest::newRow("bottom panel/strut") << QRect(0, 980, 1280, 44) << 0 << 0 << 0 << 44 << 0 << 0 << 0 << 0 << 0 << 0 << 0 << 1279 << QRect(0, 0, 1280, 980) << QRect(1280, 0, 1280, 1024) << QRect(0, 0, 2560, 980) << QRegion(0, 980, 1279, 44); QTest::newRow("top panel/no strut") << QRect(0, 0, 1280, 44) << 0 << 0 << 0 << 0 << 0 << 0 << 0 << 0 << 0 << 0 << 0 << 0 << QRect(0, 0, 1280, 1024) << QRect(1280, 0, 1280, 1024) << QRect(0, 0, 2560, 1024) << QRegion(); QTest::newRow("top panel/strut") << QRect(0, 0, 1280, 44) << 0 << 0 << 44 << 0 << 0 << 0 << 0 << 0 << 0 << 1279 << 0 << 0 << QRect(0, 44, 1280, 980) << QRect(1280, 0, 1280, 1024) << QRect(0, 44, 2560, 980) << QRegion(0, 0, 1279, 44); QTest::newRow("left panel/no strut") << QRect(0, 0, 60, 1024) << 0 << 0 << 0 << 0 << 0 << 0 << 0 << 0 << 0 << 0 << 0 << 0 << QRect(0, 0, 1280, 1024) << QRect(1280, 0, 1280, 1024) << QRect(0, 0, 2560, 1024) << QRegion(); QTest::newRow("left panel/strut") << QRect(0, 0, 60, 1024) << 60 << 0 << 0 << 0 << 0 << 1023 << 0 << 0 << 0 << 0 << 0 << 0 << QRect(60, 0, 1220, 1024) << QRect(1280, 0, 1280, 1024) << QRect(60, 0, 2500, 1024) << QRegion(0, 0, 60, 1023); QTest::newRow("right panel/no strut") << QRect(1220, 0, 60, 1024) << 0 << 0 << 0 << 0 << 0 << 0 << 0 << 0 << 0 << 0 << 0 << 0 << QRect(0, 0, 1280, 1024) << QRect(1280, 0, 1280, 1024) << QRect(0, 0, 2560, 1024) << QRegion(); QTest::newRow("right panel/strut") << QRect(1220, 0, 60, 1024) << 0 << 1340 << 0 << 0 << 0 << 0 << 0 << 1023 << 0 << 0 << 0 << 0 << QRect(0, 0, 1220, 1024) << QRect(1280, 0, 1280, 1024) << QRect(0, 0, 2560, 1024) << QRegion(1220, 0, 60, 1023); // second screen QTest::newRow("bottom panel 1/no strut") << QRect(1280, 980, 1280, 44) << 0 << 0 << 0 << 0 << 0 << 0 << 0 << 0 << 0 << 0 << 0 << 0 << QRect(0, 0, 1280, 1024) << QRect(1280, 0, 1280, 1024) << QRect(0, 0, 2560, 1024) << QRegion(); QTest::newRow("bottom panel 1/strut") << QRect(1280, 980, 1280, 44) << 0 << 0 << 0 << 44 << 0 << 0 << 0 << 0 << 0 << 0 << 1280 << 2559 << QRect(0, 0, 1280, 1024) << QRect(1280, 0, 1280, 980) << QRect(0, 0, 2560, 980) << QRegion(1280, 980, 1279, 44); QTest::newRow("top panel 1/no strut") << QRect(1280, 0, 1280, 44) << 0 << 0 << 0 << 0 << 0 << 0 << 0 << 0 << 0 << 0 << 0 << 0 << QRect(0, 0, 1280, 1024) << QRect(1280, 0, 1280, 1024) << QRect(0, 0, 2560, 1024) << QRegion(); QTest::newRow("top panel 1 /strut") << QRect(1280, 0, 1280, 44) << 0 << 0 << 44 << 0 << 0 << 0 << 0 << 0 << 1280 << 2559 << 0 << 0 << QRect(0, 0, 1280, 1024) << QRect(1280, 44, 1280, 980) << QRect(0, 44, 2560, 980) << QRegion(1280, 0, 1279, 44); QTest::newRow("left panel 1/no strut") << QRect(1280, 0, 60, 1024) << 0 << 0 << 0 << 0 << 0 << 0 << 0 << 0 << 0 << 0 << 0 << 0 << QRect(0, 0, 1280, 1024) << QRect(1280, 0, 1280, 1024) << QRect(0, 0, 2560, 1024) << QRegion(); QTest::newRow("left panel 1/strut") << QRect(1280, 0, 60, 1024) << 1340 << 0 << 0 << 0 << 0 << 1023 << 0 << 0 << 0 << 0 << 0 << 0 << QRect(0, 0, 1280, 1024) << QRect(1340, 0, 1220, 1024) << QRect(0, 0, 2560, 1024) << QRegion(1280, 0, 60, 1023); // invalid struts QTest::newRow("bottom panel/ invalid strut") << QRect(0, 980, 1280, 44) << 1280 << 0 << 0 << 44 << 980 << 1024 << 0 << 0 << 0 << 0 << 0 << 1279 << QRect(0, 0, 1280, 1024) << QRect(1280, 0, 1280, 1024) << QRect(0, 0, 2560, 1024) << QRegion(0, 980, 1280, 44); QTest::newRow("top panel/ invalid strut") << QRect(0, 0, 1280, 44) << 1280 << 0 << 44 << 0 << 0 << 44 << 0 << 0 << 0 << 1279 << 0 << 0 << QRect(0, 0, 1280, 1024) << QRect(1280, 0, 1280, 1024) << QRect(0, 0, 2560, 1024) << QRegion(0, 0, 1280, 44); QTest::newRow("top panel/invalid strut 2") << QRect(0, 0, 1280, 44) << 0 << 0 << 1024 << 0 << 0 << 0 << 0 << 0 << 0 << 1279 << 0 << 0 << QRect(0, 0, 1280, 1024) << QRect(1280, 0, 1280, 1024) << QRect(0, 0, 2560, 1024) << QRegion(0, 0, 1279, 1024); } struct XcbConnectionDeleter { static inline void cleanup(xcb_connection_t *pointer) { xcb_disconnect(pointer); } }; void StrutsTest::testX11Struts() { // this test verifies that struts are applied correctly for X11 windows // no, struts yet // first screen QCOMPARE(workspace()->clientArea(PlacementArea, 0, 1), QRect(0, 0, 1280, 1024)); QCOMPARE(workspace()->clientArea(MovementArea, 0, 1), QRect(0, 0, 1280, 1024)); QCOMPARE(workspace()->clientArea(MaximizeArea, 0, 1), QRect(0, 0, 1280, 1024)); QCOMPARE(workspace()->clientArea(MaximizeFullArea, 0, 1), QRect(0, 0, 1280, 1024)); QCOMPARE(workspace()->clientArea(FullScreenArea, 0, 1), QRect(0, 0, 1280, 1024)); QCOMPARE(workspace()->clientArea(ScreenArea, 0, 1), QRect(0, 0, 1280, 1024)); // second screen QCOMPARE(workspace()->clientArea(PlacementArea, 1, 1), QRect(1280, 0, 1280, 1024)); QCOMPARE(workspace()->clientArea(MovementArea, 1, 1), QRect(1280, 0, 1280, 1024)); QCOMPARE(workspace()->clientArea(MaximizeArea, 1, 1), QRect(1280, 0, 1280, 1024)); QCOMPARE(workspace()->clientArea(MaximizeFullArea, 1, 1), QRect(1280, 0, 1280, 1024)); QCOMPARE(workspace()->clientArea(FullScreenArea, 1, 1), QRect(1280, 0, 1280, 1024)); QCOMPARE(workspace()->clientArea(ScreenArea, 1, 1), QRect(1280, 0, 1280, 1024)); // combined QCOMPARE(workspace()->clientArea(WorkArea, 0, 1), QRect(0, 0, 2560, 1024)); QCOMPARE(workspace()->clientArea(FullArea, 0, 1), QRect(0, 0, 2560, 1024)); QCOMPARE(workspace()->restrictedMoveArea(-1), QRegion()); // create an xcb window QScopedPointer c(xcb_connect(nullptr, nullptr)); QVERIFY(!xcb_connection_has_error(c.data())); xcb_window_t w = xcb_generate_id(c.data()); QFETCH(QRect, windowGeometry); xcb_create_window(c.data(), XCB_COPY_FROM_PARENT, w, rootWindow(), windowGeometry.x(), windowGeometry.y(), windowGeometry.width(), windowGeometry.height(), 0, XCB_WINDOW_CLASS_INPUT_OUTPUT, XCB_COPY_FROM_PARENT, 0, nullptr); xcb_size_hints_t hints; memset(&hints, 0, sizeof(hints)); xcb_icccm_size_hints_set_position(&hints, 1, windowGeometry.x(), windowGeometry.y()); xcb_icccm_size_hints_set_size(&hints, 1, windowGeometry.width(), windowGeometry.height()); xcb_icccm_set_wm_normal_hints(c.data(), w, &hints); NETWinInfo info(c.data(), w, rootWindow(), NET::WMAllProperties, NET::WM2AllProperties); info.setWindowType(NET::Dock); // set the extended strut QFETCH(int, leftStrut); QFETCH(int, rightStrut); QFETCH(int, topStrut); QFETCH(int, bottomStrut); QFETCH(int, leftStrutStart); QFETCH(int, leftStrutEnd); QFETCH(int, rightStrutStart); QFETCH(int, rightStrutEnd); QFETCH(int, topStrutStart); QFETCH(int, topStrutEnd); QFETCH(int, bottomStrutStart); QFETCH(int, bottomStrutEnd); NETExtendedStrut strut; strut.left_start = leftStrutStart; strut.left_end = leftStrutEnd; strut.left_width = leftStrut; strut.right_start = rightStrutStart; strut.right_end = rightStrutEnd; strut.right_width = rightStrut; strut.top_start = topStrutStart; strut.top_end = topStrutEnd; strut.top_width = topStrut; strut.bottom_start = bottomStrutStart; strut.bottom_end = bottomStrutEnd; strut.bottom_width = bottomStrut; info.setExtendedStrut(strut); xcb_map_window(c.data(), w); xcb_flush(c.data()); // we should get a client for it QSignalSpy windowCreatedSpy(workspace(), &Workspace::clientAdded); QVERIFY(windowCreatedSpy.isValid()); QVERIFY(windowCreatedSpy.wait()); X11Client *client = windowCreatedSpy.first().first().value(); QVERIFY(client); QCOMPARE(client->window(), w); QVERIFY(!client->isDecorated()); QCOMPARE(client->windowType(), NET::Dock); QCOMPARE(client->frameGeometry(), windowGeometry); // this should have affected the client area // some props are independent of struts - those first // screen 0 QCOMPARE(workspace()->clientArea(MovementArea, 0, 1), QRect(0, 0, 1280, 1024)); QCOMPARE(workspace()->clientArea(MaximizeFullArea, 0, 1), QRect(0, 0, 1280, 1024)); QCOMPARE(workspace()->clientArea(FullScreenArea, 0, 1), QRect(0, 0, 1280, 1024)); QCOMPARE(workspace()->clientArea(ScreenArea, 0, 1), QRect(0, 0, 1280, 1024)); // screen 1 QCOMPARE(workspace()->clientArea(MovementArea, 1, 1), QRect(1280, 0, 1280, 1024)); QCOMPARE(workspace()->clientArea(MaximizeFullArea, 1, 1), QRect(1280, 0, 1280, 1024)); QCOMPARE(workspace()->clientArea(FullScreenArea, 1, 1), QRect(1280, 0, 1280, 1024)); QCOMPARE(workspace()->clientArea(ScreenArea, 1, 1), QRect(1280, 0, 1280, 1024)); // combined QCOMPARE(workspace()->clientArea(FullArea, 0, 1), QRect(0, 0, 2560, 1024)); // now verify the actual updated client areas QTEST(workspace()->clientArea(PlacementArea, 0, 1), "screen0Maximized"); QTEST(workspace()->clientArea(MaximizeArea, 0, 1), "screen0Maximized"); QTEST(workspace()->clientArea(PlacementArea, 1, 1), "screen1Maximized"); QTEST(workspace()->clientArea(MaximizeArea, 1, 1), "screen1Maximized"); QTEST(workspace()->clientArea(WorkArea, 0, 1), "workArea"); QTEST(workspace()->restrictedMoveArea(-1), "restrictedMoveArea"); // and destroy the window again xcb_unmap_window(c.data(), w); xcb_destroy_window(c.data(), w); xcb_flush(c.data()); c.reset(); QSignalSpy windowClosedSpy(client, &X11Client::windowClosed); QVERIFY(windowClosedSpy.isValid()); QVERIFY(windowClosedSpy.wait()); // now struts should be removed again QCOMPARE(workspace()->clientArea(PlacementArea, 0, 1), QRect(0, 0, 1280, 1024)); QCOMPARE(workspace()->clientArea(MovementArea, 0, 1), QRect(0, 0, 1280, 1024)); QCOMPARE(workspace()->clientArea(MaximizeArea, 0, 1), QRect(0, 0, 1280, 1024)); QCOMPARE(workspace()->clientArea(MaximizeFullArea, 0, 1), QRect(0, 0, 1280, 1024)); QCOMPARE(workspace()->clientArea(FullScreenArea, 0, 1), QRect(0, 0, 1280, 1024)); QCOMPARE(workspace()->clientArea(ScreenArea, 0, 1), QRect(0, 0, 1280, 1024)); // second screen QCOMPARE(workspace()->clientArea(PlacementArea, 1, 1), QRect(1280, 0, 1280, 1024)); QCOMPARE(workspace()->clientArea(MovementArea, 1, 1), QRect(1280, 0, 1280, 1024)); QCOMPARE(workspace()->clientArea(MaximizeArea, 1, 1), QRect(1280, 0, 1280, 1024)); QCOMPARE(workspace()->clientArea(MaximizeFullArea, 1, 1), QRect(1280, 0, 1280, 1024)); QCOMPARE(workspace()->clientArea(FullScreenArea, 1, 1), QRect(1280, 0, 1280, 1024)); QCOMPARE(workspace()->clientArea(ScreenArea, 1, 1), QRect(1280, 0, 1280, 1024)); // combined QCOMPARE(workspace()->clientArea(WorkArea, 0, 1), QRect(0, 0, 2560, 1024)); QCOMPARE(workspace()->clientArea(FullArea, 0, 1), QRect(0, 0, 2560, 1024)); QCOMPARE(workspace()->restrictedMoveArea(-1), QRegion()); } void StrutsTest::test363804() { // this test verifies the condition described in BUG 363804 // two screens in a vertical setup, aligned to right border with panel on the bottom screen const QVector geometries{QRect(0, 0, 1920, 1080), QRect(554, 1080, 1366, 768)}; QMetaObject::invokeMethod(kwinApp()->platform(), "setVirtualOutputs", Qt::DirectConnection, Q_ARG(int, 2), Q_ARG(QVector, geometries)); QCOMPARE(screens()->geometry(0), geometries.at(0)); QCOMPARE(screens()->geometry(1), geometries.at(1)); QCOMPARE(screens()->geometry(), QRect(0, 0, 1920, 1848)); // create an xcb window QScopedPointer c(xcb_connect(nullptr, nullptr)); QVERIFY(!xcb_connection_has_error(c.data())); xcb_window_t w = xcb_generate_id(c.data()); const QRect windowGeometry(554, 1812, 1366, 36); xcb_create_window(c.data(), XCB_COPY_FROM_PARENT, w, rootWindow(), windowGeometry.x(), windowGeometry.y(), windowGeometry.width(), windowGeometry.height(), 0, XCB_WINDOW_CLASS_INPUT_OUTPUT, XCB_COPY_FROM_PARENT, 0, nullptr); xcb_size_hints_t hints; memset(&hints, 0, sizeof(hints)); xcb_icccm_size_hints_set_position(&hints, 1, windowGeometry.x(), windowGeometry.y()); xcb_icccm_size_hints_set_size(&hints, 1, windowGeometry.width(), windowGeometry.height()); xcb_icccm_set_wm_normal_hints(c.data(), w, &hints); NETWinInfo info(c.data(), w, rootWindow(), NET::WMAllProperties, NET::WM2AllProperties); info.setWindowType(NET::Dock); NETExtendedStrut strut; strut.left_start = 0; strut.left_end = 0; strut.left_width = 0; strut.right_start = 0; strut.right_end = 0; strut.right_width = 0; strut.top_start = 0; strut.top_end = 0; strut.top_width = 0; strut.bottom_start = 554; strut.bottom_end = 1919; strut.bottom_width = 36; info.setExtendedStrut(strut); xcb_map_window(c.data(), w); xcb_flush(c.data()); // we should get a client for it QSignalSpy windowCreatedSpy(workspace(), &Workspace::clientAdded); QVERIFY(windowCreatedSpy.isValid()); QVERIFY(windowCreatedSpy.wait()); X11Client *client = windowCreatedSpy.first().first().value(); QVERIFY(client); QCOMPARE(client->window(), w); QVERIFY(!client->isDecorated()); QCOMPARE(client->windowType(), NET::Dock); QCOMPARE(client->frameGeometry(), windowGeometry); // now verify the actual updated client areas QCOMPARE(workspace()->clientArea(PlacementArea, 0, 1), geometries.at(0)); QCOMPARE(workspace()->clientArea(MaximizeArea, 0, 1), geometries.at(0)); QCOMPARE(workspace()->clientArea(PlacementArea, 1, 1), QRect(554, 1080, 1366, 732)); QCOMPARE(workspace()->clientArea(MaximizeArea, 1, 1), QRect(554, 1080, 1366, 732)); QCOMPARE(workspace()->clientArea(WorkArea, 0, 1), QRect(0, 0, 1920, 1812)); // and destroy the window again xcb_unmap_window(c.data(), w); xcb_destroy_window(c.data(), w); xcb_flush(c.data()); c.reset(); QSignalSpy windowClosedSpy(client, &X11Client::windowClosed); QVERIFY(windowClosedSpy.isValid()); QVERIFY(windowClosedSpy.wait()); } void StrutsTest::testLeftScreenSmallerBottomAligned() { // this test verifies a two screen setup with the left screen smaller than the right and bottom aligned // the panel is on the top of the left screen, thus not at 0/0 // what this test in addition tests is whether a window larger than the left screen is not placed into // the dead area const QVector geometries{QRect(0, 282, 1366, 768), QRect(1366, 0, 1680, 1050)}; QMetaObject::invokeMethod(kwinApp()->platform(), "setVirtualOutputs", Qt::DirectConnection, Q_ARG(int, 2), Q_ARG(QVector, geometries)); QCOMPARE(screens()->geometry(0), geometries.at(0)); QCOMPARE(screens()->geometry(1), geometries.at(1)); QCOMPARE(screens()->geometry(), QRect(0, 0, 3046, 1050)); // create the panel QScopedPointer c(xcb_connect(nullptr, nullptr)); QVERIFY(!xcb_connection_has_error(c.data())); xcb_window_t w = xcb_generate_id(c.data()); const QRect windowGeometry(0, 282, 1366, 24); xcb_create_window(c.data(), XCB_COPY_FROM_PARENT, w, rootWindow(), windowGeometry.x(), windowGeometry.y(), windowGeometry.width(), windowGeometry.height(), 0, XCB_WINDOW_CLASS_INPUT_OUTPUT, XCB_COPY_FROM_PARENT, 0, nullptr); xcb_size_hints_t hints; memset(&hints, 0, sizeof(hints)); xcb_icccm_size_hints_set_position(&hints, 1, windowGeometry.x(), windowGeometry.y()); xcb_icccm_size_hints_set_size(&hints, 1, windowGeometry.width(), windowGeometry.height()); xcb_icccm_set_wm_normal_hints(c.data(), w, &hints); NETWinInfo info(c.data(), w, rootWindow(), NET::WMAllProperties, NET::WM2AllProperties); info.setWindowType(NET::Dock); NETExtendedStrut strut; strut.left_start = 0; strut.left_end = 0; strut.left_width = 0; strut.right_start = 0; strut.right_end = 0; strut.right_width = 0; strut.top_start = 0; strut.top_end = 1365; strut.top_width = 306; strut.bottom_start = 0; strut.bottom_end = 0; strut.bottom_width = 0; info.setExtendedStrut(strut); xcb_map_window(c.data(), w); xcb_flush(c.data()); // we should get a client for it QSignalSpy windowCreatedSpy(workspace(), &Workspace::clientAdded); QVERIFY(windowCreatedSpy.isValid()); QVERIFY(windowCreatedSpy.wait()); X11Client *client = windowCreatedSpy.first().first().value(); QVERIFY(client); QCOMPARE(client->window(), w); QVERIFY(!client->isDecorated()); QCOMPARE(client->windowType(), NET::Dock); QCOMPARE(client->frameGeometry(), windowGeometry); // now verify the actual updated client areas QCOMPARE(workspace()->clientArea(PlacementArea, 0, 1), QRect(0, 306, 1366, 744)); QCOMPARE(workspace()->clientArea(MaximizeArea, 0, 1), QRect(0, 306, 1366, 744)); QCOMPARE(workspace()->clientArea(PlacementArea, 1, 1), geometries.at(1)); QCOMPARE(workspace()->clientArea(MaximizeArea, 1, 1), geometries.at(1)); QCOMPARE(workspace()->clientArea(WorkArea, 0, 1), QRect(0, 0, 3046, 1050)); // now create a window which is larger than screen 0 xcb_window_t w2 = xcb_generate_id(c.data()); const QRect windowGeometry2(0, 26, 1280, 774); xcb_create_window(c.data(), XCB_COPY_FROM_PARENT, w2, rootWindow(), windowGeometry2.x(), windowGeometry2.y(), windowGeometry2.width(), windowGeometry2.height(), 0, XCB_WINDOW_CLASS_INPUT_OUTPUT, XCB_COPY_FROM_PARENT, 0, nullptr); xcb_size_hints_t hints2; memset(&hints2, 0, sizeof(hints2)); xcb_icccm_size_hints_set_min_size(&hints2, 868, 431); xcb_icccm_set_wm_normal_hints(c.data(), w2, &hints2); xcb_map_window(c.data(), w2); xcb_flush(c.data()); QVERIFY(windowCreatedSpy.wait()); X11Client *client2 = windowCreatedSpy.last().first().value(); QVERIFY(client2); QVERIFY(client2 != client); QVERIFY(client2->isDecorated()); QCOMPARE(client2->frameGeometry(), QRect(0, 306, 1366, 744)); QCOMPARE(client2->maximizeMode(), KWin::MaximizeFull); // destroy window again QSignalSpy normalWindowClosedSpy(client2, &X11Client::windowClosed); QVERIFY(normalWindowClosedSpy.isValid()); xcb_unmap_window(c.data(), w2); xcb_destroy_window(c.data(), w2); xcb_flush(c.data()); QVERIFY(normalWindowClosedSpy.wait()); // and destroy the window again xcb_unmap_window(c.data(), w); xcb_destroy_window(c.data(), w); xcb_flush(c.data()); c.reset(); QSignalSpy windowClosedSpy(client, &X11Client::windowClosed); QVERIFY(windowClosedSpy.isValid()); QVERIFY(windowClosedSpy.wait()); } void StrutsTest::testWindowMoveWithPanelBetweenScreens() { // this test verifies the condition of BUG // when moving a window with decorations in a restricted way it should pass from one screen // to the other even if there is a panel in between. // left screen must be smaller than right screen const QVector geometries{QRect(0, 282, 1366, 768), QRect(1366, 0, 1680, 1050)}; QMetaObject::invokeMethod(kwinApp()->platform(), "setVirtualOutputs", Qt::DirectConnection, Q_ARG(int, 2), Q_ARG(QVector, geometries)); QCOMPARE(screens()->geometry(0), geometries.at(0)); QCOMPARE(screens()->geometry(1), geometries.at(1)); QCOMPARE(screens()->geometry(), QRect(0, 0, 3046, 1050)); // create the panel on the right screen, left edge QScopedPointer c(xcb_connect(nullptr, nullptr)); QVERIFY(!xcb_connection_has_error(c.data())); xcb_window_t w = xcb_generate_id(c.data()); const QRect windowGeometry(1366, 0, 24, 1050); xcb_create_window(c.data(), XCB_COPY_FROM_PARENT, w, rootWindow(), windowGeometry.x(), windowGeometry.y(), windowGeometry.width(), windowGeometry.height(), 0, XCB_WINDOW_CLASS_INPUT_OUTPUT, XCB_COPY_FROM_PARENT, 0, nullptr); xcb_size_hints_t hints; memset(&hints, 0, sizeof(hints)); xcb_icccm_size_hints_set_position(&hints, 1, windowGeometry.x(), windowGeometry.y()); xcb_icccm_size_hints_set_size(&hints, 1, windowGeometry.width(), windowGeometry.height()); xcb_icccm_set_wm_normal_hints(c.data(), w, &hints); NETWinInfo info(c.data(), w, rootWindow(), NET::WMAllProperties, NET::WM2AllProperties); info.setWindowType(NET::Dock); NETExtendedStrut strut; strut.left_start = 0; strut.left_end = 1050; strut.left_width = 1366+24; strut.right_start = 0; strut.right_end = 0; strut.right_width = 0; strut.top_start = 0; strut.top_end = 0; strut.top_width = 0; strut.bottom_start = 0; strut.bottom_end = 0; strut.bottom_width = 0; info.setExtendedStrut(strut); xcb_map_window(c.data(), w); xcb_flush(c.data()); // we should get a client for it QSignalSpy windowCreatedSpy(workspace(), &Workspace::clientAdded); QVERIFY(windowCreatedSpy.isValid()); QVERIFY(windowCreatedSpy.wait()); X11Client *client = windowCreatedSpy.first().first().value(); QVERIFY(client); QCOMPARE(client->window(), w); QVERIFY(!client->isDecorated()); QCOMPARE(client->windowType(), NET::Dock); QCOMPARE(client->frameGeometry(), windowGeometry); // now verify the actual updated client areas QCOMPARE(workspace()->clientArea(PlacementArea, 0, 1), QRect(0, 282, 1366, 768)); QCOMPARE(workspace()->clientArea(MaximizeArea, 0, 1), QRect(0, 282, 1366, 768)); QCOMPARE(workspace()->clientArea(PlacementArea, 1, 1), QRect(1390, 0, 1656, 1050)); QCOMPARE(workspace()->clientArea(MaximizeArea, 1, 1), QRect(1390, 0, 1656, 1050)); QCOMPARE(workspace()->clientArea(WorkArea, 0, 1), QRect(0, 0, 3046, 1050)); QCOMPARE(workspace()->restrictedMoveArea(-1), QRegion(1366, 0, 24, 1050)); // create another window and try to move it xcb_window_t w2 = xcb_generate_id(c.data()); const QRect windowGeometry2(1500, 400, 200, 300); xcb_create_window(c.data(), XCB_COPY_FROM_PARENT, w2, rootWindow(), windowGeometry2.x(), windowGeometry2.y(), windowGeometry2.width(), windowGeometry2.height(), 0, XCB_WINDOW_CLASS_INPUT_OUTPUT, XCB_COPY_FROM_PARENT, 0, nullptr); xcb_size_hints_t hints2; memset(&hints2, 0, sizeof(hints2)); xcb_icccm_size_hints_set_position(&hints2, 1, windowGeometry2.x(), windowGeometry2.y()); xcb_icccm_size_hints_set_min_size(&hints2, 200, 300); xcb_icccm_set_wm_normal_hints(c.data(), w2, &hints2); xcb_map_window(c.data(), w2); xcb_flush(c.data()); QVERIFY(windowCreatedSpy.wait()); X11Client *client2 = windowCreatedSpy.last().first().value(); QVERIFY(client2); QVERIFY(client2 != client); QVERIFY(client2->isDecorated()); QCOMPARE(client2->clientSize(), QSize(200, 300)); QCOMPARE(client2->pos(), QPoint(1500, 400)); const QRect origGeo = client2->frameGeometry(); Cursor::setPos(origGeo.center()); workspace()->performWindowOperation(client2, Options::MoveOp); QTRY_COMPARE(workspace()->moveResizeClient(), client2); QVERIFY(client2->isMove()); // move to next screen - step is 8 pixel, so 800 pixel for (int i = 0; i < 100; i++) { client2->keyPressEvent(Qt::Key_Left); QTest::qWait(50); } client2->keyPressEvent(Qt::Key_Enter); QCOMPARE(client2->isMove(), false); QVERIFY(workspace()->moveResizeClient() == nullptr); QCOMPARE(client2->frameGeometry(), QRect(origGeo.translated(-800, 0))); } } WAYLANDTEST_MAIN(KWin::StrutsTest) #include "struts_test.moc" diff --git a/autotests/integration/transient_placement.cpp b/autotests/integration/transient_placement.cpp index 7a4e1b388..368395714 100644 --- a/autotests/integration/transient_placement.cpp +++ b/autotests/integration/transient_placement.cpp @@ -1,368 +1,368 @@ /******************************************************************** 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 "abstract_client.h" #include "cursor.h" #include "screenedge.h" #include "screens.h" #include "wayland_server.h" #include "workspace.h" #include "xdgshellclient.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace KWin { static const QString s_socketName = QStringLiteral("wayland_test_kwin_transient_placement-0"); class TransientPlacementTest : public QObject { Q_OBJECT private Q_SLOTS: void initTestCase(); void init(); void cleanup(); void testXdgPopup_data(); void testXdgPopup(); void testXdgPopupWithPanel(); }; void TransientPlacementTest::initTestCase() { qRegisterMetaType(); qRegisterMetaType(); QSignalSpy workspaceCreatedSpy(kwinApp(), &Application::workspaceCreated); QVERIFY(workspaceCreatedSpy.isValid()); kwinApp()->platform()->setInitialWindowSize(QSize(1280, 1024)); QVERIFY(waylandServer()->init(s_socketName.toLocal8Bit())); QMetaObject::invokeMethod(kwinApp()->platform(), "setVirtualOutputs", Qt::DirectConnection, Q_ARG(int, 2)); 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)); setenv("QT_QPA_PLATFORM", "wayland", true); waylandServer()->initWorkspace(); } void TransientPlacementTest::init() { QVERIFY(Test::setupWaylandConnection(Test::AdditionalWaylandInterface::Decoration | Test::AdditionalWaylandInterface::PlasmaShell)); screens()->setCurrent(0); Cursor::setPos(QPoint(640, 512)); } void TransientPlacementTest::cleanup() { Test::destroyWaylandConnection(); } void TransientPlacementTest::testXdgPopup_data() { using namespace KWayland::Client; QTest::addColumn("parentSize"); QTest::addColumn("parentPosition"); QTest::addColumn("positioner"); QTest::addColumn("expectedGeometry"); // window in the middle, plenty of room either side: Changing anchor // parent window is 500,500, starting at 300,300, anchorRect is therefore between 350->750 in both dirs XdgPositioner positioner(QSize(200,200), QRect(50,50, 400,400)); positioner.setGravity(Qt::BottomEdge | Qt::RightEdge); positioner.setAnchorEdge(Qt::Edges()); QTest::newRow("anchorCentre") << QSize(500, 500) << QPoint(300,300) << positioner << QRect(550, 550, 200, 200); positioner.setAnchorEdge(Qt::TopEdge | Qt::LeftEdge); QTest::newRow("anchorTopLeft") << QSize(500, 500) << QPoint(300,300) << positioner << QRect(350,350, 200, 200); positioner.setAnchorEdge(Qt::TopEdge); QTest::newRow("anchorTop") << QSize(500, 500) << QPoint(300,300) << positioner << QRect(550, 350, 200, 200); positioner.setAnchorEdge(Qt::TopEdge | Qt::RightEdge); QTest::newRow("anchorTopRight") << QSize(500, 500) << QPoint(300,300) << positioner << QRect(750, 350, 200, 200); positioner.setAnchorEdge(Qt::RightEdge); QTest::newRow("anchorRight") << QSize(500, 500) << QPoint(300,300) << positioner << QRect(750, 550, 200, 200); positioner.setAnchorEdge(Qt::BottomEdge | Qt::RightEdge); QTest::newRow("anchorBottomRight") << QSize(500,500) << QPoint(300,300) << positioner << QRect(750, 750, 200, 200); positioner.setAnchorEdge(Qt::BottomEdge); QTest::newRow("anchorBottom") << QSize(500, 500) << QPoint(300,300) << positioner << QRect(550, 750, 200, 200); positioner.setAnchorEdge(Qt::BottomEdge | Qt::LeftEdge); QTest::newRow("anchorBottomLeft") << QSize(500, 500) << QPoint(300,300) << positioner << QRect(350, 750, 200, 200); positioner.setAnchorEdge(Qt::LeftEdge); QTest::newRow("anchorLeft") << QSize(500, 500) << QPoint(300,300) << positioner << QRect(350, 550, 200, 200); // ---------------------------------------------------------------- // window in the middle, plenty of room either side: Changing gravity around the bottom right anchor positioner.setAnchorEdge(Qt::BottomEdge | Qt::RightEdge); positioner.setGravity(Qt::Edges()); QTest::newRow("gravityCentre") << QSize(500, 500) << QPoint(300, 300) << positioner << QRect(650, 650, 200, 200); positioner.setGravity(Qt::TopEdge | Qt::LeftEdge); QTest::newRow("gravityTopLeft") << QSize(500, 500) << QPoint(300, 300) << positioner << QRect(550, 550, 200, 200); positioner.setGravity(Qt::TopEdge); QTest::newRow("gravityTop") << QSize(500, 500) << QPoint(300, 300) << positioner << QRect(650, 550, 200, 200); positioner.setGravity(Qt::TopEdge | Qt::RightEdge); QTest::newRow("gravityTopRight") << QSize(500, 500) << QPoint(300, 300) << positioner << QRect(750, 550, 200, 200); positioner.setGravity(Qt::RightEdge); QTest::newRow("gravityRight") << QSize(500, 500) << QPoint(300, 300) << positioner << QRect(750, 650, 200, 200); positioner.setGravity(Qt::BottomEdge | Qt::RightEdge); QTest::newRow("gravityBottomRight") << QSize(500, 500) << QPoint(300, 300) << positioner << QRect(750, 750, 200, 200); positioner.setGravity(Qt::BottomEdge); QTest::newRow("gravityBottom") << QSize(500, 500) << QPoint(300, 300) << positioner << QRect(650, 750, 200, 200); positioner.setGravity(Qt::BottomEdge | Qt::LeftEdge); QTest::newRow("gravityBottomLeft") << QSize(500, 500) << QPoint(300, 300) << positioner << QRect(550, 750, 200, 200); positioner.setGravity(Qt::LeftEdge); QTest::newRow("gravityLeft") << QSize(500, 500) << QPoint(300, 300) << positioner << QRect(550, 650, 200, 200); // ---------------------------------------------------------------- //constrain and slide //popup is still 200,200. window moved near edge of screen, popup always comes out towards the screen edge positioner.setConstraints(XdgPositioner::Constraint::SlideX | XdgPositioner::Constraint::SlideY); positioner.setAnchorEdge(Qt::TopEdge); positioner.setGravity(Qt::TopEdge); QTest::newRow("constraintSlideTop") << QSize(500, 500) << QPoint(80, 80) << positioner << QRect(80 + 250 - 100, 0, 200, 200); positioner.setAnchorEdge(Qt::LeftEdge); positioner.setGravity(Qt::LeftEdge); QTest::newRow("constraintSlideLeft") << QSize(500, 500) << QPoint(80, 80) << positioner << QRect(0, 80 + 250 - 100, 200, 200); positioner.setAnchorEdge(Qt::RightEdge); positioner.setGravity(Qt::RightEdge); QTest::newRow("constraintSlideRight") << QSize(500, 500) << QPoint(700, 80) << positioner << QRect(1280 - 200, 80 + 250 - 100, 200, 200); positioner.setAnchorEdge(Qt::BottomEdge); positioner.setGravity(Qt::BottomEdge); QTest::newRow("constraintSlideBottom") << QSize(500, 500) << QPoint(80, 500) << positioner << QRect(80 + 250 - 100, 1024 - 200, 200, 200); positioner.setAnchorEdge(Qt::BottomEdge | Qt::RightEdge); positioner.setGravity(Qt::BottomEdge| Qt::RightEdge); QTest::newRow("constraintSlideBottomRight") << QSize(500, 500) << QPoint(700, 1000) << positioner << QRect(1280 - 200, 1024 - 200, 200, 200); // ---------------------------------------------------------------- // constrain and flip positioner.setConstraints(XdgPositioner::Constraint::FlipX | XdgPositioner::Constraint::FlipY); positioner.setAnchorEdge(Qt::TopEdge); positioner.setGravity(Qt::TopEdge); QTest::newRow("constraintFlipTop") << QSize(500, 500) << QPoint(80, 80) << positioner << QRect(230, 80 + 500 - 50, 200, 200); positioner.setAnchorEdge(Qt::LeftEdge); positioner.setGravity(Qt::LeftEdge); QTest::newRow("constraintFlipLeft") << QSize(500, 500) << QPoint(80, 80) << positioner << QRect(80 + 500 - 50, 230, 200, 200); positioner.setAnchorEdge(Qt::RightEdge); positioner.setGravity(Qt::RightEdge); QTest::newRow("constraintFlipRight") << QSize(500, 500) << QPoint(700, 80) << positioner << QRect(700 + 50 - 200, 230, 200, 200); positioner.setAnchorEdge(Qt::BottomEdge); positioner.setGravity(Qt::BottomEdge); QTest::newRow("constraintFlipBottom") << QSize(500, 500) << QPoint(80, 500) << positioner << QRect(230, 500 + 50 - 200, 200, 200); positioner.setAnchorEdge(Qt::BottomEdge | Qt::RightEdge); positioner.setGravity(Qt::BottomEdge| Qt::RightEdge); QTest::newRow("constraintFlipBottomRight") << QSize(500, 500) << QPoint(700, 500) << positioner << QRect(700 + 50 - 200, 500 + 50 - 200, 200, 200); positioner.setAnchorEdge(Qt::TopEdge); positioner.setGravity(Qt::RightEdge); //as popup is positioned in the middle of the parent we need a massive popup to be able to overflow positioner.setInitialSize(QSize(400, 400)); QTest::newRow("constraintFlipRightNoAnchor") << QSize(500, 500) << QPoint(700, 80) << positioner << QRect(700 + 250 - 400, 330, 400, 400); positioner.setAnchorEdge(Qt::RightEdge); positioner.setGravity(Qt::TopEdge); positioner.setInitialSize(QSize(300, 200)); QTest::newRow("constraintFlipRightNoGravity") << QSize(500, 500) << QPoint(700, 80) << positioner << QRect(700 + 50 - 150, 130, 300, 200); // ---------------------------------------------------------------- // resize positioner.setConstraints(XdgPositioner::Constraint::ResizeX | XdgPositioner::Constraint::ResizeY); positioner.setInitialSize(QSize(200, 200)); positioner.setAnchorEdge(Qt::TopEdge); positioner.setGravity(Qt::TopEdge); QTest::newRow("resizeTop") << QSize(500, 500) << QPoint(80, 80) << positioner << QRect(80 + 250 - 100, 0, 200, 130); positioner.setAnchorEdge(Qt::LeftEdge); positioner.setGravity(Qt::LeftEdge); QTest::newRow("resizeLeft") << QSize(500, 500) << QPoint(80, 80) << positioner << QRect(0, 80 + 250 - 100, 130, 200); positioner.setAnchorEdge(Qt::RightEdge); positioner.setGravity(Qt::RightEdge); QTest::newRow("resizeRight") << QSize(500, 500) << QPoint(700, 80) << positioner << QRect(700 + 50 + 400, 80 + 250 - 100, 130, 200); positioner.setAnchorEdge(Qt::BottomEdge); positioner.setGravity(Qt::BottomEdge); QTest::newRow("resizeBottom") << QSize(500, 500) << QPoint(80, 500) << positioner << QRect(80 + 250 - 100, 500 + 50 + 400, 200, 74); } void TransientPlacementTest::testXdgPopup() { using namespace KWayland::Client; // this test verifies that the position of a transient window is taken from the passed position // there are no further constraints like window too large to fit screen, cascading transients, etc // some test cases also verify that the transient fits on the screen QFETCH(QSize, parentSize); QFETCH(QPoint, parentPosition); QFETCH(QRect, expectedGeometry); const QRect expectedRelativeGeometry = expectedGeometry.translated(-parentPosition); Surface *surface = Test::createSurface(Test::waylandCompositor()); QVERIFY(surface); auto parentShellSurface = Test::createXdgShellStableSurface(surface, Test::waylandCompositor()); QVERIFY(parentShellSurface); auto parent = Test::renderAndWaitForShown(surface, parentSize, Qt::blue); QVERIFY(parent); QVERIFY(!parent->isDecorated()); parent->move(parentPosition); QCOMPARE(parent->frameGeometry(), QRect(parentPosition, parentSize)); //create popup QFETCH(XdgPositioner, positioner); Surface *transientSurface = Test::createSurface(Test::waylandCompositor()); QVERIFY(transientSurface); auto popup = Test::createXdgShellStablePopup(transientSurface, parentShellSurface, positioner, Test::waylandCompositor(), Test::CreationSetup::CreateOnly); QSignalSpy configureRequestedSpy(popup, &XdgShellPopup::configureRequested); transientSurface->commit(Surface::CommitFlag::None); configureRequestedSpy.wait(); QCOMPARE(configureRequestedSpy.count(), 1); QCOMPARE(configureRequestedSpy.first()[0].value(), expectedRelativeGeometry); popup->ackConfigure(configureRequestedSpy.first()[1].toUInt()); auto transient = Test::renderAndWaitForShown(transientSurface, expectedRelativeGeometry.size(), Qt::red); QVERIFY(transient); QVERIFY(!transient->isDecorated()); QVERIFY(transient->hasTransientPlacementHint()); QCOMPARE(transient->frameGeometry(), expectedGeometry); QCOMPARE(configureRequestedSpy.count(), 1); // check that we did not get reconfigured } void TransientPlacementTest::testXdgPopupWithPanel() { using namespace KWayland::Client; QScopedPointer surface{Test::createSurface()}; QVERIFY(!surface.isNull()); QScopedPointer dockShellSurface{Test::createXdgShellStableSurface(surface.data(), surface.data())}; QVERIFY(!dockShellSurface.isNull()); QScopedPointer plasmaSurface(Test::waylandPlasmaShell()->createSurface(surface.data())); QVERIFY(!plasmaSurface.isNull()); plasmaSurface->setRole(PlasmaShellSurface::Role::Panel); plasmaSurface->setPosition(QPoint(0, screens()->geometry(0).height() - 50)); plasmaSurface->setPanelBehavior(PlasmaShellSurface::PanelBehavior::AlwaysVisible); // now render and map the window QVERIFY(workspace()->clientArea(PlacementArea, 0, 1) == workspace()->clientArea(FullScreenArea, 0, 1)); auto dock = Test::renderAndWaitForShown(surface.data(), QSize(1280, 50), Qt::blue); QVERIFY(dock); QCOMPARE(dock->windowType(), NET::Dock); QVERIFY(dock->isDock()); QCOMPARE(dock->frameGeometry(), QRect(0, screens()->geometry(0).height() - 50, 1280, 50)); QCOMPARE(dock->hasStrut(), true); QVERIFY(workspace()->clientArea(PlacementArea, 0, 1) != workspace()->clientArea(FullScreenArea, 0, 1)); //create parent Surface *parentSurface = Test::createSurface(Test::waylandCompositor()); QVERIFY(parentSurface); auto parentShellSurface = Test::createXdgShellStableSurface(parentSurface, Test::waylandCompositor()); QVERIFY(parentShellSurface); auto parent = Test::renderAndWaitForShown(parentSurface, {800, 600}, Qt::blue); QVERIFY(parent); QVERIFY(!parent->isDecorated()); parent->move({0, screens()->geometry(0).height() - 600}); parent->keepInArea(workspace()->clientArea(PlacementArea, parent)); QCOMPARE(parent->frameGeometry(), QRect(0, screens()->geometry(0).height() - 600 - 50, 800, 600)); Surface *transientSurface = Test::createSurface(Test::waylandCompositor()); QVERIFY(transientSurface); XdgPositioner positioner(QSize(200,200), QRect(50,500, 200,200)); auto transientShellSurface = Test::createXdgShellStablePopup(transientSurface, parentShellSurface, positioner, Test::waylandCompositor()); auto transient = Test::renderAndWaitForShown(transientSurface, positioner.initialSize(), Qt::red); QVERIFY(transient); QVERIFY(!transient->isDecorated()); QVERIFY(transient->hasTransientPlacementHint()); QCOMPARE(transient->frameGeometry(), QRect(50, screens()->geometry(0).height() - 200 - 50, 200, 200)); transientShellSurface->deleteLater(); transientSurface->deleteLater(); QVERIFY(Test::waitForWindowDestroyed(transient)); // now parent to fullscreen - on fullscreen the panel is ignored QSignalSpy fullscreenSpy{parentShellSurface, &XdgShellSurface::configureRequested}; QVERIFY(fullscreenSpy.isValid()); parent->setFullScreen(true); QVERIFY(fullscreenSpy.wait()); parentShellSurface->ackConfigure(fullscreenSpy.first().at(2).value()); - QSignalSpy geometryShapeChangedSpy{parent, &XdgShellClient::geometryShapeChanged}; - QVERIFY(geometryShapeChangedSpy.isValid()); + QSignalSpy frameGeometryChangedSpy{parent, &XdgShellClient::frameGeometryChanged}; + QVERIFY(frameGeometryChangedSpy.isValid()); Test::render(parentSurface, fullscreenSpy.first().at(0).toSize(), Qt::red); - QVERIFY(geometryShapeChangedSpy.wait()); + QVERIFY(frameGeometryChangedSpy.wait()); QCOMPARE(parent->frameGeometry(), screens()->geometry(0)); QVERIFY(parent->isFullScreen()); // another transient, with same hints as before from bottom of window transientSurface = Test::createSurface(Test::waylandCompositor()); QVERIFY(transientSurface); XdgPositioner positioner2(QSize(200,200), QRect(50,screens()->geometry(0).height()-100, 200,200)); transientShellSurface = Test::createXdgShellStablePopup(transientSurface, parentShellSurface, positioner2, Test::waylandCompositor()); transient = Test::renderAndWaitForShown(transientSurface, positioner2.initialSize(), Qt::red); QVERIFY(transient); QVERIFY(!transient->isDecorated()); QVERIFY(transient->hasTransientPlacementHint()); QCOMPARE(transient->frameGeometry(), QRect(50, screens()->geometry(0).height() - 200, 200, 200)); } } WAYLANDTEST_MAIN(KWin::TransientPlacementTest) #include "transient_placement.moc" diff --git a/autotests/integration/xdgshellclient_rules_test.cpp b/autotests/integration/xdgshellclient_rules_test.cpp index 52bd1c4ad..46dfc81b1 100644 --- a/autotests/integration/xdgshellclient_rules_test.cpp +++ b/autotests/integration/xdgshellclient_rules_test.cpp @@ -1,4544 +1,4544 @@ /******************************************************************** KWin - the KDE window manager This file is part of the KDE project. Copyright (C) 2017 Martin Flöser Copyright (C) 2019 Vlad Zahorodnii This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . *********************************************************************/ #include "kwin_wayland_test.h" #include "cursor.h" #include "platform.h" #include "rules.h" #include "screens.h" #include "xdgshellclient.h" #include "virtualdesktops.h" #include "wayland_server.h" #include "workspace.h" #include #include #include using namespace KWin; using namespace KWayland::Client; static const QString s_socketName = QStringLiteral("wayland_test_kwin_xdgshellclient_rules-0"); class TestXdgShellClientRules : public QObject { Q_OBJECT private Q_SLOTS: void initTestCase(); void init(); void cleanup(); void testPositionDontAffect_data(); void testPositionDontAffect(); void testPositionApply_data(); void testPositionApply(); void testPositionRemember_data(); void testPositionRemember(); void testPositionForce_data(); void testPositionForce(); void testPositionApplyNow_data(); void testPositionApplyNow(); void testPositionForceTemporarily_data(); void testPositionForceTemporarily(); void testSizeDontAffect_data(); void testSizeDontAffect(); void testSizeApply_data(); void testSizeApply(); void testSizeRemember_data(); void testSizeRemember(); void testSizeForce_data(); void testSizeForce(); void testSizeApplyNow_data(); void testSizeApplyNow(); void testSizeForceTemporarily_data(); void testSizeForceTemporarily(); void testMaximizeDontAffect_data(); void testMaximizeDontAffect(); void testMaximizeApply_data(); void testMaximizeApply(); void testMaximizeRemember_data(); void testMaximizeRemember(); void testMaximizeForce_data(); void testMaximizeForce(); void testMaximizeApplyNow_data(); void testMaximizeApplyNow(); void testMaximizeForceTemporarily_data(); void testMaximizeForceTemporarily(); void testDesktopDontAffect_data(); void testDesktopDontAffect(); void testDesktopApply_data(); void testDesktopApply(); void testDesktopRemember_data(); void testDesktopRemember(); void testDesktopForce_data(); void testDesktopForce(); void testDesktopApplyNow_data(); void testDesktopApplyNow(); void testDesktopForceTemporarily_data(); void testDesktopForceTemporarily(); void testMinimizeDontAffect_data(); void testMinimizeDontAffect(); void testMinimizeApply_data(); void testMinimizeApply(); void testMinimizeRemember_data(); void testMinimizeRemember(); void testMinimizeForce_data(); void testMinimizeForce(); void testMinimizeApplyNow_data(); void testMinimizeApplyNow(); void testMinimizeForceTemporarily_data(); void testMinimizeForceTemporarily(); void testSkipTaskbarDontAffect_data(); void testSkipTaskbarDontAffect(); void testSkipTaskbarApply_data(); void testSkipTaskbarApply(); void testSkipTaskbarRemember_data(); void testSkipTaskbarRemember(); void testSkipTaskbarForce_data(); void testSkipTaskbarForce(); void testSkipTaskbarApplyNow_data(); void testSkipTaskbarApplyNow(); void testSkipTaskbarForceTemporarily_data(); void testSkipTaskbarForceTemporarily(); void testSkipPagerDontAffect_data(); void testSkipPagerDontAffect(); void testSkipPagerApply_data(); void testSkipPagerApply(); void testSkipPagerRemember_data(); void testSkipPagerRemember(); void testSkipPagerForce_data(); void testSkipPagerForce(); void testSkipPagerApplyNow_data(); void testSkipPagerApplyNow(); void testSkipPagerForceTemporarily_data(); void testSkipPagerForceTemporarily(); void testSkipSwitcherDontAffect_data(); void testSkipSwitcherDontAffect(); void testSkipSwitcherApply_data(); void testSkipSwitcherApply(); void testSkipSwitcherRemember_data(); void testSkipSwitcherRemember(); void testSkipSwitcherForce_data(); void testSkipSwitcherForce(); void testSkipSwitcherApplyNow_data(); void testSkipSwitcherApplyNow(); void testSkipSwitcherForceTemporarily_data(); void testSkipSwitcherForceTemporarily(); void testKeepAboveDontAffect_data(); void testKeepAboveDontAffect(); void testKeepAboveApply_data(); void testKeepAboveApply(); void testKeepAboveRemember_data(); void testKeepAboveRemember(); void testKeepAboveForce_data(); void testKeepAboveForce(); void testKeepAboveApplyNow_data(); void testKeepAboveApplyNow(); void testKeepAboveForceTemporarily_data(); void testKeepAboveForceTemporarily(); void testKeepBelowDontAffect_data(); void testKeepBelowDontAffect(); void testKeepBelowApply_data(); void testKeepBelowApply(); void testKeepBelowRemember_data(); void testKeepBelowRemember(); void testKeepBelowForce_data(); void testKeepBelowForce(); void testKeepBelowApplyNow_data(); void testKeepBelowApplyNow(); void testKeepBelowForceTemporarily_data(); void testKeepBelowForceTemporarily(); void testShortcutDontAffect_data(); void testShortcutDontAffect(); void testShortcutApply_data(); void testShortcutApply(); void testShortcutRemember_data(); void testShortcutRemember(); void testShortcutForce_data(); void testShortcutForce(); void testShortcutApplyNow_data(); void testShortcutApplyNow(); void testShortcutForceTemporarily_data(); void testShortcutForceTemporarily(); void testDesktopFileDontAffect_data(); void testDesktopFileDontAffect(); void testDesktopFileApply_data(); void testDesktopFileApply(); void testDesktopFileRemember_data(); void testDesktopFileRemember(); void testDesktopFileForce_data(); void testDesktopFileForce(); void testDesktopFileApplyNow_data(); void testDesktopFileApplyNow(); void testDesktopFileForceTemporarily_data(); void testDesktopFileForceTemporarily(); void testActiveOpacityDontAffect_data(); void testActiveOpacityDontAffect(); void testActiveOpacityForce_data(); void testActiveOpacityForce(); void testActiveOpacityForceTemporarily_data(); void testActiveOpacityForceTemporarily(); void testInactiveOpacityDontAffect_data(); void testInactiveOpacityDontAffect(); void testInactiveOpacityForce_data(); void testInactiveOpacityForce(); void testInactiveOpacityForceTemporarily_data(); void testInactiveOpacityForceTemporarily(); void testMatchAfterNameChange(); }; void TestXdgShellClientRules::initTestCase() { qRegisterMetaType(); qRegisterMetaType(); QSignalSpy workspaceCreatedSpy(kwinApp(), &Application::workspaceCreated); QVERIFY(workspaceCreatedSpy.isValid()); kwinApp()->platform()->setInitialWindowSize(QSize(1280, 1024)); QVERIFY(waylandServer()->init(s_socketName.toLocal8Bit())); QMetaObject::invokeMethod(kwinApp()->platform(), "setVirtualOutputs", Qt::DirectConnection, Q_ARG(int, 2)); 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 TestXdgShellClientRules::init() { VirtualDesktopManager::self()->setCurrent(VirtualDesktopManager::self()->desktops().first()); QVERIFY(Test::setupWaylandConnection(Test::AdditionalWaylandInterface::Decoration)); screens()->setCurrent(0); } void TestXdgShellClientRules::cleanup() { Test::destroyWaylandConnection(); // Unreference the previous config. RuleBook::self()->setConfig({}); workspace()->slotReconfigure(); // Restore virtual desktops to the initial state. VirtualDesktopManager::self()->setCount(1); QCOMPARE(VirtualDesktopManager::self()->count(), 1u); } #define TEST_DATA(name) \ void TestXdgShellClientRules::name##_data() \ { \ QTest::addColumn("type"); \ QTest::newRow("XdgShellV6") << Test::XdgShellSurfaceType::XdgShellV6; \ QTest::newRow("XdgWmBase") << Test::XdgShellSurfaceType::XdgShellStable; \ } std::tuple createWindow(Test::XdgShellSurfaceType type, const QByteArray &appId) { // Create an xdg surface. Surface *surface = Test::createSurface(); XdgShellSurface *shellSurface = Test::createXdgShellSurface(type, surface, surface, Test::CreationSetup::CreateOnly); // Assign the desired app id. shellSurface->setAppId(appId); // Wait for the initial configure event. QSignalSpy configureRequestedSpy(shellSurface, &XdgShellSurface::configureRequested); surface->commit(Surface::CommitFlag::None); configureRequestedSpy.wait(); // Draw content of the surface. shellSurface->ackConfigure(configureRequestedSpy.last().at(2).value()); XdgShellClient *client = Test::renderAndWaitForShown(surface, QSize(100, 50), Qt::blue); return {client, surface, shellSurface}; } TEST_DATA(testPositionDontAffect) void TestXdgShellClientRules::testPositionDontAffect() { // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("position", QPoint(42, 42)); group.writeEntry("positionrule", int(Rules::DontAffect)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // Create the test client. QFETCH(Test::XdgShellSurfaceType, type); XdgShellClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); QVERIFY(client->isActive()); // The position of the client should not be affected by the rule. The default // placement policy will put the client in the top-left corner of the screen. QVERIFY(client->isMovable()); QVERIFY(client->isMovableAcrossScreens()); QCOMPARE(client->pos(), QPoint(0, 0)); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testPositionApply) void TestXdgShellClientRules::testPositionApply() { // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("position", QPoint(42, 42)); group.writeEntry("positionrule", int(Rules::Apply)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // Create the test client. QFETCH(Test::XdgShellSurfaceType, type); XdgShellClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); QVERIFY(client->isActive()); // The client should be moved to the position specified by the rule. QVERIFY(client->isMovable()); QVERIFY(client->isMovableAcrossScreens()); QCOMPARE(client->pos(), QPoint(42, 42)); // One should still be able to move the client around. QSignalSpy clientStartMoveResizedSpy(client, &AbstractClient::clientStartUserMovedResized); QVERIFY(clientStartMoveResizedSpy.isValid()); QSignalSpy clientStepUserMovedResizedSpy(client, &AbstractClient::clientStepUserMovedResized); QVERIFY(clientStepUserMovedResizedSpy.isValid()); QSignalSpy clientFinishUserMovedResizedSpy(client, &AbstractClient::clientFinishUserMovedResized); QVERIFY(clientFinishUserMovedResizedSpy.isValid()); QCOMPARE(workspace()->moveResizeClient(), nullptr); QVERIFY(!client->isMove()); QVERIFY(!client->isResize()); workspace()->slotWindowMove(); QCOMPARE(workspace()->moveResizeClient(), client); QCOMPARE(clientStartMoveResizedSpy.count(), 1); QVERIFY(client->isMove()); QVERIFY(!client->isResize()); const QPoint cursorPos = KWin::Cursor::pos(); client->keyPressEvent(Qt::Key_Right); client->updateMoveResize(KWin::Cursor::pos()); QCOMPARE(KWin::Cursor::pos(), cursorPos + QPoint(8, 0)); QCOMPARE(clientStepUserMovedResizedSpy.count(), 1); QCOMPARE(client->pos(), QPoint(50, 42)); client->keyPressEvent(Qt::Key_Enter); QCOMPARE(clientFinishUserMovedResizedSpy.count(), 1); QCOMPARE(workspace()->moveResizeClient(), nullptr); QVERIFY(!client->isMove()); QVERIFY(!client->isResize()); QCOMPARE(client->pos(), QPoint(50, 42)); // The rule should be applied again if the client appears after it's been closed. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); QVERIFY(client->isActive()); QVERIFY(client->isMovable()); QVERIFY(client->isMovableAcrossScreens()); QCOMPARE(client->pos(), QPoint(42, 42)); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testPositionRemember) void TestXdgShellClientRules::testPositionRemember() { // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("position", QPoint(42, 42)); group.writeEntry("positionrule", int(Rules::Remember)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // Create the test client. QFETCH(Test::XdgShellSurfaceType, type); XdgShellClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); QVERIFY(client->isActive()); // The client should be moved to the position specified by the rule. QVERIFY(client->isMovable()); QVERIFY(client->isMovableAcrossScreens()); QCOMPARE(client->pos(), QPoint(42, 42)); // One should still be able to move the client around. QSignalSpy clientStartMoveResizedSpy(client, &AbstractClient::clientStartUserMovedResized); QVERIFY(clientStartMoveResizedSpy.isValid()); QSignalSpy clientStepUserMovedResizedSpy(client, &AbstractClient::clientStepUserMovedResized); QVERIFY(clientStepUserMovedResizedSpy.isValid()); QSignalSpy clientFinishUserMovedResizedSpy(client, &AbstractClient::clientFinishUserMovedResized); QVERIFY(clientFinishUserMovedResizedSpy.isValid()); QCOMPARE(workspace()->moveResizeClient(), nullptr); QVERIFY(!client->isMove()); QVERIFY(!client->isResize()); workspace()->slotWindowMove(); QCOMPARE(workspace()->moveResizeClient(), client); QCOMPARE(clientStartMoveResizedSpy.count(), 1); QVERIFY(client->isMove()); QVERIFY(!client->isResize()); const QPoint cursorPos = KWin::Cursor::pos(); client->keyPressEvent(Qt::Key_Right); client->updateMoveResize(KWin::Cursor::pos()); QCOMPARE(KWin::Cursor::pos(), cursorPos + QPoint(8, 0)); QCOMPARE(clientStepUserMovedResizedSpy.count(), 1); QCOMPARE(client->pos(), QPoint(50, 42)); client->keyPressEvent(Qt::Key_Enter); QCOMPARE(clientFinishUserMovedResizedSpy.count(), 1); QCOMPARE(workspace()->moveResizeClient(), nullptr); QVERIFY(!client->isMove()); QVERIFY(!client->isResize()); QCOMPARE(client->pos(), QPoint(50, 42)); // The client should be placed at the last know position if we reopen it. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); QVERIFY(client->isActive()); QVERIFY(client->isMovable()); QVERIFY(client->isMovableAcrossScreens()); QCOMPARE(client->pos(), QPoint(50, 42)); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testPositionForce) void TestXdgShellClientRules::testPositionForce() { // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("position", QPoint(42, 42)); group.writeEntry("positionrule", int(Rules::Force)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // Create the test client. QFETCH(Test::XdgShellSurfaceType, type); XdgShellClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); QVERIFY(client->isActive()); // The client should be moved to the position specified by the rule. QVERIFY(!client->isMovable()); QVERIFY(!client->isMovableAcrossScreens()); QCOMPARE(client->pos(), QPoint(42, 42)); // User should not be able to move the client. QSignalSpy clientStartMoveResizedSpy(client, &AbstractClient::clientStartUserMovedResized); QVERIFY(clientStartMoveResizedSpy.isValid()); QCOMPARE(workspace()->moveResizeClient(), nullptr); QVERIFY(!client->isMove()); QVERIFY(!client->isResize()); workspace()->slotWindowMove(); QCOMPARE(workspace()->moveResizeClient(), nullptr); QCOMPARE(clientStartMoveResizedSpy.count(), 0); QVERIFY(!client->isMove()); QVERIFY(!client->isResize()); // The position should still be forced if we reopen the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); QVERIFY(client->isActive()); QVERIFY(!client->isMovable()); QVERIFY(!client->isMovableAcrossScreens()); QCOMPARE(client->pos(), QPoint(42, 42)); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testPositionApplyNow) void TestXdgShellClientRules::testPositionApplyNow() { // Create the test client. QFETCH(Test::XdgShellSurfaceType, type); XdgShellClient *client; Surface *surface; QObject *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); QVERIFY(client->isActive()); // The position of the client isn't set by any rule, thus the default placement // policy will try to put the client in the top-left corner of the screen. QVERIFY(client->isMovable()); QVERIFY(client->isMovableAcrossScreens()); QCOMPARE(client->pos(), QPoint(0, 0)); // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("position", QPoint(42, 42)); group.writeEntry("positionrule", int(Rules::ApplyNow)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); // The client should be moved to the position specified by the rule. - QSignalSpy geometryChangedSpy(client, &AbstractClient::geometryChanged); - QVERIFY(geometryChangedSpy.isValid()); + QSignalSpy frameGeometryChangedSpy(client, &AbstractClient::frameGeometryChanged); + QVERIFY(frameGeometryChangedSpy.isValid()); workspace()->slotReconfigure(); - QCOMPARE(geometryChangedSpy.count(), 1); + QCOMPARE(frameGeometryChangedSpy.count(), 1); QCOMPARE(client->pos(), QPoint(42, 42)); // We still have to be able to move the client around. QVERIFY(client->isMovable()); QVERIFY(client->isMovableAcrossScreens()); QSignalSpy clientStartMoveResizedSpy(client, &AbstractClient::clientStartUserMovedResized); QVERIFY(clientStartMoveResizedSpy.isValid()); QSignalSpy clientStepUserMovedResizedSpy(client, &AbstractClient::clientStepUserMovedResized); QVERIFY(clientStepUserMovedResizedSpy.isValid()); QSignalSpy clientFinishUserMovedResizedSpy(client, &AbstractClient::clientFinishUserMovedResized); QVERIFY(clientFinishUserMovedResizedSpy.isValid()); QCOMPARE(workspace()->moveResizeClient(), nullptr); QVERIFY(!client->isMove()); QVERIFY(!client->isResize()); workspace()->slotWindowMove(); QCOMPARE(workspace()->moveResizeClient(), client); QCOMPARE(clientStartMoveResizedSpy.count(), 1); QVERIFY(client->isMove()); QVERIFY(!client->isResize()); const QPoint cursorPos = KWin::Cursor::pos(); client->keyPressEvent(Qt::Key_Right); client->updateMoveResize(KWin::Cursor::pos()); QCOMPARE(KWin::Cursor::pos(), cursorPos + QPoint(8, 0)); QCOMPARE(clientStepUserMovedResizedSpy.count(), 1); QCOMPARE(client->pos(), QPoint(50, 42)); client->keyPressEvent(Qt::Key_Enter); QCOMPARE(clientFinishUserMovedResizedSpy.count(), 1); QCOMPARE(workspace()->moveResizeClient(), nullptr); QVERIFY(!client->isMove()); QVERIFY(!client->isResize()); QCOMPARE(client->pos(), QPoint(50, 42)); // The rule should not be applied again. client->evaluateWindowRules(); QCOMPARE(client->pos(), QPoint(50, 42)); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testPositionForceTemporarily) void TestXdgShellClientRules::testPositionForceTemporarily() { // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("position", QPoint(42, 42)); group.writeEntry("positionrule", int(Rules::ForceTemporarily)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // Create the test client. QFETCH(Test::XdgShellSurfaceType, type); XdgShellClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); QVERIFY(client->isActive()); // The client should be moved to the position specified by the rule. QVERIFY(!client->isMovable()); QVERIFY(!client->isMovableAcrossScreens()); QCOMPARE(client->pos(), QPoint(42, 42)); // User should not be able to move the client. QSignalSpy clientStartMoveResizedSpy(client, &AbstractClient::clientStartUserMovedResized); QVERIFY(clientStartMoveResizedSpy.isValid()); QCOMPARE(workspace()->moveResizeClient(), nullptr); QVERIFY(!client->isMove()); QVERIFY(!client->isResize()); workspace()->slotWindowMove(); QCOMPARE(workspace()->moveResizeClient(), nullptr); QCOMPARE(clientStartMoveResizedSpy.count(), 0); QVERIFY(!client->isMove()); QVERIFY(!client->isResize()); // The rule should be discarded if we close the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); QVERIFY(client->isActive()); QVERIFY(client->isMovable()); QVERIFY(client->isMovableAcrossScreens()); QCOMPARE(client->pos(), QPoint(0, 0)); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testSizeDontAffect) void TestXdgShellClientRules::testSizeDontAffect() { // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("size", QSize(480, 640)); group.writeEntry("sizerule", int(Rules::DontAffect)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // Create the test client. QFETCH(Test::XdgShellSurfaceType, type); QScopedPointer surface; surface.reset(Test::createSurface()); QScopedPointer shellSurface; shellSurface.reset(createXdgShellSurface(type, surface.data(), surface.data(), Test::CreationSetup::CreateOnly)); QScopedPointer configureRequestedSpy; configureRequestedSpy.reset(new QSignalSpy(shellSurface.data(), &XdgShellSurface::configureRequested)); shellSurface->setAppId("org.kde.foo"); surface->commit(Surface::CommitFlag::None); // The window size shouldn't be enforced by the rule. QVERIFY(configureRequestedSpy->wait()); QCOMPARE(configureRequestedSpy->count(), 1); QCOMPARE(configureRequestedSpy->last().first().toSize(), QSize(0, 0)); // Map the client. shellSurface->ackConfigure(configureRequestedSpy->last().at(2).value()); XdgShellClient *client = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); QVERIFY(client); QVERIFY(client->isActive()); QVERIFY(client->isResizable()); QCOMPARE(client->size(), QSize(100, 50)); // We should receive a configure event when the client becomes active. QVERIFY(configureRequestedSpy->wait()); QCOMPARE(configureRequestedSpy->count(), 2); // Destroy the client. shellSurface.reset(); surface.reset(); QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testSizeApply) void TestXdgShellClientRules::testSizeApply() { // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("size", QSize(480, 640)); group.writeEntry("sizerule", int(Rules::Apply)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // Create the test client. QFETCH(Test::XdgShellSurfaceType, type); QScopedPointer surface; surface.reset(Test::createSurface()); QScopedPointer shellSurface; shellSurface.reset(createXdgShellSurface(type, surface.data(), surface.data(), Test::CreationSetup::CreateOnly)); QScopedPointer configureRequestedSpy; configureRequestedSpy.reset(new QSignalSpy(shellSurface.data(), &XdgShellSurface::configureRequested)); shellSurface->setAppId("org.kde.foo"); surface->commit(Surface::CommitFlag::None); // The initial configure event should contain size hint set by the rule. XdgShellSurface::States states; QVERIFY(configureRequestedSpy->wait()); QCOMPARE(configureRequestedSpy->count(), 1); QCOMPARE(configureRequestedSpy->last().at(0).toSize(), QSize(480, 640)); states = configureRequestedSpy->last().at(1).value(); QVERIFY(!states.testFlag(XdgShellSurface::State::Activated)); QVERIFY(!states.testFlag(XdgShellSurface::State::Resizing)); // Map the client. shellSurface->ackConfigure(configureRequestedSpy->last().at(2).value()); XdgShellClient *client = Test::renderAndWaitForShown(surface.data(), QSize(480, 640), Qt::blue); QVERIFY(client); QVERIFY(client->isActive()); QVERIFY(client->isResizable()); QCOMPARE(client->size(), QSize(480, 640)); // We should receive a configure event when the client becomes active. QVERIFY(configureRequestedSpy->wait()); QCOMPARE(configureRequestedSpy->count(), 2); states = configureRequestedSpy->last().at(1).value(); QVERIFY(states.testFlag(XdgShellSurface::State::Activated)); QVERIFY(!states.testFlag(XdgShellSurface::State::Resizing)); // One still should be able to resize the client. - QSignalSpy geometryChangedSpy(client, &AbstractClient::geometryChanged); - QVERIFY(geometryChangedSpy.isValid()); + QSignalSpy frameGeometryChangedSpy(client, &AbstractClient::frameGeometryChanged); + QVERIFY(frameGeometryChangedSpy.isValid()); QSignalSpy clientStartMoveResizedSpy(client, &AbstractClient::clientStartUserMovedResized); QVERIFY(clientStartMoveResizedSpy.isValid()); QSignalSpy clientStepUserMovedResizedSpy(client, &AbstractClient::clientStepUserMovedResized); QVERIFY(clientStepUserMovedResizedSpy.isValid()); QSignalSpy clientFinishUserMovedResizedSpy(client, &AbstractClient::clientFinishUserMovedResized); QVERIFY(clientFinishUserMovedResizedSpy.isValid()); QSignalSpy surfaceSizeChangedSpy(shellSurface.data(), &XdgShellSurface::sizeChanged); QVERIFY(surfaceSizeChangedSpy.isValid()); QCOMPARE(workspace()->moveResizeClient(), nullptr); QVERIFY(!client->isMove()); QVERIFY(!client->isResize()); workspace()->slotWindowResize(); QCOMPARE(workspace()->moveResizeClient(), client); QCOMPARE(clientStartMoveResizedSpy.count(), 1); QVERIFY(!client->isMove()); QVERIFY(client->isResize()); QVERIFY(configureRequestedSpy->wait()); QCOMPARE(configureRequestedSpy->count(), 3); states = configureRequestedSpy->last().at(1).value(); QVERIFY(states.testFlag(XdgShellSurface::State::Activated)); QVERIFY(states.testFlag(XdgShellSurface::State::Resizing)); shellSurface->ackConfigure(configureRequestedSpy->last().at(2).value()); const QPoint cursorPos = KWin::Cursor::pos(); client->keyPressEvent(Qt::Key_Right); client->updateMoveResize(KWin::Cursor::pos()); QCOMPARE(KWin::Cursor::pos(), cursorPos + QPoint(8, 0)); QVERIFY(configureRequestedSpy->wait()); QCOMPARE(configureRequestedSpy->count(), 4); states = configureRequestedSpy->last().at(1).value(); QVERIFY(states.testFlag(XdgShellSurface::State::Activated)); QVERIFY(states.testFlag(XdgShellSurface::State::Resizing)); QCOMPARE(surfaceSizeChangedSpy.count(), 1); QCOMPARE(surfaceSizeChangedSpy.last().first().toSize(), QSize(488, 640)); QCOMPARE(clientStepUserMovedResizedSpy.count(), 0); shellSurface->ackConfigure(configureRequestedSpy->last().at(2).value()); Test::render(surface.data(), QSize(488, 640), Qt::blue); - QVERIFY(geometryChangedSpy.wait()); + QVERIFY(frameGeometryChangedSpy.wait()); QCOMPARE(client->size(), QSize(488, 640)); QCOMPARE(clientStepUserMovedResizedSpy.count(), 1); client->keyPressEvent(Qt::Key_Enter); QCOMPARE(clientFinishUserMovedResizedSpy.count(), 1); QCOMPARE(workspace()->moveResizeClient(), nullptr); QVERIFY(!client->isMove()); QVERIFY(!client->isResize()); QEXPECT_FAIL("", "Interactive resize is not spec-compliant", Continue); QVERIFY(configureRequestedSpy->wait(10)); QEXPECT_FAIL("", "Interactive resize is not spec-compliant", Continue); QCOMPARE(configureRequestedSpy->count(), 5); // The rule should be applied again if the client appears after it's been closed. shellSurface.reset(); surface.reset(); QVERIFY(Test::waitForWindowDestroyed(client)); surface.reset(Test::createSurface()); shellSurface.reset(createXdgShellSurface(type, surface.data(), surface.data(), Test::CreationSetup::CreateOnly)); configureRequestedSpy.reset(new QSignalSpy(shellSurface.data(), &XdgShellSurface::configureRequested)); shellSurface->setAppId("org.kde.foo"); surface->commit(Surface::CommitFlag::None); QVERIFY(configureRequestedSpy->wait()); QCOMPARE(configureRequestedSpy->count(), 1); QCOMPARE(configureRequestedSpy->last().first().toSize(), QSize(480, 640)); shellSurface->ackConfigure(configureRequestedSpy->last().at(2).value()); client = Test::renderAndWaitForShown(surface.data(), QSize(480, 640), Qt::blue); QVERIFY(client); QVERIFY(client->isActive()); QVERIFY(client->isResizable()); QCOMPARE(client->size(), QSize(480, 640)); QVERIFY(configureRequestedSpy->wait()); QCOMPARE(configureRequestedSpy->count(), 2); // Destroy the client. shellSurface.reset(); surface.reset(); QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testSizeRemember) void TestXdgShellClientRules::testSizeRemember() { // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("size", QSize(480, 640)); group.writeEntry("sizerule", int(Rules::Remember)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // Create the test client. QFETCH(Test::XdgShellSurfaceType, type); QScopedPointer surface; surface.reset(Test::createSurface()); QScopedPointer shellSurface; shellSurface.reset(createXdgShellSurface(type, surface.data(), surface.data(), Test::CreationSetup::CreateOnly)); QScopedPointer configureRequestedSpy; configureRequestedSpy.reset(new QSignalSpy(shellSurface.data(), &XdgShellSurface::configureRequested)); shellSurface->setAppId("org.kde.foo"); surface->commit(Surface::CommitFlag::None); // The initial configure event should contain size hint set by the rule. XdgShellSurface::States states; QVERIFY(configureRequestedSpy->wait()); QCOMPARE(configureRequestedSpy->count(), 1); QCOMPARE(configureRequestedSpy->last().first().toSize(), QSize(480, 640)); states = configureRequestedSpy->last().at(1).value(); QVERIFY(!states.testFlag(XdgShellSurface::State::Activated)); QVERIFY(!states.testFlag(XdgShellSurface::State::Resizing)); // Map the client. shellSurface->ackConfigure(configureRequestedSpy->last().at(2).value()); XdgShellClient *client = Test::renderAndWaitForShown(surface.data(), QSize(480, 640), Qt::blue); QVERIFY(client); QVERIFY(client->isActive()); QVERIFY(client->isResizable()); QCOMPARE(client->size(), QSize(480, 640)); // We should receive a configure event when the client becomes active. QVERIFY(configureRequestedSpy->wait()); QCOMPARE(configureRequestedSpy->count(), 2); states = configureRequestedSpy->last().at(1).value(); QVERIFY(states.testFlag(XdgShellSurface::State::Activated)); QVERIFY(!states.testFlag(XdgShellSurface::State::Resizing)); // One should still be able to resize the client. - QSignalSpy geometryChangedSpy(client, &AbstractClient::geometryChanged); - QVERIFY(geometryChangedSpy.isValid()); + QSignalSpy frameGeometryChangedSpy(client, &AbstractClient::frameGeometryChanged); + QVERIFY(frameGeometryChangedSpy.isValid()); QSignalSpy clientStartMoveResizedSpy(client, &AbstractClient::clientStartUserMovedResized); QVERIFY(clientStartMoveResizedSpy.isValid()); QSignalSpy clientStepUserMovedResizedSpy(client, &AbstractClient::clientStepUserMovedResized); QVERIFY(clientStepUserMovedResizedSpy.isValid()); QSignalSpy clientFinishUserMovedResizedSpy(client, &AbstractClient::clientFinishUserMovedResized); QVERIFY(clientFinishUserMovedResizedSpy.isValid()); QSignalSpy surfaceSizeChangedSpy(shellSurface.data(), &XdgShellSurface::sizeChanged); QVERIFY(surfaceSizeChangedSpy.isValid()); QCOMPARE(workspace()->moveResizeClient(), nullptr); QVERIFY(!client->isMove()); QVERIFY(!client->isResize()); workspace()->slotWindowResize(); QCOMPARE(workspace()->moveResizeClient(), client); QCOMPARE(clientStartMoveResizedSpy.count(), 1); QVERIFY(!client->isMove()); QVERIFY(client->isResize()); QVERIFY(configureRequestedSpy->wait()); QCOMPARE(configureRequestedSpy->count(), 3); states = configureRequestedSpy->last().at(1).value(); QVERIFY(states.testFlag(XdgShellSurface::State::Activated)); QVERIFY(states.testFlag(XdgShellSurface::State::Resizing)); shellSurface->ackConfigure(configureRequestedSpy->last().at(2).value()); const QPoint cursorPos = KWin::Cursor::pos(); client->keyPressEvent(Qt::Key_Right); client->updateMoveResize(KWin::Cursor::pos()); QCOMPARE(KWin::Cursor::pos(), cursorPos + QPoint(8, 0)); QVERIFY(configureRequestedSpy->wait()); QCOMPARE(configureRequestedSpy->count(), 4); states = configureRequestedSpy->last().at(1).value(); QVERIFY(states.testFlag(XdgShellSurface::State::Activated)); QVERIFY(states.testFlag(XdgShellSurface::State::Resizing)); QCOMPARE(surfaceSizeChangedSpy.count(), 1); QCOMPARE(surfaceSizeChangedSpy.last().first().toSize(), QSize(488, 640)); QCOMPARE(clientStepUserMovedResizedSpy.count(), 0); shellSurface->ackConfigure(configureRequestedSpy->last().at(2).value()); Test::render(surface.data(), QSize(488, 640), Qt::blue); - QVERIFY(geometryChangedSpy.wait()); + QVERIFY(frameGeometryChangedSpy.wait()); QCOMPARE(client->size(), QSize(488, 640)); QCOMPARE(clientStepUserMovedResizedSpy.count(), 1); client->keyPressEvent(Qt::Key_Enter); QCOMPARE(clientFinishUserMovedResizedSpy.count(), 1); QCOMPARE(workspace()->moveResizeClient(), nullptr); QVERIFY(!client->isMove()); QVERIFY(!client->isResize()); QEXPECT_FAIL("", "Interactive resize is not spec-compliant", Continue); QVERIFY(configureRequestedSpy->wait(10)); QEXPECT_FAIL("", "Interactive resize is not spec-compliant", Continue); QCOMPARE(configureRequestedSpy->count(), 5); // If the client appears again, it should have the last known size. shellSurface.reset(); surface.reset(); QVERIFY(Test::waitForWindowDestroyed(client)); surface.reset(Test::createSurface()); shellSurface.reset(createXdgShellSurface(type, surface.data(), surface.data(), Test::CreationSetup::CreateOnly)); configureRequestedSpy.reset(new QSignalSpy(shellSurface.data(), &XdgShellSurface::configureRequested)); shellSurface->setAppId("org.kde.foo"); surface->commit(Surface::CommitFlag::None); QVERIFY(configureRequestedSpy->wait()); QCOMPARE(configureRequestedSpy->count(), 1); QCOMPARE(configureRequestedSpy->last().first().toSize(), QSize(488, 640)); shellSurface->ackConfigure(configureRequestedSpy->last().at(2).value()); client = Test::renderAndWaitForShown(surface.data(), QSize(488, 640), Qt::blue); QVERIFY(client); QVERIFY(client->isActive()); QVERIFY(client->isResizable()); QCOMPARE(client->size(), QSize(488, 640)); QVERIFY(configureRequestedSpy->wait()); QCOMPARE(configureRequestedSpy->count(), 2); // Destroy the client. shellSurface.reset(); surface.reset(); QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testSizeForce) void TestXdgShellClientRules::testSizeForce() { // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("size", QSize(480, 640)); group.writeEntry("sizerule", int(Rules::Force)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // Create the test client. QFETCH(Test::XdgShellSurfaceType, type); QScopedPointer surface; surface.reset(Test::createSurface()); QScopedPointer shellSurface; shellSurface.reset(createXdgShellSurface(type, surface.data(), surface.data(), Test::CreationSetup::CreateOnly)); QScopedPointer configureRequestedSpy; configureRequestedSpy.reset(new QSignalSpy(shellSurface.data(), &XdgShellSurface::configureRequested)); shellSurface->setAppId("org.kde.foo"); surface->commit(Surface::CommitFlag::None); // The initial configure event should contain size hint set by the rule. QVERIFY(configureRequestedSpy->wait()); QCOMPARE(configureRequestedSpy->count(), 1); QCOMPARE(configureRequestedSpy->last().first().toSize(), QSize(480, 640)); // Map the client. shellSurface->ackConfigure(configureRequestedSpy->last().at(2).value()); XdgShellClient *client = Test::renderAndWaitForShown(surface.data(), QSize(480, 640), Qt::blue); QVERIFY(client); QVERIFY(client->isActive()); QVERIFY(!client->isResizable()); QCOMPARE(client->size(), QSize(480, 640)); // We should receive a configure event when the client becomes active. QVERIFY(configureRequestedSpy->wait()); QCOMPARE(configureRequestedSpy->count(), 2); // Any attempt to resize the client should not succeed. QSignalSpy clientStartMoveResizedSpy(client, &AbstractClient::clientStartUserMovedResized); QVERIFY(clientStartMoveResizedSpy.isValid()); QCOMPARE(workspace()->moveResizeClient(), nullptr); QVERIFY(!client->isMove()); QVERIFY(!client->isResize()); workspace()->slotWindowResize(); QCOMPARE(workspace()->moveResizeClient(), nullptr); QCOMPARE(clientStartMoveResizedSpy.count(), 0); QVERIFY(!client->isMove()); QVERIFY(!client->isResize()); QVERIFY(!configureRequestedSpy->wait(100)); // If the client appears again, the size should still be forced. shellSurface.reset(); surface.reset(); QVERIFY(Test::waitForWindowDestroyed(client)); surface.reset(Test::createSurface()); shellSurface.reset(createXdgShellSurface(type, surface.data(), surface.data(), Test::CreationSetup::CreateOnly)); configureRequestedSpy.reset(new QSignalSpy(shellSurface.data(), &XdgShellSurface::configureRequested)); shellSurface->setAppId("org.kde.foo"); surface->commit(Surface::CommitFlag::None); QVERIFY(configureRequestedSpy->wait()); QCOMPARE(configureRequestedSpy->count(), 1); QCOMPARE(configureRequestedSpy->last().first().toSize(), QSize(480, 640)); shellSurface->ackConfigure(configureRequestedSpy->last().at(2).value()); client = Test::renderAndWaitForShown(surface.data(), QSize(480, 640), Qt::blue); QVERIFY(client); QVERIFY(client->isActive()); QVERIFY(!client->isResizable()); QCOMPARE(client->size(), QSize(480, 640)); QVERIFY(configureRequestedSpy->wait()); QCOMPARE(configureRequestedSpy->count(), 2); // Destroy the client. shellSurface.reset(); surface.reset(); QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testSizeApplyNow) void TestXdgShellClientRules::testSizeApplyNow() { // Create the test client. QFETCH(Test::XdgShellSurfaceType, type); QScopedPointer surface; surface.reset(Test::createSurface()); QScopedPointer shellSurface; shellSurface.reset(createXdgShellSurface(type, surface.data(), surface.data(), Test::CreationSetup::CreateOnly)); QScopedPointer configureRequestedSpy; configureRequestedSpy.reset(new QSignalSpy(shellSurface.data(), &XdgShellSurface::configureRequested)); shellSurface->setAppId("org.kde.foo"); surface->commit(Surface::CommitFlag::None); // The expected surface dimensions should be set by the rule. QVERIFY(configureRequestedSpy->wait()); QCOMPARE(configureRequestedSpy->count(), 1); QCOMPARE(configureRequestedSpy->last().first().toSize(), QSize(0, 0)); // Map the client. shellSurface->ackConfigure(configureRequestedSpy->last().at(2).value()); XdgShellClient *client = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); QVERIFY(client); QVERIFY(client->isActive()); QVERIFY(client->isResizable()); QCOMPARE(client->size(), QSize(100, 50)); // We should receive a configure event when the client becomes active. QVERIFY(configureRequestedSpy->wait()); QCOMPARE(configureRequestedSpy->count(), 2); // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("size", QSize(480, 640)); group.writeEntry("sizerule", int(Rules::ApplyNow)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // The compositor should send a configure event with a new size. QVERIFY(configureRequestedSpy->wait()); QCOMPARE(configureRequestedSpy->count(), 3); QCOMPARE(configureRequestedSpy->last().first().toSize(), QSize(480, 640)); // Draw the surface with the new size. - QSignalSpy geometryChangedSpy(client, &AbstractClient::geometryChanged); - QVERIFY(geometryChangedSpy.isValid()); + QSignalSpy frameGeometryChangedSpy(client, &AbstractClient::frameGeometryChanged); + QVERIFY(frameGeometryChangedSpy.isValid()); shellSurface->ackConfigure(configureRequestedSpy->last().at(2).value()); Test::render(surface.data(), QSize(480, 640), Qt::blue); - QVERIFY(geometryChangedSpy.wait()); + QVERIFY(frameGeometryChangedSpy.wait()); QCOMPARE(client->size(), QSize(480, 640)); QVERIFY(!configureRequestedSpy->wait(100)); // The rule should not be applied again. client->evaluateWindowRules(); QVERIFY(!configureRequestedSpy->wait(100)); // Destroy the client. shellSurface.reset(); surface.reset(); QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testSizeForceTemporarily) void TestXdgShellClientRules::testSizeForceTemporarily() { // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("size", QSize(480, 640)); group.writeEntry("sizerule", int(Rules::ForceTemporarily)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // Create the test client. QFETCH(Test::XdgShellSurfaceType, type); QScopedPointer surface; surface.reset(Test::createSurface()); QScopedPointer shellSurface; shellSurface.reset(createXdgShellSurface(type, surface.data(), surface.data(), Test::CreationSetup::CreateOnly)); QScopedPointer configureRequestedSpy; configureRequestedSpy.reset(new QSignalSpy(shellSurface.data(), &XdgShellSurface::configureRequested)); shellSurface->setAppId("org.kde.foo"); surface->commit(Surface::CommitFlag::None); // The initial configure event should contain size hint set by the rule. QVERIFY(configureRequestedSpy->wait()); QCOMPARE(configureRequestedSpy->count(), 1); QCOMPARE(configureRequestedSpy->last().first().toSize(), QSize(480, 640)); // Map the client. shellSurface->ackConfigure(configureRequestedSpy->last().at(2).value()); XdgShellClient *client = Test::renderAndWaitForShown(surface.data(), QSize(480, 640), Qt::blue); QVERIFY(client); QVERIFY(client->isActive()); QVERIFY(!client->isResizable()); QCOMPARE(client->size(), QSize(480, 640)); // We should receive a configure event when the client becomes active. QVERIFY(configureRequestedSpy->wait()); QCOMPARE(configureRequestedSpy->count(), 2); // Any attempt to resize the client should not succeed. QSignalSpy clientStartMoveResizedSpy(client, &AbstractClient::clientStartUserMovedResized); QVERIFY(clientStartMoveResizedSpy.isValid()); QCOMPARE(workspace()->moveResizeClient(), nullptr); QVERIFY(!client->isMove()); QVERIFY(!client->isResize()); workspace()->slotWindowResize(); QCOMPARE(workspace()->moveResizeClient(), nullptr); QCOMPARE(clientStartMoveResizedSpy.count(), 0); QVERIFY(!client->isMove()); QVERIFY(!client->isResize()); QVERIFY(!configureRequestedSpy->wait(100)); // The rule should be discarded when the client is closed. shellSurface.reset(); surface.reset(); QVERIFY(Test::waitForWindowDestroyed(client)); surface.reset(Test::createSurface()); shellSurface.reset(createXdgShellSurface(type, surface.data(), surface.data(), Test::CreationSetup::CreateOnly)); configureRequestedSpy.reset(new QSignalSpy(shellSurface.data(), &XdgShellSurface::configureRequested)); shellSurface->setAppId("org.kde.foo"); surface->commit(Surface::CommitFlag::None); QVERIFY(configureRequestedSpy->wait()); QCOMPARE(configureRequestedSpy->count(), 1); QCOMPARE(configureRequestedSpy->last().first().toSize(), QSize(0, 0)); shellSurface->ackConfigure(configureRequestedSpy->last().at(2).value()); client = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); QVERIFY(client); QVERIFY(client->isActive()); QVERIFY(client->isResizable()); QCOMPARE(client->size(), QSize(100, 50)); QVERIFY(configureRequestedSpy->wait()); QCOMPARE(configureRequestedSpy->count(), 2); // Destroy the client. shellSurface.reset(); surface.reset(); QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testMaximizeDontAffect) void TestXdgShellClientRules::testMaximizeDontAffect() { // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("maximizehoriz", true); group.writeEntry("maximizehorizrule", int(Rules::DontAffect)); group.writeEntry("maximizevert", true); group.writeEntry("maximizevertrule", int(Rules::DontAffect)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // Create the test client. QFETCH(Test::XdgShellSurfaceType, type); QScopedPointer surface; surface.reset(Test::createSurface()); QScopedPointer shellSurface; shellSurface.reset(createXdgShellSurface(type, surface.data(), surface.data(), Test::CreationSetup::CreateOnly)); QScopedPointer configureRequestedSpy; configureRequestedSpy.reset(new QSignalSpy(shellSurface.data(), &XdgShellSurface::configureRequested)); shellSurface->setAppId("org.kde.foo"); surface->commit(Surface::CommitFlag::None); // Wait for the initial configure event. XdgShellSurface::States states; QVERIFY(configureRequestedSpy->wait()); QCOMPARE(configureRequestedSpy->count(), 1); QCOMPARE(configureRequestedSpy->last().at(0).toSize(), QSize(0, 0)); states = configureRequestedSpy->last().at(1).value(); QVERIFY(!states.testFlag(XdgShellSurface::State::Activated)); QVERIFY(!states.testFlag(XdgShellSurface::State::Maximized)); // Map the client. shellSurface->ackConfigure(configureRequestedSpy->last().at(2).value()); XdgShellClient *client = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); QVERIFY(client); QVERIFY(client->isActive()); QVERIFY(client->isMaximizable()); QCOMPARE(client->maximizeMode(), MaximizeMode::MaximizeRestore); QCOMPARE(client->requestedMaximizeMode(), MaximizeMode::MaximizeRestore); QCOMPARE(client->size(), QSize(100, 50)); // We should receive a configure event when the client becomes active. QVERIFY(configureRequestedSpy->wait()); QCOMPARE(configureRequestedSpy->count(), 2); states = configureRequestedSpy->last().at(1).value(); QVERIFY(states.testFlag(XdgShellSurface::State::Activated)); QVERIFY(!states.testFlag(XdgShellSurface::State::Maximized)); // Destroy the client. shellSurface.reset(); surface.reset(); QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testMaximizeApply) void TestXdgShellClientRules::testMaximizeApply() { // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("maximizehoriz", true); group.writeEntry("maximizehorizrule", int(Rules::Apply)); group.writeEntry("maximizevert", true); group.writeEntry("maximizevertrule", int(Rules::Apply)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // Create the test client. QFETCH(Test::XdgShellSurfaceType, type); QScopedPointer surface; surface.reset(Test::createSurface()); QScopedPointer shellSurface; shellSurface.reset(createXdgShellSurface(type, surface.data(), surface.data(), Test::CreationSetup::CreateOnly)); QScopedPointer configureRequestedSpy; configureRequestedSpy.reset(new QSignalSpy(shellSurface.data(), &XdgShellSurface::configureRequested)); shellSurface->setAppId("org.kde.foo"); surface->commit(Surface::CommitFlag::None); // Wait for the initial configure event. XdgShellSurface::States states; QVERIFY(configureRequestedSpy->wait()); QCOMPARE(configureRequestedSpy->count(), 1); QCOMPARE(configureRequestedSpy->last().at(0).toSize(), QSize(1280, 1024)); states = configureRequestedSpy->last().at(1).value(); QVERIFY(!states.testFlag(XdgShellSurface::State::Activated)); QVERIFY(states.testFlag(XdgShellSurface::State::Maximized)); // Map the client. shellSurface->ackConfigure(configureRequestedSpy->last().at(2).value()); XdgShellClient *client = Test::renderAndWaitForShown(surface.data(), QSize(1280, 1024), Qt::blue); QVERIFY(client); QVERIFY(client->isActive()); QVERIFY(client->isMaximizable()); QCOMPARE(client->maximizeMode(), MaximizeMode::MaximizeFull); QCOMPARE(client->requestedMaximizeMode(), MaximizeMode::MaximizeFull); QCOMPARE(client->size(), QSize(1280, 1024)); // We should receive a configure event when the client becomes active. QVERIFY(configureRequestedSpy->wait()); QCOMPARE(configureRequestedSpy->count(), 2); states = configureRequestedSpy->last().at(1).value(); QVERIFY(states.testFlag(XdgShellSurface::State::Activated)); QVERIFY(states.testFlag(XdgShellSurface::State::Maximized)); // One should still be able to change the maximized state of the client. workspace()->slotWindowMaximize(); QVERIFY(configureRequestedSpy->wait()); QCOMPARE(configureRequestedSpy->count(), 3); QCOMPARE(configureRequestedSpy->last().at(0).toSize(), QSize(0, 0)); states = configureRequestedSpy->last().at(1).value(); QVERIFY(states.testFlag(XdgShellSurface::State::Activated)); QVERIFY(!states.testFlag(XdgShellSurface::State::Maximized)); - QSignalSpy geometryChangedSpy(client, &AbstractClient::geometryChanged); - QVERIFY(geometryChangedSpy.isValid()); + QSignalSpy frameGeometryChangedSpy(client, &AbstractClient::frameGeometryChanged); + QVERIFY(frameGeometryChangedSpy.isValid()); shellSurface->ackConfigure(configureRequestedSpy->last().at(2).value()); Test::render(surface.data(), QSize(100, 50), Qt::blue); - QVERIFY(geometryChangedSpy.wait()); + QVERIFY(frameGeometryChangedSpy.wait()); QCOMPARE(client->size(), QSize(100, 50)); QCOMPARE(client->maximizeMode(), MaximizeMode::MaximizeRestore); QCOMPARE(client->requestedMaximizeMode(), MaximizeMode::MaximizeRestore); // If we create the client again, it should be initially maximized. shellSurface.reset(); surface.reset(); QVERIFY(Test::waitForWindowDestroyed(client)); surface.reset(Test::createSurface()); shellSurface.reset(createXdgShellSurface(type, surface.data(), surface.data(), Test::CreationSetup::CreateOnly)); configureRequestedSpy.reset(new QSignalSpy(shellSurface.data(), &XdgShellSurface::configureRequested)); shellSurface->setAppId("org.kde.foo"); surface->commit(Surface::CommitFlag::None); QVERIFY(configureRequestedSpy->wait()); QCOMPARE(configureRequestedSpy->count(), 1); QCOMPARE(configureRequestedSpy->last().at(0).toSize(), QSize(1280, 1024)); states = configureRequestedSpy->last().at(1).value(); QVERIFY(!states.testFlag(XdgShellSurface::State::Activated)); QVERIFY(states.testFlag(XdgShellSurface::State::Maximized)); shellSurface->ackConfigure(configureRequestedSpy->last().at(2).value()); client = Test::renderAndWaitForShown(surface.data(), QSize(1280, 1024), Qt::blue); QVERIFY(client); QVERIFY(client->isActive()); QVERIFY(client->isMaximizable()); QCOMPARE(client->maximizeMode(), MaximizeMode::MaximizeFull); QCOMPARE(client->requestedMaximizeMode(), MaximizeMode::MaximizeFull); QCOMPARE(client->size(), QSize(1280, 1024)); QVERIFY(configureRequestedSpy->wait()); QCOMPARE(configureRequestedSpy->count(), 2); states = configureRequestedSpy->last().at(1).value(); QVERIFY(states.testFlag(XdgShellSurface::State::Activated)); QVERIFY(states.testFlag(XdgShellSurface::State::Maximized)); // Destroy the client. shellSurface.reset(); surface.reset(); QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testMaximizeRemember) void TestXdgShellClientRules::testMaximizeRemember() { // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("maximizehoriz", true); group.writeEntry("maximizehorizrule", int(Rules::Remember)); group.writeEntry("maximizevert", true); group.writeEntry("maximizevertrule", int(Rules::Remember)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // Create the test client. QFETCH(Test::XdgShellSurfaceType, type); QScopedPointer surface; surface.reset(Test::createSurface()); QScopedPointer shellSurface; shellSurface.reset(createXdgShellSurface(type, surface.data(), surface.data(), Test::CreationSetup::CreateOnly)); QScopedPointer configureRequestedSpy; configureRequestedSpy.reset(new QSignalSpy(shellSurface.data(), &XdgShellSurface::configureRequested)); shellSurface->setAppId("org.kde.foo"); surface->commit(Surface::CommitFlag::None); // Wait for the initial configure event. XdgShellSurface::States states; QVERIFY(configureRequestedSpy->wait()); QCOMPARE(configureRequestedSpy->count(), 1); QCOMPARE(configureRequestedSpy->last().at(0).toSize(), QSize(1280, 1024)); states = configureRequestedSpy->last().at(1).value(); QVERIFY(!states.testFlag(XdgShellSurface::State::Activated)); QVERIFY(states.testFlag(XdgShellSurface::State::Maximized)); // Map the client. shellSurface->ackConfigure(configureRequestedSpy->last().at(2).value()); XdgShellClient *client = Test::renderAndWaitForShown(surface.data(), QSize(1280, 1024), Qt::blue); QVERIFY(client); QVERIFY(client->isActive()); QVERIFY(client->isMaximizable()); QCOMPARE(client->maximizeMode(), MaximizeMode::MaximizeFull); QCOMPARE(client->requestedMaximizeMode(), MaximizeMode::MaximizeFull); QCOMPARE(client->size(), QSize(1280, 1024)); // We should receive a configure event when the client becomes active. QVERIFY(configureRequestedSpy->wait()); QCOMPARE(configureRequestedSpy->count(), 2); states = configureRequestedSpy->last().at(1).value(); QVERIFY(states.testFlag(XdgShellSurface::State::Activated)); QVERIFY(states.testFlag(XdgShellSurface::State::Maximized)); // One should still be able to change the maximized state of the client. workspace()->slotWindowMaximize(); QVERIFY(configureRequestedSpy->wait()); QCOMPARE(configureRequestedSpy->count(), 3); QCOMPARE(configureRequestedSpy->last().at(0).toSize(), QSize(0, 0)); states = configureRequestedSpy->last().at(1).value(); QVERIFY(states.testFlag(XdgShellSurface::State::Activated)); QVERIFY(!states.testFlag(XdgShellSurface::State::Maximized)); - QSignalSpy geometryChangedSpy(client, &AbstractClient::geometryChanged); - QVERIFY(geometryChangedSpy.isValid()); + QSignalSpy frameGeometryChangedSpy(client, &AbstractClient::frameGeometryChanged); + QVERIFY(frameGeometryChangedSpy.isValid()); shellSurface->ackConfigure(configureRequestedSpy->last().at(2).value()); Test::render(surface.data(), QSize(100, 50), Qt::blue); - QVERIFY(geometryChangedSpy.wait()); + QVERIFY(frameGeometryChangedSpy.wait()); QCOMPARE(client->size(), QSize(100, 50)); QCOMPARE(client->maximizeMode(), MaximizeMode::MaximizeRestore); QCOMPARE(client->requestedMaximizeMode(), MaximizeMode::MaximizeRestore); // If we create the client again, it should not be maximized (because last time it wasn't). shellSurface.reset(); surface.reset(); QVERIFY(Test::waitForWindowDestroyed(client)); surface.reset(Test::createSurface()); shellSurface.reset(createXdgShellSurface(type, surface.data(), surface.data(), Test::CreationSetup::CreateOnly)); configureRequestedSpy.reset(new QSignalSpy(shellSurface.data(), &XdgShellSurface::configureRequested)); shellSurface->setAppId("org.kde.foo"); surface->commit(Surface::CommitFlag::None); QVERIFY(configureRequestedSpy->wait()); QCOMPARE(configureRequestedSpy->count(), 1); QCOMPARE(configureRequestedSpy->last().at(0).toSize(), QSize(0, 0)); states = configureRequestedSpy->last().at(1).value(); QVERIFY(!states.testFlag(XdgShellSurface::State::Activated)); QVERIFY(!states.testFlag(XdgShellSurface::State::Maximized)); shellSurface->ackConfigure(configureRequestedSpy->last().at(2).value()); client = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); QVERIFY(client); QVERIFY(client->isActive()); QVERIFY(client->isMaximizable()); QCOMPARE(client->maximizeMode(), MaximizeMode::MaximizeRestore); QCOMPARE(client->requestedMaximizeMode(), MaximizeMode::MaximizeRestore); QCOMPARE(client->size(), QSize(100, 50)); QVERIFY(configureRequestedSpy->wait()); QCOMPARE(configureRequestedSpy->count(), 2); states = configureRequestedSpy->last().at(1).value(); QVERIFY(states.testFlag(XdgShellSurface::State::Activated)); QVERIFY(!states.testFlag(XdgShellSurface::State::Maximized)); // Destroy the client. shellSurface.reset(); surface.reset(); QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testMaximizeForce) void TestXdgShellClientRules::testMaximizeForce() { // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("maximizehoriz", true); group.writeEntry("maximizehorizrule", int(Rules::Force)); group.writeEntry("maximizevert", true); group.writeEntry("maximizevertrule", int(Rules::Force)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // Create the test client. QFETCH(Test::XdgShellSurfaceType, type); QScopedPointer surface; surface.reset(Test::createSurface()); QScopedPointer shellSurface; shellSurface.reset(createXdgShellSurface(type, surface.data(), surface.data(), Test::CreationSetup::CreateOnly)); QScopedPointer configureRequestedSpy; configureRequestedSpy.reset(new QSignalSpy(shellSurface.data(), &XdgShellSurface::configureRequested)); shellSurface->setAppId("org.kde.foo"); surface->commit(Surface::CommitFlag::None); // Wait for the initial configure event. XdgShellSurface::States states; QVERIFY(configureRequestedSpy->wait()); QCOMPARE(configureRequestedSpy->count(), 1); QCOMPARE(configureRequestedSpy->last().at(0).toSize(), QSize(1280, 1024)); states = configureRequestedSpy->last().at(1).value(); QVERIFY(!states.testFlag(XdgShellSurface::State::Activated)); QVERIFY(states.testFlag(XdgShellSurface::State::Maximized)); // Map the client. shellSurface->ackConfigure(configureRequestedSpy->last().at(2).value()); XdgShellClient *client = Test::renderAndWaitForShown(surface.data(), QSize(1280, 1024), Qt::blue); QVERIFY(client); QVERIFY(client->isActive()); QVERIFY(!client->isMaximizable()); QCOMPARE(client->maximizeMode(), MaximizeMode::MaximizeFull); QCOMPARE(client->requestedMaximizeMode(), MaximizeMode::MaximizeFull); QCOMPARE(client->size(), QSize(1280, 1024)); // We should receive a configure event when the client becomes active. QVERIFY(configureRequestedSpy->wait()); QCOMPARE(configureRequestedSpy->count(), 2); states = configureRequestedSpy->last().at(1).value(); QVERIFY(states.testFlag(XdgShellSurface::State::Activated)); QVERIFY(states.testFlag(XdgShellSurface::State::Maximized)); // Any attempt to change the maximized state should not succeed. const QRect oldGeometry = client->frameGeometry(); workspace()->slotWindowMaximize(); QVERIFY(!configureRequestedSpy->wait(100)); QCOMPARE(client->maximizeMode(), MaximizeMode::MaximizeFull); QCOMPARE(client->requestedMaximizeMode(), MaximizeMode::MaximizeFull); QCOMPARE(client->frameGeometry(), oldGeometry); // If we create the client again, the maximized state should still be forced. shellSurface.reset(); surface.reset(); QVERIFY(Test::waitForWindowDestroyed(client)); surface.reset(Test::createSurface()); shellSurface.reset(createXdgShellSurface(type, surface.data(), surface.data(), Test::CreationSetup::CreateOnly)); configureRequestedSpy.reset(new QSignalSpy(shellSurface.data(), &XdgShellSurface::configureRequested)); shellSurface->setAppId("org.kde.foo"); surface->commit(Surface::CommitFlag::None); QVERIFY(configureRequestedSpy->wait()); QCOMPARE(configureRequestedSpy->count(), 1); QCOMPARE(configureRequestedSpy->last().at(0).toSize(), QSize(1280, 1024)); states = configureRequestedSpy->last().at(1).value(); QVERIFY(!states.testFlag(XdgShellSurface::State::Activated)); QVERIFY(states.testFlag(XdgShellSurface::State::Maximized)); shellSurface->ackConfigure(configureRequestedSpy->last().at(2).value()); client = Test::renderAndWaitForShown(surface.data(), QSize(1280, 1024), Qt::blue); QVERIFY(client); QVERIFY(client->isActive()); QVERIFY(!client->isMaximizable()); QCOMPARE(client->maximizeMode(), MaximizeMode::MaximizeFull); QCOMPARE(client->requestedMaximizeMode(), MaximizeMode::MaximizeFull); QCOMPARE(client->size(), QSize(1280, 1024)); QVERIFY(configureRequestedSpy->wait()); QCOMPARE(configureRequestedSpy->count(), 2); states = configureRequestedSpy->last().at(1).value(); QVERIFY(states.testFlag(XdgShellSurface::State::Activated)); QVERIFY(states.testFlag(XdgShellSurface::State::Maximized)); // Destroy the client. shellSurface.reset(); surface.reset(); QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testMaximizeApplyNow) void TestXdgShellClientRules::testMaximizeApplyNow() { // Create the test client. QFETCH(Test::XdgShellSurfaceType, type); QScopedPointer surface; surface.reset(Test::createSurface()); QScopedPointer shellSurface; shellSurface.reset(createXdgShellSurface(type, surface.data(), surface.data(), Test::CreationSetup::CreateOnly)); QScopedPointer configureRequestedSpy; configureRequestedSpy.reset(new QSignalSpy(shellSurface.data(), &XdgShellSurface::configureRequested)); shellSurface->setAppId("org.kde.foo"); surface->commit(Surface::CommitFlag::None); // Wait for the initial configure event. XdgShellSurface::States states; QVERIFY(configureRequestedSpy->wait()); QCOMPARE(configureRequestedSpy->count(), 1); QCOMPARE(configureRequestedSpy->last().at(0).toSize(), QSize(0, 0)); states = configureRequestedSpy->last().at(1).value(); QVERIFY(!states.testFlag(XdgShellSurface::State::Activated)); QVERIFY(!states.testFlag(XdgShellSurface::State::Maximized)); // Map the client. shellSurface->ackConfigure(configureRequestedSpy->last().at(2).value()); XdgShellClient *client = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); QVERIFY(client); QVERIFY(client->isActive()); QVERIFY(client->isMaximizable()); QCOMPARE(client->maximizeMode(), MaximizeMode::MaximizeRestore); QCOMPARE(client->requestedMaximizeMode(), MaximizeMode::MaximizeRestore); QCOMPARE(client->size(), QSize(100, 50)); // We should receive a configure event when the client becomes active. QVERIFY(configureRequestedSpy->wait()); QCOMPARE(configureRequestedSpy->count(), 2); states = configureRequestedSpy->last().at(1).value(); QVERIFY(states.testFlag(XdgShellSurface::State::Activated)); QVERIFY(!states.testFlag(XdgShellSurface::State::Maximized)); // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("maximizehoriz", true); group.writeEntry("maximizehorizrule", int(Rules::ApplyNow)); group.writeEntry("maximizevert", true); group.writeEntry("maximizevertrule", int(Rules::ApplyNow)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // We should receive a configure event with a new surface size. QVERIFY(configureRequestedSpy->wait()); QCOMPARE(configureRequestedSpy->count(), 3); QCOMPARE(configureRequestedSpy->last().at(0).toSize(), QSize(1280, 1024)); states = configureRequestedSpy->last().at(1).value(); QVERIFY(states.testFlag(XdgShellSurface::State::Activated)); QVERIFY(states.testFlag(XdgShellSurface::State::Maximized)); // Draw contents of the maximized client. - QSignalSpy geometryChangedSpy(client, &AbstractClient::geometryChanged); - QVERIFY(geometryChangedSpy.isValid()); + QSignalSpy frameGeometryChangedSpy(client, &AbstractClient::frameGeometryChanged); + QVERIFY(frameGeometryChangedSpy.isValid()); shellSurface->ackConfigure(configureRequestedSpy->last().at(2).value()); Test::render(surface.data(), QSize(1280, 1024), Qt::blue); - QVERIFY(geometryChangedSpy.wait()); + QVERIFY(frameGeometryChangedSpy.wait()); QCOMPARE(client->size(), QSize(1280, 1024)); QCOMPARE(client->maximizeMode(), MaximizeMode::MaximizeFull); QCOMPARE(client->requestedMaximizeMode(), MaximizeMode::MaximizeFull); // The client still has to be maximizeable. QVERIFY(client->isMaximizable()); // Restore the client. workspace()->slotWindowMaximize(); QVERIFY(configureRequestedSpy->wait()); QCOMPARE(configureRequestedSpy->count(), 4); QCOMPARE(configureRequestedSpy->last().at(0).toSize(), QSize(100, 50)); states = configureRequestedSpy->last().at(1).value(); QVERIFY(states.testFlag(XdgShellSurface::State::Activated)); QVERIFY(!states.testFlag(XdgShellSurface::State::Maximized)); shellSurface->ackConfigure(configureRequestedSpy->last().at(2).value()); Test::render(surface.data(), QSize(100, 50), Qt::blue); - QVERIFY(geometryChangedSpy.wait()); + QVERIFY(frameGeometryChangedSpy.wait()); QCOMPARE(client->size(), QSize(100, 50)); QCOMPARE(client->maximizeMode(), MaximizeMode::MaximizeRestore); QCOMPARE(client->requestedMaximizeMode(), MaximizeMode::MaximizeRestore); // The rule should be discarded after it's been applied. const QRect oldGeometry = client->frameGeometry(); client->evaluateWindowRules(); QVERIFY(!configureRequestedSpy->wait(100)); QCOMPARE(client->maximizeMode(), MaximizeMode::MaximizeRestore); QCOMPARE(client->requestedMaximizeMode(), MaximizeMode::MaximizeRestore); QCOMPARE(client->frameGeometry(), oldGeometry); // Destroy the client. shellSurface.reset(); surface.reset(); QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testMaximizeForceTemporarily) void TestXdgShellClientRules::testMaximizeForceTemporarily() { // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("maximizehoriz", true); group.writeEntry("maximizehorizrule", int(Rules::ForceTemporarily)); group.writeEntry("maximizevert", true); group.writeEntry("maximizevertrule", int(Rules::ForceTemporarily)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // Create the test client. QFETCH(Test::XdgShellSurfaceType, type); QScopedPointer surface; surface.reset(Test::createSurface()); QScopedPointer shellSurface; shellSurface.reset(createXdgShellSurface(type, surface.data(), surface.data(), Test::CreationSetup::CreateOnly)); QScopedPointer configureRequestedSpy; configureRequestedSpy.reset(new QSignalSpy(shellSurface.data(), &XdgShellSurface::configureRequested)); shellSurface->setAppId("org.kde.foo"); surface->commit(Surface::CommitFlag::None); // Wait for the initial configure event. XdgShellSurface::States states; QVERIFY(configureRequestedSpy->wait()); QCOMPARE(configureRequestedSpy->count(), 1); QCOMPARE(configureRequestedSpy->last().at(0).toSize(), QSize(1280, 1024)); states = configureRequestedSpy->last().at(1).value(); QVERIFY(!states.testFlag(XdgShellSurface::State::Activated)); QVERIFY(states.testFlag(XdgShellSurface::State::Maximized)); // Map the client. shellSurface->ackConfigure(configureRequestedSpy->last().at(2).value()); XdgShellClient *client = Test::renderAndWaitForShown(surface.data(), QSize(1280, 1024), Qt::blue); QVERIFY(client); QVERIFY(client->isActive()); QVERIFY(!client->isMaximizable()); QCOMPARE(client->maximizeMode(), MaximizeMode::MaximizeFull); QCOMPARE(client->requestedMaximizeMode(), MaximizeMode::MaximizeFull); QCOMPARE(client->size(), QSize(1280, 1024)); // We should receive a configure event when the client becomes active. QVERIFY(configureRequestedSpy->wait()); QCOMPARE(configureRequestedSpy->count(), 2); states = configureRequestedSpy->last().at(1).value(); QVERIFY(states.testFlag(XdgShellSurface::State::Activated)); QVERIFY(states.testFlag(XdgShellSurface::State::Maximized)); // Any attempt to change the maximized state should not succeed. const QRect oldGeometry = client->frameGeometry(); workspace()->slotWindowMaximize(); QVERIFY(!configureRequestedSpy->wait(100)); QCOMPARE(client->maximizeMode(), MaximizeMode::MaximizeFull); QCOMPARE(client->requestedMaximizeMode(), MaximizeMode::MaximizeFull); QCOMPARE(client->frameGeometry(), oldGeometry); // The rule should be discarded if we close the client. shellSurface.reset(); surface.reset(); QVERIFY(Test::waitForWindowDestroyed(client)); surface.reset(Test::createSurface()); shellSurface.reset(createXdgShellSurface(type, surface.data(), surface.data(), Test::CreationSetup::CreateOnly)); configureRequestedSpy.reset(new QSignalSpy(shellSurface.data(), &XdgShellSurface::configureRequested)); shellSurface->setAppId("org.kde.foo"); surface->commit(Surface::CommitFlag::None); QVERIFY(configureRequestedSpy->wait()); QCOMPARE(configureRequestedSpy->count(), 1); QCOMPARE(configureRequestedSpy->last().at(0).toSize(), QSize(0, 0)); states = configureRequestedSpy->last().at(1).value(); QVERIFY(!states.testFlag(XdgShellSurface::State::Activated)); QVERIFY(!states.testFlag(XdgShellSurface::State::Maximized)); shellSurface->ackConfigure(configureRequestedSpy->last().at(2).value()); client = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); QVERIFY(client); QVERIFY(client->isActive()); QVERIFY(client->isMaximizable()); QCOMPARE(client->maximizeMode(), MaximizeMode::MaximizeRestore); QCOMPARE(client->requestedMaximizeMode(), MaximizeMode::MaximizeRestore); QCOMPARE(client->size(), QSize(100, 50)); QVERIFY(configureRequestedSpy->wait()); QCOMPARE(configureRequestedSpy->count(), 2); states = configureRequestedSpy->last().at(1).value(); QVERIFY(states.testFlag(XdgShellSurface::State::Activated)); QVERIFY(!states.testFlag(XdgShellSurface::State::Maximized)); // Destroy the client. shellSurface.reset(); surface.reset(); QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testDesktopDontAffect) void TestXdgShellClientRules::testDesktopDontAffect() { // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("desktop", 2); group.writeEntry("desktoprule", int(Rules::DontAffect)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // We need at least two virtual desktop for this test. VirtualDesktopManager::self()->setCount(2); QCOMPARE(VirtualDesktopManager::self()->count(), 2u); VirtualDesktopManager::self()->setCurrent(1); QCOMPARE(VirtualDesktopManager::self()->current(), 1); // Create the test client. QFETCH(Test::XdgShellSurfaceType, type); XdgShellClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); // The client should appear on the current virtual desktop. QCOMPARE(client->desktop(), 1); QCOMPARE(VirtualDesktopManager::self()->current(), 1); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testDesktopApply) void TestXdgShellClientRules::testDesktopApply() { // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("desktop", 2); group.writeEntry("desktoprule", int(Rules::Apply)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // We need at least two virtual desktop for this test. VirtualDesktopManager::self()->setCount(2); QCOMPARE(VirtualDesktopManager::self()->count(), 2u); VirtualDesktopManager::self()->setCurrent(1); QCOMPARE(VirtualDesktopManager::self()->current(), 1); // Create the test client. QFETCH(Test::XdgShellSurfaceType, type); XdgShellClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); // The client should appear on the second virtual desktop. QCOMPARE(client->desktop(), 2); QCOMPARE(VirtualDesktopManager::self()->current(), 2); // We still should be able to move the client between desktops. workspace()->sendClientToDesktop(client, 1, true); QCOMPARE(client->desktop(), 1); QCOMPARE(VirtualDesktopManager::self()->current(), 2); // If we re-open the client, it should appear on the second virtual desktop again. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); VirtualDesktopManager::self()->setCurrent(1); QCOMPARE(VirtualDesktopManager::self()->current(), 1); std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); QCOMPARE(client->desktop(), 2); QCOMPARE(VirtualDesktopManager::self()->current(), 2); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testDesktopRemember) void TestXdgShellClientRules::testDesktopRemember() { // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("desktop", 2); group.writeEntry("desktoprule", int(Rules::Remember)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // We need at least two virtual desktop for this test. VirtualDesktopManager::self()->setCount(2); QCOMPARE(VirtualDesktopManager::self()->count(), 2u); VirtualDesktopManager::self()->setCurrent(1); QCOMPARE(VirtualDesktopManager::self()->current(), 1); // Create the test client. QFETCH(Test::XdgShellSurfaceType, type); XdgShellClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); QCOMPARE(client->desktop(), 2); QCOMPARE(VirtualDesktopManager::self()->current(), 2); // Move the client to the first virtual desktop. workspace()->sendClientToDesktop(client, 1, true); QCOMPARE(client->desktop(), 1); QCOMPARE(VirtualDesktopManager::self()->current(), 2); // If we create the client again, it should appear on the first virtual desktop. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); QCOMPARE(client->desktop(), 1); QCOMPARE(VirtualDesktopManager::self()->current(), 1); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testDesktopForce) void TestXdgShellClientRules::testDesktopForce() { // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("desktop", 2); group.writeEntry("desktoprule", int(Rules::Force)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // We need at least two virtual desktop for this test. VirtualDesktopManager::self()->setCount(2); QCOMPARE(VirtualDesktopManager::self()->count(), 2u); VirtualDesktopManager::self()->setCurrent(1); QCOMPARE(VirtualDesktopManager::self()->current(), 1); // Create the test client. QFETCH(Test::XdgShellSurfaceType, type); XdgShellClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); // The client should appear on the second virtual desktop. QCOMPARE(client->desktop(), 2); QCOMPARE(VirtualDesktopManager::self()->current(), 2); // Any attempt to move the client to another virtual desktop should fail. workspace()->sendClientToDesktop(client, 1, true); QCOMPARE(client->desktop(), 2); QCOMPARE(VirtualDesktopManager::self()->current(), 2); // If we re-open the client, it should appear on the second virtual desktop again. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); VirtualDesktopManager::self()->setCurrent(1); QCOMPARE(VirtualDesktopManager::self()->current(), 1); std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); QCOMPARE(client->desktop(), 2); QCOMPARE(VirtualDesktopManager::self()->current(), 2); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testDesktopApplyNow) void TestXdgShellClientRules::testDesktopApplyNow() { // We need at least two virtual desktop for this test. VirtualDesktopManager::self()->setCount(2); QCOMPARE(VirtualDesktopManager::self()->count(), 2u); VirtualDesktopManager::self()->setCurrent(1); QCOMPARE(VirtualDesktopManager::self()->current(), 1); // Create the test client. QFETCH(Test::XdgShellSurfaceType, type); XdgShellClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); QCOMPARE(client->desktop(), 1); QCOMPARE(VirtualDesktopManager::self()->current(), 1); // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("desktop", 2); group.writeEntry("desktoprule", int(Rules::ApplyNow)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // The client should have been moved to the second virtual desktop. QCOMPARE(client->desktop(), 2); QCOMPARE(VirtualDesktopManager::self()->current(), 1); // One should still be able to move the client between desktops. workspace()->sendClientToDesktop(client, 1, true); QCOMPARE(client->desktop(), 1); QCOMPARE(VirtualDesktopManager::self()->current(), 1); // The rule should not be applied again. client->evaluateWindowRules(); QCOMPARE(client->desktop(), 1); QCOMPARE(VirtualDesktopManager::self()->current(), 1); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testDesktopForceTemporarily) void TestXdgShellClientRules::testDesktopForceTemporarily() { // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("desktop", 2); group.writeEntry("desktoprule", int(Rules::ForceTemporarily)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // We need at least two virtual desktop for this test. VirtualDesktopManager::self()->setCount(2); QCOMPARE(VirtualDesktopManager::self()->count(), 2u); VirtualDesktopManager::self()->setCurrent(1); QCOMPARE(VirtualDesktopManager::self()->current(), 1); // Create the test client. QFETCH(Test::XdgShellSurfaceType, type); XdgShellClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); // The client should appear on the second virtual desktop. QCOMPARE(client->desktop(), 2); QCOMPARE(VirtualDesktopManager::self()->current(), 2); // Any attempt to move the client to another virtual desktop should fail. workspace()->sendClientToDesktop(client, 1, true); QCOMPARE(client->desktop(), 2); QCOMPARE(VirtualDesktopManager::self()->current(), 2); // The rule should be discarded when the client is withdrawn. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); VirtualDesktopManager::self()->setCurrent(1); QCOMPARE(VirtualDesktopManager::self()->current(), 1); std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); QCOMPARE(client->desktop(), 1); QCOMPARE(VirtualDesktopManager::self()->current(), 1); // One should be able to move the client between desktops. workspace()->sendClientToDesktop(client, 2, true); QCOMPARE(client->desktop(), 2); QCOMPARE(VirtualDesktopManager::self()->current(), 1); workspace()->sendClientToDesktop(client, 1, true); QCOMPARE(client->desktop(), 1); QCOMPARE(VirtualDesktopManager::self()->current(), 1); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testMinimizeDontAffect) void TestXdgShellClientRules::testMinimizeDontAffect() { // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("minimize", true); group.writeEntry("minimizerule", int(Rules::DontAffect)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // Create the test client. QFETCH(Test::XdgShellSurfaceType, type); XdgShellClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); QVERIFY(client->isMinimizable()); // The client should not be minimized. QVERIFY(!client->isMinimized()); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testMinimizeApply) void TestXdgShellClientRules::testMinimizeApply() { // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("minimize", true); group.writeEntry("minimizerule", int(Rules::Apply)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // Create the test client. QFETCH(Test::XdgShellSurfaceType, type); XdgShellClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); QVERIFY(client->isMinimizable()); // The client should be minimized. QVERIFY(client->isMinimized()); // We should still be able to unminimize the client. client->unminimize(); QVERIFY(!client->isMinimized()); // If we re-open the client, it should be minimized back again. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); QVERIFY(client->isMinimizable()); QVERIFY(client->isMinimized()); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testMinimizeRemember) void TestXdgShellClientRules::testMinimizeRemember() { // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("minimize", false); group.writeEntry("minimizerule", int(Rules::Remember)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // Create the test client. QFETCH(Test::XdgShellSurfaceType, type); XdgShellClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); QVERIFY(client->isMinimizable()); QVERIFY(!client->isMinimized()); // Minimize the client. client->minimize(); QVERIFY(client->isMinimized()); // If we open the client again, it should be minimized. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); QVERIFY(client->isMinimizable()); QVERIFY(client->isMinimized()); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testMinimizeForce) void TestXdgShellClientRules::testMinimizeForce() { // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("minimize", false); group.writeEntry("minimizerule", int(Rules::Force)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // Create the test client. QFETCH(Test::XdgShellSurfaceType, type); XdgShellClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); QVERIFY(!client->isMinimizable()); QVERIFY(!client->isMinimized()); // Any attempt to minimize the client should fail. client->minimize(); QVERIFY(!client->isMinimized()); // If we re-open the client, the minimized state should still be forced. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); QVERIFY(!client->isMinimizable()); QVERIFY(!client->isMinimized()); client->minimize(); QVERIFY(!client->isMinimized()); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testMinimizeApplyNow) void TestXdgShellClientRules::testMinimizeApplyNow() { // Create the test client. QFETCH(Test::XdgShellSurfaceType, type); XdgShellClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); QVERIFY(client->isMinimizable()); QVERIFY(!client->isMinimized()); // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("minimize", true); group.writeEntry("minimizerule", int(Rules::ApplyNow)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // The client should be minimized now. QVERIFY(client->isMinimizable()); QVERIFY(client->isMinimized()); // One is still able to unminimize the client. client->unminimize(); QVERIFY(!client->isMinimized()); // The rule should not be applied again. client->evaluateWindowRules(); QVERIFY(client->isMinimizable()); QVERIFY(!client->isMinimized()); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testMinimizeForceTemporarily) void TestXdgShellClientRules::testMinimizeForceTemporarily() { // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("minimize", false); group.writeEntry("minimizerule", int(Rules::ForceTemporarily)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // Create the test client. QFETCH(Test::XdgShellSurfaceType, type); XdgShellClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); QVERIFY(!client->isMinimizable()); QVERIFY(!client->isMinimized()); // Any attempt to minimize the client should fail until the client is closed. client->minimize(); QVERIFY(!client->isMinimized()); // The rule should be discarded when the client is closed. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); QVERIFY(client->isMinimizable()); QVERIFY(!client->isMinimized()); client->minimize(); QVERIFY(client->isMinimized()); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testSkipTaskbarDontAffect) void TestXdgShellClientRules::testSkipTaskbarDontAffect() { // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("skiptaskbar", true); group.writeEntry("skiptaskbarrule", int(Rules::DontAffect)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // Create the test client. QFETCH(Test::XdgShellSurfaceType, type); XdgShellClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); // The client should not be affected by the rule. QVERIFY(!client->skipTaskbar()); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testSkipTaskbarApply) void TestXdgShellClientRules::testSkipTaskbarApply() { // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("skiptaskbar", true); group.writeEntry("skiptaskbarrule", int(Rules::Apply)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // Create the test client. QFETCH(Test::XdgShellSurfaceType, type); XdgShellClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); // The client should not be included on a taskbar. QVERIFY(client->skipTaskbar()); // Though one can change that. client->setOriginalSkipTaskbar(false); QVERIFY(!client->skipTaskbar()); // Reopen the client, the rule should be applied again. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); QVERIFY(client->skipTaskbar()); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testSkipTaskbarRemember) void TestXdgShellClientRules::testSkipTaskbarRemember() { // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("skiptaskbar", true); group.writeEntry("skiptaskbarrule", int(Rules::Remember)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // Create the test client. QFETCH(Test::XdgShellSurfaceType, type); XdgShellClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); // The client should not be included on a taskbar. QVERIFY(client->skipTaskbar()); // Change the skip-taskbar state. client->setOriginalSkipTaskbar(false); QVERIFY(!client->skipTaskbar()); // Reopen the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); // The client should be included on a taskbar. QVERIFY(!client->skipTaskbar()); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testSkipTaskbarForce) void TestXdgShellClientRules::testSkipTaskbarForce() { // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("skiptaskbar", true); group.writeEntry("skiptaskbarrule", int(Rules::Force)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // Create the test client. QFETCH(Test::XdgShellSurfaceType, type); XdgShellClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); // The client should not be included on a taskbar. QVERIFY(client->skipTaskbar()); // Any attempt to change the skip-taskbar state should not succeed. client->setOriginalSkipTaskbar(false); QVERIFY(client->skipTaskbar()); // Reopen the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); // The skip-taskbar state should be still forced. QVERIFY(client->skipTaskbar()); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testSkipTaskbarApplyNow) void TestXdgShellClientRules::testSkipTaskbarApplyNow() { // Create the test client. QFETCH(Test::XdgShellSurfaceType, type); XdgShellClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); QVERIFY(!client->skipTaskbar()); // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("skiptaskbar", true); group.writeEntry("skiptaskbarrule", int(Rules::ApplyNow)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // The client should not be on a taskbar now. QVERIFY(client->skipTaskbar()); // Also, one change the skip-taskbar state. client->setOriginalSkipTaskbar(false); QVERIFY(!client->skipTaskbar()); // The rule should not be applied again. client->evaluateWindowRules(); QVERIFY(!client->skipTaskbar()); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testSkipTaskbarForceTemporarily) void TestXdgShellClientRules::testSkipTaskbarForceTemporarily() { // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("skiptaskbar", true); group.writeEntry("skiptaskbarrule", int(Rules::ForceTemporarily)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // Create the test client. QFETCH(Test::XdgShellSurfaceType, type); XdgShellClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); // The client should not be included on a taskbar. QVERIFY(client->skipTaskbar()); // Any attempt to change the skip-taskbar state should not succeed. client->setOriginalSkipTaskbar(false); QVERIFY(client->skipTaskbar()); // The rule should be discarded when the client is closed. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); QVERIFY(!client->skipTaskbar()); // The skip-taskbar state is no longer forced. client->setOriginalSkipTaskbar(true); QVERIFY(client->skipTaskbar()); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testSkipPagerDontAffect) void TestXdgShellClientRules::testSkipPagerDontAffect() { // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("skippager", true); group.writeEntry("skippagerrule", int(Rules::DontAffect)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // Create the test client. QFETCH(Test::XdgShellSurfaceType, type); XdgShellClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); // The client should not be affected by the rule. QVERIFY(!client->skipPager()); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testSkipPagerApply) void TestXdgShellClientRules::testSkipPagerApply() { // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("skippager", true); group.writeEntry("skippagerrule", int(Rules::Apply)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // Create the test client. QFETCH(Test::XdgShellSurfaceType, type); XdgShellClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); // The client should not be included on a pager. QVERIFY(client->skipPager()); // Though one can change that. client->setSkipPager(false); QVERIFY(!client->skipPager()); // Reopen the client, the rule should be applied again. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); QVERIFY(client->skipPager()); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testSkipPagerRemember) void TestXdgShellClientRules::testSkipPagerRemember() { // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("skippager", true); group.writeEntry("skippagerrule", int(Rules::Remember)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // Create the test client. QFETCH(Test::XdgShellSurfaceType, type); XdgShellClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); // The client should not be included on a pager. QVERIFY(client->skipPager()); // Change the skip-pager state. client->setSkipPager(false); QVERIFY(!client->skipPager()); // Reopen the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); // The client should be included on a pager. QVERIFY(!client->skipPager()); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testSkipPagerForce) void TestXdgShellClientRules::testSkipPagerForce() { // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("skippager", true); group.writeEntry("skippagerrule", int(Rules::Force)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // Create the test client. QFETCH(Test::XdgShellSurfaceType, type); XdgShellClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); // The client should not be included on a pager. QVERIFY(client->skipPager()); // Any attempt to change the skip-pager state should not succeed. client->setSkipPager(false); QVERIFY(client->skipPager()); // Reopen the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); // The skip-pager state should be still forced. QVERIFY(client->skipPager()); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testSkipPagerApplyNow) void TestXdgShellClientRules::testSkipPagerApplyNow() { // Create the test client. QFETCH(Test::XdgShellSurfaceType, type); XdgShellClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); QVERIFY(!client->skipPager()); // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("skippager", true); group.writeEntry("skippagerrule", int(Rules::ApplyNow)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // The client should not be on a pager now. QVERIFY(client->skipPager()); // Also, one change the skip-pager state. client->setSkipPager(false); QVERIFY(!client->skipPager()); // The rule should not be applied again. client->evaluateWindowRules(); QVERIFY(!client->skipPager()); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testSkipPagerForceTemporarily) void TestXdgShellClientRules::testSkipPagerForceTemporarily() { // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("skippager", true); group.writeEntry("skippagerrule", int(Rules::ForceTemporarily)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // Create the test client. QFETCH(Test::XdgShellSurfaceType, type); XdgShellClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); // The client should not be included on a pager. QVERIFY(client->skipPager()); // Any attempt to change the skip-pager state should not succeed. client->setSkipPager(false); QVERIFY(client->skipPager()); // The rule should be discarded when the client is closed. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); QVERIFY(!client->skipPager()); // The skip-pager state is no longer forced. client->setSkipPager(true); QVERIFY(client->skipPager()); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testSkipSwitcherDontAffect) void TestXdgShellClientRules::testSkipSwitcherDontAffect() { // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("skipswitcher", true); group.writeEntry("skipswitcherrule", int(Rules::DontAffect)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // Create the test client. QFETCH(Test::XdgShellSurfaceType, type); XdgShellClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); // The client should not be affected by the rule. QVERIFY(!client->skipSwitcher()); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testSkipSwitcherApply) void TestXdgShellClientRules::testSkipSwitcherApply() { // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("skipswitcher", true); group.writeEntry("skipswitcherrule", int(Rules::Apply)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // Create the test client. QFETCH(Test::XdgShellSurfaceType, type); XdgShellClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); // The client should be excluded from window switching effects. QVERIFY(client->skipSwitcher()); // Though one can change that. client->setSkipSwitcher(false); QVERIFY(!client->skipSwitcher()); // Reopen the client, the rule should be applied again. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); QVERIFY(client->skipSwitcher()); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testSkipSwitcherRemember) void TestXdgShellClientRules::testSkipSwitcherRemember() { // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("skipswitcher", true); group.writeEntry("skipswitcherrule", int(Rules::Remember)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // Create the test client. QFETCH(Test::XdgShellSurfaceType, type); XdgShellClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); // The client should be excluded from window switching effects. QVERIFY(client->skipSwitcher()); // Change the skip-switcher state. client->setSkipSwitcher(false); QVERIFY(!client->skipSwitcher()); // Reopen the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); // The client should be included in window switching effects. QVERIFY(!client->skipSwitcher()); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testSkipSwitcherForce) void TestXdgShellClientRules::testSkipSwitcherForce() { // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("skipswitcher", true); group.writeEntry("skipswitcherrule", int(Rules::Force)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // Create the test client. QFETCH(Test::XdgShellSurfaceType, type); XdgShellClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); // The client should be excluded from window switching effects. QVERIFY(client->skipSwitcher()); // Any attempt to change the skip-switcher state should not succeed. client->setSkipSwitcher(false); QVERIFY(client->skipSwitcher()); // Reopen the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); // The skip-switcher state should be still forced. QVERIFY(client->skipSwitcher()); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testSkipSwitcherApplyNow) void TestXdgShellClientRules::testSkipSwitcherApplyNow() { // Create the test client. QFETCH(Test::XdgShellSurfaceType, type); XdgShellClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); QVERIFY(!client->skipSwitcher()); // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("skipswitcher", true); group.writeEntry("skipswitcherrule", int(Rules::ApplyNow)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // The client should be excluded from window switching effects now. QVERIFY(client->skipSwitcher()); // Also, one change the skip-switcher state. client->setSkipSwitcher(false); QVERIFY(!client->skipSwitcher()); // The rule should not be applied again. client->evaluateWindowRules(); QVERIFY(!client->skipSwitcher()); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testSkipSwitcherForceTemporarily) void TestXdgShellClientRules::testSkipSwitcherForceTemporarily() { // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("skipswitcher", true); group.writeEntry("skipswitcherrule", int(Rules::ForceTemporarily)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // Create the test client. QFETCH(Test::XdgShellSurfaceType, type); XdgShellClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); // The client should be excluded from window switching effects. QVERIFY(client->skipSwitcher()); // Any attempt to change the skip-switcher state should not succeed. client->setSkipSwitcher(false); QVERIFY(client->skipSwitcher()); // The rule should be discarded when the client is closed. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); QVERIFY(!client->skipSwitcher()); // The skip-switcher state is no longer forced. client->setSkipSwitcher(true); QVERIFY(client->skipSwitcher()); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testKeepAboveDontAffect) void TestXdgShellClientRules::testKeepAboveDontAffect() { // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("above", true); group.writeEntry("aboverule", int(Rules::DontAffect)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // Create the test client. QFETCH(Test::XdgShellSurfaceType, type); XdgShellClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); // The keep-above state of the client should not be affected by the rule. QVERIFY(!client->keepAbove()); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testKeepAboveApply) void TestXdgShellClientRules::testKeepAboveApply() { // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("above", true); group.writeEntry("aboverule", int(Rules::Apply)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // Create the test client. QFETCH(Test::XdgShellSurfaceType, type); XdgShellClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); // Initially, the client should be kept above. QVERIFY(client->keepAbove()); // One should also be able to alter the keep-above state. client->setKeepAbove(false); QVERIFY(!client->keepAbove()); // If one re-opens the client, it should be kept above back again. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); QVERIFY(client->keepAbove()); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testKeepAboveRemember) void TestXdgShellClientRules::testKeepAboveRemember() { // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("above", true); group.writeEntry("aboverule", int(Rules::Remember)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // Create the test client. QFETCH(Test::XdgShellSurfaceType, type); XdgShellClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); // Initially, the client should be kept above. QVERIFY(client->keepAbove()); // Unset the keep-above state. client->setKeepAbove(false); QVERIFY(!client->keepAbove()); delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); // Re-open the client, it should not be kept above. std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); QVERIFY(!client->keepAbove()); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testKeepAboveForce) void TestXdgShellClientRules::testKeepAboveForce() { // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("above", true); group.writeEntry("aboverule", int(Rules::Force)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // Create the test client. QFETCH(Test::XdgShellSurfaceType, type); XdgShellClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); // Initially, the client should be kept above. QVERIFY(client->keepAbove()); // Any attemt to unset the keep-above should not succeed. client->setKeepAbove(false); QVERIFY(client->keepAbove()); // If we re-open the client, it should still be kept above. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); QVERIFY(client->keepAbove()); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testKeepAboveApplyNow) void TestXdgShellClientRules::testKeepAboveApplyNow() { // Create the test client. QFETCH(Test::XdgShellSurfaceType, type); XdgShellClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); QVERIFY(!client->keepAbove()); // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("above", true); group.writeEntry("aboverule", int(Rules::ApplyNow)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // The client should now be kept above other clients. QVERIFY(client->keepAbove()); // One is still able to change the keep-above state of the client. client->setKeepAbove(false); QVERIFY(!client->keepAbove()); // The rule should not be applied again. client->evaluateWindowRules(); QVERIFY(!client->keepAbove()); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testKeepAboveForceTemporarily) void TestXdgShellClientRules::testKeepAboveForceTemporarily() { // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("above", true); group.writeEntry("aboverule", int(Rules::ForceTemporarily)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // Create the test client. QFETCH(Test::XdgShellSurfaceType, type); XdgShellClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); // Initially, the client should be kept above. QVERIFY(client->keepAbove()); // Any attempt to alter the keep-above state should not succeed. client->setKeepAbove(false); QVERIFY(client->keepAbove()); // The rule should be discarded when the client is closed. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); QVERIFY(!client->keepAbove()); // The keep-above state is no longer forced. client->setKeepAbove(true); QVERIFY(client->keepAbove()); client->setKeepAbove(false); QVERIFY(!client->keepAbove()); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testKeepBelowDontAffect) void TestXdgShellClientRules::testKeepBelowDontAffect() { // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("below", true); group.writeEntry("belowrule", int(Rules::DontAffect)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // Create the test client. QFETCH(Test::XdgShellSurfaceType, type); XdgShellClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); // The keep-below state of the client should not be affected by the rule. QVERIFY(!client->keepBelow()); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testKeepBelowApply) void TestXdgShellClientRules::testKeepBelowApply() { // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("below", true); group.writeEntry("belowrule", int(Rules::Apply)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // Create the test client. QFETCH(Test::XdgShellSurfaceType, type); XdgShellClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); // Initially, the client should be kept below. QVERIFY(client->keepBelow()); // One should also be able to alter the keep-below state. client->setKeepBelow(false); QVERIFY(!client->keepBelow()); // If one re-opens the client, it should be kept above back again. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); QVERIFY(client->keepBelow()); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testKeepBelowRemember) void TestXdgShellClientRules::testKeepBelowRemember() { // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("below", true); group.writeEntry("belowrule", int(Rules::Remember)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // Create the test client. QFETCH(Test::XdgShellSurfaceType, type); XdgShellClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); // Initially, the client should be kept below. QVERIFY(client->keepBelow()); // Unset the keep-below state. client->setKeepBelow(false); QVERIFY(!client->keepBelow()); delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); // Re-open the client, it should not be kept below. std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); QVERIFY(!client->keepBelow()); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testKeepBelowForce) void TestXdgShellClientRules::testKeepBelowForce() { // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("below", true); group.writeEntry("belowrule", int(Rules::Force)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // Create the test client. QFETCH(Test::XdgShellSurfaceType, type); XdgShellClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); // Initially, the client should be kept below. QVERIFY(client->keepBelow()); // Any attemt to unset the keep-below should not succeed. client->setKeepBelow(false); QVERIFY(client->keepBelow()); // If we re-open the client, it should still be kept below. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); QVERIFY(client->keepBelow()); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testKeepBelowApplyNow) void TestXdgShellClientRules::testKeepBelowApplyNow() { // Create the test client. QFETCH(Test::XdgShellSurfaceType, type); XdgShellClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); QVERIFY(!client->keepBelow()); // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("below", true); group.writeEntry("belowrule", int(Rules::ApplyNow)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // The client should now be kept below other clients. QVERIFY(client->keepBelow()); // One is still able to change the keep-below state of the client. client->setKeepBelow(false); QVERIFY(!client->keepBelow()); // The rule should not be applied again. client->evaluateWindowRules(); QVERIFY(!client->keepBelow()); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testKeepBelowForceTemporarily) void TestXdgShellClientRules::testKeepBelowForceTemporarily() { // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("below", true); group.writeEntry("belowrule", int(Rules::ForceTemporarily)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // Create the test client. QFETCH(Test::XdgShellSurfaceType, type); XdgShellClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); // Initially, the client should be kept below. QVERIFY(client->keepBelow()); // Any attempt to alter the keep-below state should not succeed. client->setKeepBelow(false); QVERIFY(client->keepBelow()); // The rule should be discarded when the client is closed. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); QVERIFY(!client->keepBelow()); // The keep-below state is no longer forced. client->setKeepBelow(true); QVERIFY(client->keepBelow()); client->setKeepBelow(false); QVERIFY(!client->keepBelow()); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testShortcutDontAffect) void TestXdgShellClientRules::testShortcutDontAffect() { // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("shortcut", "Ctrl+Alt+1"); group.writeEntry("shortcutrule", int(Rules::DontAffect)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // Create the test client. QFETCH(Test::XdgShellSurfaceType, type); XdgShellClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); QCOMPARE(client->shortcut(), QKeySequence()); client->minimize(); QVERIFY(client->isMinimized()); // If we press the window shortcut, nothing should happen. QSignalSpy clientUnminimizedSpy(client, &AbstractClient::clientUnminimized); QVERIFY(clientUnminimizedSpy.isValid()); quint32 timestamp = 1; kwinApp()->platform()->keyboardKeyPressed(KEY_LEFTCTRL, timestamp++); kwinApp()->platform()->keyboardKeyPressed(KEY_LEFTALT, timestamp++); kwinApp()->platform()->keyboardKeyPressed(KEY_1, timestamp++); kwinApp()->platform()->keyboardKeyReleased(KEY_1, timestamp++); kwinApp()->platform()->keyboardKeyReleased(KEY_LEFTALT, timestamp++); kwinApp()->platform()->keyboardKeyReleased(KEY_LEFTCTRL, timestamp++); QVERIFY(!clientUnminimizedSpy.wait(100)); QVERIFY(client->isMinimized()); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testShortcutApply) void TestXdgShellClientRules::testShortcutApply() { // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("shortcut", "Ctrl+Alt+1"); group.writeEntry("shortcutrule", int(Rules::Apply)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // Create the test client. QFETCH(Test::XdgShellSurfaceType, type); XdgShellClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); // If we press the window shortcut, the window should be brought back to user. QSignalSpy clientUnminimizedSpy(client, &AbstractClient::clientUnminimized); QVERIFY(clientUnminimizedSpy.isValid()); quint32 timestamp = 1; QCOMPARE(client->shortcut(), (QKeySequence{Qt::CTRL + Qt::ALT + Qt::Key_1})); client->minimize(); QVERIFY(client->isMinimized()); kwinApp()->platform()->keyboardKeyPressed(KEY_LEFTCTRL, timestamp++); kwinApp()->platform()->keyboardKeyPressed(KEY_LEFTALT, timestamp++); kwinApp()->platform()->keyboardKeyPressed(KEY_1, timestamp++); kwinApp()->platform()->keyboardKeyReleased(KEY_1, timestamp++); kwinApp()->platform()->keyboardKeyReleased(KEY_LEFTALT, timestamp++); kwinApp()->platform()->keyboardKeyReleased(KEY_LEFTCTRL, timestamp++); QVERIFY(clientUnminimizedSpy.wait()); QVERIFY(!client->isMinimized()); // One can also change the shortcut. client->setShortcut(QStringLiteral("Ctrl+Alt+2")); QCOMPARE(client->shortcut(), (QKeySequence{Qt::CTRL + Qt::ALT + Qt::Key_2})); client->minimize(); QVERIFY(client->isMinimized()); kwinApp()->platform()->keyboardKeyPressed(KEY_LEFTCTRL, timestamp++); kwinApp()->platform()->keyboardKeyPressed(KEY_LEFTALT, timestamp++); kwinApp()->platform()->keyboardKeyPressed(KEY_2, timestamp++); kwinApp()->platform()->keyboardKeyReleased(KEY_2, timestamp++); kwinApp()->platform()->keyboardKeyReleased(KEY_LEFTALT, timestamp++); kwinApp()->platform()->keyboardKeyReleased(KEY_LEFTCTRL, timestamp++); QVERIFY(clientUnminimizedSpy.wait()); QVERIFY(!client->isMinimized()); // The old shortcut should do nothing. client->minimize(); QVERIFY(client->isMinimized()); kwinApp()->platform()->keyboardKeyPressed(KEY_LEFTCTRL, timestamp++); kwinApp()->platform()->keyboardKeyPressed(KEY_LEFTALT, timestamp++); kwinApp()->platform()->keyboardKeyPressed(KEY_1, timestamp++); kwinApp()->platform()->keyboardKeyReleased(KEY_1, timestamp++); kwinApp()->platform()->keyboardKeyReleased(KEY_LEFTALT, timestamp++); kwinApp()->platform()->keyboardKeyReleased(KEY_LEFTCTRL, timestamp++); QVERIFY(!clientUnminimizedSpy.wait(100)); QVERIFY(client->isMinimized()); // Reopen the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); // The window shortcut should be set back to Ctrl+Alt+1. QCOMPARE(client->shortcut(), (QKeySequence{Qt::CTRL + Qt::ALT + Qt::Key_1})); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testShortcutRemember) void TestXdgShellClientRules::testShortcutRemember() { QSKIP("KWin core doesn't try to save the last used window shortcut"); // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("shortcut", "Ctrl+Alt+1"); group.writeEntry("shortcutrule", int(Rules::Remember)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // Create the test client. QFETCH(Test::XdgShellSurfaceType, type); XdgShellClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); // If we press the window shortcut, the window should be brought back to user. QSignalSpy clientUnminimizedSpy(client, &AbstractClient::clientUnminimized); QVERIFY(clientUnminimizedSpy.isValid()); quint32 timestamp = 1; QCOMPARE(client->shortcut(), (QKeySequence{Qt::CTRL + Qt::ALT + Qt::Key_1})); client->minimize(); QVERIFY(client->isMinimized()); kwinApp()->platform()->keyboardKeyPressed(KEY_LEFTCTRL, timestamp++); kwinApp()->platform()->keyboardKeyPressed(KEY_LEFTALT, timestamp++); kwinApp()->platform()->keyboardKeyPressed(KEY_1, timestamp++); kwinApp()->platform()->keyboardKeyReleased(KEY_1, timestamp++); kwinApp()->platform()->keyboardKeyReleased(KEY_LEFTALT, timestamp++); kwinApp()->platform()->keyboardKeyReleased(KEY_LEFTCTRL, timestamp++); QVERIFY(clientUnminimizedSpy.wait()); QVERIFY(!client->isMinimized()); // Change the window shortcut to Ctrl+Alt+2. client->setShortcut(QStringLiteral("Ctrl+Alt+2")); QCOMPARE(client->shortcut(), (QKeySequence{Qt::CTRL + Qt::ALT + Qt::Key_2})); client->minimize(); QVERIFY(client->isMinimized()); kwinApp()->platform()->keyboardKeyPressed(KEY_LEFTCTRL, timestamp++); kwinApp()->platform()->keyboardKeyPressed(KEY_LEFTALT, timestamp++); kwinApp()->platform()->keyboardKeyPressed(KEY_2, timestamp++); kwinApp()->platform()->keyboardKeyReleased(KEY_2, timestamp++); kwinApp()->platform()->keyboardKeyReleased(KEY_LEFTALT, timestamp++); kwinApp()->platform()->keyboardKeyReleased(KEY_LEFTCTRL, timestamp++); QVERIFY(clientUnminimizedSpy.wait()); QVERIFY(!client->isMinimized()); // Reopen the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); // The window shortcut should be set to the last known value. QCOMPARE(client->shortcut(), (QKeySequence{Qt::CTRL + Qt::ALT + Qt::Key_2})); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testShortcutForce) void TestXdgShellClientRules::testShortcutForce() { QSKIP("KWin core can't release forced window shortcuts"); // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("shortcut", "Ctrl+Alt+1"); group.writeEntry("shortcutrule", int(Rules::Force)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // Create the test client. QFETCH(Test::XdgShellSurfaceType, type); XdgShellClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); // If we press the window shortcut, the window should be brought back to user. QSignalSpy clientUnminimizedSpy(client, &AbstractClient::clientUnminimized); QVERIFY(clientUnminimizedSpy.isValid()); quint32 timestamp = 1; QCOMPARE(client->shortcut(), (QKeySequence{Qt::CTRL + Qt::ALT + Qt::Key_1})); client->minimize(); QVERIFY(client->isMinimized()); kwinApp()->platform()->keyboardKeyPressed(KEY_LEFTCTRL, timestamp++); kwinApp()->platform()->keyboardKeyPressed(KEY_LEFTALT, timestamp++); kwinApp()->platform()->keyboardKeyPressed(KEY_1, timestamp++); kwinApp()->platform()->keyboardKeyReleased(KEY_1, timestamp++); kwinApp()->platform()->keyboardKeyReleased(KEY_LEFTALT, timestamp++); kwinApp()->platform()->keyboardKeyReleased(KEY_LEFTCTRL, timestamp++); QVERIFY(clientUnminimizedSpy.wait()); QVERIFY(!client->isMinimized()); // Any attempt to change the window shortcut should not succeed. client->setShortcut(QStringLiteral("Ctrl+Alt+2")); QCOMPARE(client->shortcut(), (QKeySequence{Qt::CTRL + Qt::ALT + Qt::Key_1})); client->minimize(); QVERIFY(client->isMinimized()); kwinApp()->platform()->keyboardKeyPressed(KEY_LEFTCTRL, timestamp++); kwinApp()->platform()->keyboardKeyPressed(KEY_LEFTALT, timestamp++); kwinApp()->platform()->keyboardKeyPressed(KEY_2, timestamp++); kwinApp()->platform()->keyboardKeyReleased(KEY_2, timestamp++); kwinApp()->platform()->keyboardKeyReleased(KEY_LEFTALT, timestamp++); kwinApp()->platform()->keyboardKeyReleased(KEY_LEFTCTRL, timestamp++); QVERIFY(!clientUnminimizedSpy.wait(100)); QVERIFY(client->isMinimized()); // Reopen the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); // The window shortcut should still be forced. QCOMPARE(client->shortcut(), (QKeySequence{Qt::CTRL + Qt::ALT + Qt::Key_1})); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testShortcutApplyNow) void TestXdgShellClientRules::testShortcutApplyNow() { // Create the test client. QFETCH(Test::XdgShellSurfaceType, type); XdgShellClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); QVERIFY(client->shortcut().isEmpty()); // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("shortcut", "Ctrl+Alt+1"); group.writeEntry("shortcutrule", int(Rules::ApplyNow)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // The client should now have a window shortcut assigned. QCOMPARE(client->shortcut(), (QKeySequence{Qt::CTRL + Qt::ALT + Qt::Key_1})); QSignalSpy clientUnminimizedSpy(client, &AbstractClient::clientUnminimized); QVERIFY(clientUnminimizedSpy.isValid()); quint32 timestamp = 1; client->minimize(); QVERIFY(client->isMinimized()); kwinApp()->platform()->keyboardKeyPressed(KEY_LEFTCTRL, timestamp++); kwinApp()->platform()->keyboardKeyPressed(KEY_LEFTALT, timestamp++); kwinApp()->platform()->keyboardKeyPressed(KEY_1, timestamp++); kwinApp()->platform()->keyboardKeyReleased(KEY_1, timestamp++); kwinApp()->platform()->keyboardKeyReleased(KEY_LEFTALT, timestamp++); kwinApp()->platform()->keyboardKeyReleased(KEY_LEFTCTRL, timestamp++); QVERIFY(clientUnminimizedSpy.wait()); QVERIFY(!client->isMinimized()); // Assign a different shortcut. client->setShortcut(QStringLiteral("Ctrl+Alt+2")); QCOMPARE(client->shortcut(), (QKeySequence{Qt::CTRL + Qt::ALT + Qt::Key_2})); client->minimize(); QVERIFY(client->isMinimized()); kwinApp()->platform()->keyboardKeyPressed(KEY_LEFTCTRL, timestamp++); kwinApp()->platform()->keyboardKeyPressed(KEY_LEFTALT, timestamp++); kwinApp()->platform()->keyboardKeyPressed(KEY_2, timestamp++); kwinApp()->platform()->keyboardKeyReleased(KEY_2, timestamp++); kwinApp()->platform()->keyboardKeyReleased(KEY_LEFTALT, timestamp++); kwinApp()->platform()->keyboardKeyReleased(KEY_LEFTCTRL, timestamp++); QVERIFY(clientUnminimizedSpy.wait()); QVERIFY(!client->isMinimized()); // The rule should not be applied again. client->evaluateWindowRules(); QCOMPARE(client->shortcut(), (QKeySequence{Qt::CTRL + Qt::ALT + Qt::Key_2})); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testShortcutForceTemporarily) void TestXdgShellClientRules::testShortcutForceTemporarily() { QSKIP("KWin core can't release forced window shortcuts"); // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("shortcut", "Ctrl+Alt+1"); group.writeEntry("shortcutrule", int(Rules::ForceTemporarily)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // Create the test client. QFETCH(Test::XdgShellSurfaceType, type); XdgShellClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); // If we press the window shortcut, the window should be brought back to user. QSignalSpy clientUnminimizedSpy(client, &AbstractClient::clientUnminimized); QVERIFY(clientUnminimizedSpy.isValid()); quint32 timestamp = 1; QCOMPARE(client->shortcut(), (QKeySequence{Qt::CTRL + Qt::ALT + Qt::Key_1})); client->minimize(); QVERIFY(client->isMinimized()); kwinApp()->platform()->keyboardKeyPressed(KEY_LEFTCTRL, timestamp++); kwinApp()->platform()->keyboardKeyPressed(KEY_LEFTALT, timestamp++); kwinApp()->platform()->keyboardKeyPressed(KEY_1, timestamp++); kwinApp()->platform()->keyboardKeyReleased(KEY_1, timestamp++); kwinApp()->platform()->keyboardKeyReleased(KEY_LEFTALT, timestamp++); kwinApp()->platform()->keyboardKeyReleased(KEY_LEFTCTRL, timestamp++); QVERIFY(clientUnminimizedSpy.wait()); QVERIFY(!client->isMinimized()); // Any attempt to change the window shortcut should not succeed. client->setShortcut(QStringLiteral("Ctrl+Alt+2")); QCOMPARE(client->shortcut(), (QKeySequence{Qt::CTRL + Qt::ALT + Qt::Key_1})); client->minimize(); QVERIFY(client->isMinimized()); kwinApp()->platform()->keyboardKeyPressed(KEY_LEFTCTRL, timestamp++); kwinApp()->platform()->keyboardKeyPressed(KEY_LEFTALT, timestamp++); kwinApp()->platform()->keyboardKeyPressed(KEY_2, timestamp++); kwinApp()->platform()->keyboardKeyReleased(KEY_2, timestamp++); kwinApp()->platform()->keyboardKeyReleased(KEY_LEFTALT, timestamp++); kwinApp()->platform()->keyboardKeyReleased(KEY_LEFTCTRL, timestamp++); QVERIFY(!clientUnminimizedSpy.wait(100)); QVERIFY(client->isMinimized()); // The rule should be discarded when the client is closed. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); QVERIFY(client->shortcut().isEmpty()); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testDesktopFileDontAffect) void TestXdgShellClientRules::testDesktopFileDontAffect() { // Currently, the desktop file name is derived from the app id. If the app id is // changed, then the old rules will be lost. Either setDesktopFileName should // be exposed or the desktop file name rule should be removed for wayland clients. QSKIP("Needs changes in KWin core to pass"); } TEST_DATA(testDesktopFileApply) void TestXdgShellClientRules::testDesktopFileApply() { // Currently, the desktop file name is derived from the app id. If the app id is // changed, then the old rules will be lost. Either setDesktopFileName should // be exposed or the desktop file name rule should be removed for wayland clients. QSKIP("Needs changes in KWin core to pass"); } TEST_DATA(testDesktopFileRemember) void TestXdgShellClientRules::testDesktopFileRemember() { // Currently, the desktop file name is derived from the app id. If the app id is // changed, then the old rules will be lost. Either setDesktopFileName should // be exposed or the desktop file name rule should be removed for wayland clients. QSKIP("Needs changes in KWin core to pass"); } TEST_DATA(testDesktopFileForce) void TestXdgShellClientRules::testDesktopFileForce() { // Currently, the desktop file name is derived from the app id. If the app id is // changed, then the old rules will be lost. Either setDesktopFileName should // be exposed or the desktop file name rule should be removed for wayland clients. QSKIP("Needs changes in KWin core to pass"); } TEST_DATA(testDesktopFileApplyNow) void TestXdgShellClientRules::testDesktopFileApplyNow() { // Currently, the desktop file name is derived from the app id. If the app id is // changed, then the old rules will be lost. Either setDesktopFileName should // be exposed or the desktop file name rule should be removed for wayland clients. QSKIP("Needs changes in KWin core to pass"); } TEST_DATA(testDesktopFileForceTemporarily) void TestXdgShellClientRules::testDesktopFileForceTemporarily() { // Currently, the desktop file name is derived from the app id. If the app id is // changed, then the old rules will be lost. Either setDesktopFileName should // be exposed or the desktop file name rule should be removed for wayland clients. QSKIP("Needs changes in KWin core to pass"); } TEST_DATA(testActiveOpacityDontAffect) void TestXdgShellClientRules::testActiveOpacityDontAffect() { // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("opacityactive", 90); group.writeEntry("opacityactiverule", int(Rules::DontAffect)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // Create the test client. QFETCH(Test::XdgShellSurfaceType, type); XdgShellClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); QVERIFY(client->isActive()); // The opacity should not be affected by the rule. QCOMPARE(client->opacity(), 1.0); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testActiveOpacityForce) void TestXdgShellClientRules::testActiveOpacityForce() { // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("opacityactive", 90); group.writeEntry("opacityactiverule", int(Rules::Force)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // Create the test client. QFETCH(Test::XdgShellSurfaceType, type); XdgShellClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); QVERIFY(client->isActive()); QCOMPARE(client->opacity(), 0.9); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testActiveOpacityForceTemporarily) void TestXdgShellClientRules::testActiveOpacityForceTemporarily() { // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("opacityactive", 90); group.writeEntry("opacityactiverule", int(Rules::ForceTemporarily)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // Create the test client. QFETCH(Test::XdgShellSurfaceType, type); XdgShellClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); QVERIFY(client->isActive()); QCOMPARE(client->opacity(), 0.9); // The rule should be discarded when the client is closed. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); QVERIFY(client->isActive()); QCOMPARE(client->opacity(), 1.0); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testInactiveOpacityDontAffect) void TestXdgShellClientRules::testInactiveOpacityDontAffect() { // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("opacityinactive", 80); group.writeEntry("opacityinactiverule", int(Rules::DontAffect)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // Create the test client. QFETCH(Test::XdgShellSurfaceType, type); XdgShellClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); QVERIFY(client->isActive()); // Make the client inactive. workspace()->setActiveClient(nullptr); QVERIFY(!client->isActive()); // The opacity of the client should not be affected by the rule. QCOMPARE(client->opacity(), 1.0); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testInactiveOpacityForce) void TestXdgShellClientRules::testInactiveOpacityForce() { // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("opacityinactive", 80); group.writeEntry("opacityinactiverule", int(Rules::Force)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // Create the test client. QFETCH(Test::XdgShellSurfaceType, type); XdgShellClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); QVERIFY(client->isActive()); QCOMPARE(client->opacity(), 1.0); // Make the client inactive. workspace()->setActiveClient(nullptr); QVERIFY(!client->isActive()); // The opacity should be forced by the rule. QCOMPARE(client->opacity(), 0.8); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testInactiveOpacityForceTemporarily) void TestXdgShellClientRules::testInactiveOpacityForceTemporarily() { // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("opacityinactive", 80); group.writeEntry("opacityinactiverule", int(Rules::ForceTemporarily)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // Create the test client. QFETCH(Test::XdgShellSurfaceType, type); XdgShellClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); QVERIFY(client->isActive()); QCOMPARE(client->opacity(), 1.0); // Make the client inactive. workspace()->setActiveClient(nullptr); QVERIFY(!client->isActive()); // The opacity should be forced by the rule. QCOMPARE(client->opacity(), 0.8); // The rule should be discarded when the client is closed. delete shellSurface; delete surface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); QVERIFY(client->isActive()); QCOMPARE(client->opacity(), 1.0); workspace()->setActiveClient(nullptr); QVERIFY(!client->isActive()); QCOMPARE(client->opacity(), 1.0); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } void TestXdgShellClientRules::testMatchAfterNameChange() { KSharedConfig::Ptr config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("above", true); group.writeEntry("aboverule", int(Rules::Force)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); QScopedPointer surface(Test::createSurface()); QScopedPointer shellSurface(Test::createXdgShellV6Surface(surface.data())); auto c = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); QVERIFY(c); QVERIFY(c->isActive()); QCOMPARE(c->keepAbove(), false); QSignalSpy desktopFileNameSpy(c, &AbstractClient::desktopFileNameChanged); QVERIFY(desktopFileNameSpy.isValid()); shellSurface->setAppId(QByteArrayLiteral("org.kde.foo")); QVERIFY(desktopFileNameSpy.wait()); QCOMPARE(c->keepAbove(), true); } WAYLANDTEST_MAIN(TestXdgShellClientRules) #include "xdgshellclient_rules_test.moc" diff --git a/autotests/integration/xdgshellclient_test.cpp b/autotests/integration/xdgshellclient_test.cpp index 25234b9a9..459306635 100644 --- a/autotests/integration/xdgshellclient_test.cpp +++ b/autotests/integration/xdgshellclient_test.cpp @@ -1,1642 +1,1644 @@ /******************************************************************** KWin - the KDE window manager This file is part of the KDE project. Copyright (C) 2016 Martin Gräßlin Copyright (C) 2019 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 "kwin_wayland_test.h" #include "cursor.h" #include "decorations/decorationbridge.h" #include "decorations/settings.h" #include "effects.h" #include "deleted.h" #include "platform.h" #include "xdgshellclient.h" #include "screens.h" #include "wayland_server.h" #include "workspace.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // system #include #include #include #include using namespace KWin; using namespace KWayland::Client; static const QString s_socketName = QStringLiteral("wayland_test_kwin_xdgshellclient-0"); class TestXdgShellClient : public QObject { Q_OBJECT private Q_SLOTS: void initTestCase(); void init(); void cleanup(); void testMapUnmapMap_data(); void testMapUnmapMap(); void testDesktopPresenceChanged(); void testTransientPositionAfterRemap(); void testWindowOutputs_data(); void testWindowOutputs(); void testMinimizeActiveWindow_data(); void testMinimizeActiveWindow(); void testFullscreen_data(); void testFullscreen(); void testFullscreenRestore_data(); void testFullscreenRestore(); void testUserCanSetFullscreen_data(); void testUserCanSetFullscreen(); void testUserSetFullscreen_data(); void testUserSetFullscreen(); void testMaximizedToFullscreen_data(); void testMaximizedToFullscreen(); void testWindowOpensLargerThanScreen_data(); void testWindowOpensLargerThanScreen(); void testHidden_data(); void testHidden(); void testDesktopFileName(); void testCaptionSimplified(); void testCaptionMultipleWindows(); void testUnresponsiveWindow_data(); void testUnresponsiveWindow(); void testX11WindowId_data(); void testX11WindowId(); void testAppMenu(); void testNoDecorationModeRequested_data(); void testNoDecorationModeRequested(); void testSendClientWithTransientToDesktop_data(); void testSendClientWithTransientToDesktop(); void testMinimizeWindowWithTransients_data(); void testMinimizeWindowWithTransients(); void testXdgDecoration_data(); void testXdgDecoration(); void testXdgNeverCommitted(); void testXdgInitialState(); void testXdgInitiallyMaximised(); void testXdgInitiallyFullscreen(); void testXdgInitiallyMinimized(); void testXdgWindowGeometryIsntSet(); void testXdgWindowGeometryAttachBuffer(); void testXdgWindowGeometryAttachSubSurface(); void testXdgWindowGeometryInteractiveResize(); void testXdgWindowGeometryFullScreen(); void testXdgWindowGeometryMaximize(); }; void TestXdgShellClient::initTestCase() { qRegisterMetaType(); qRegisterMetaType(); qRegisterMetaType(); qRegisterMetaType(); QSignalSpy workspaceCreatedSpy(kwinApp(), &Application::workspaceCreated); QVERIFY(workspaceCreatedSpy.isValid()); kwinApp()->platform()->setInitialWindowSize(QSize(1280, 1024)); QVERIFY(waylandServer()->init(s_socketName.toLocal8Bit())); QMetaObject::invokeMethod(kwinApp()->platform(), "setVirtualOutputs", Qt::DirectConnection, Q_ARG(int, 2)); 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 TestXdgShellClient::init() { QVERIFY(Test::setupWaylandConnection(Test::AdditionalWaylandInterface::Decoration | Test::AdditionalWaylandInterface::XdgDecoration | Test::AdditionalWaylandInterface::AppMenu)); screens()->setCurrent(0); KWin::Cursor::setPos(QPoint(1280, 512)); } void TestXdgShellClient::cleanup() { Test::destroyWaylandConnection(); } void TestXdgShellClient::testMapUnmapMap_data() { QTest::addColumn("type"); QTest::newRow("xdgShellV6") << Test::XdgShellSurfaceType::XdgShellV6; QTest::newRow("xdgWmBase") << Test::XdgShellSurfaceType::XdgShellStable; } void TestXdgShellClient::testMapUnmapMap() { // this test verifies that mapping a previously mapped window works correctly QSignalSpy clientAddedSpy(waylandServer(), &WaylandServer::shellClientAdded); QVERIFY(clientAddedSpy.isValid()); QSignalSpy effectsWindowShownSpy(effects, &EffectsHandler::windowShown); QVERIFY(effectsWindowShownSpy.isValid()); QSignalSpy effectsWindowHiddenSpy(effects, &EffectsHandler::windowHidden); QVERIFY(effectsWindowHiddenSpy.isValid()); QScopedPointer surface(Test::createSurface()); QFETCH(Test::XdgShellSurfaceType, type); QScopedPointer shellSurface(Test::createXdgShellSurface(type, surface.data())); // now let's render Test::render(surface.data(), QSize(100, 50), Qt::blue); QVERIFY(clientAddedSpy.isEmpty()); QVERIFY(clientAddedSpy.wait()); auto client = clientAddedSpy.first().first().value(); QVERIFY(client); QVERIFY(client->isShown(true)); QCOMPARE(client->isHiddenInternal(), false); QCOMPARE(client->readyForPainting(), true); QCOMPARE(client->depth(), 32); QVERIFY(client->hasAlpha()); QCOMPARE(client->icon().name(), QStringLiteral("wayland")); QCOMPARE(workspace()->activeClient(), client); QVERIFY(effectsWindowShownSpy.isEmpty()); QVERIFY(client->isMaximizable()); QVERIFY(client->isMovable()); QVERIFY(client->isMovableAcrossScreens()); QVERIFY(client->isResizable()); QVERIFY(client->property("maximizable").toBool()); QVERIFY(client->property("moveable").toBool()); QVERIFY(client->property("moveableAcrossScreens").toBool()); QVERIFY(client->property("resizeable").toBool()); QCOMPARE(client->isInternal(), false); QVERIFY(client->effectWindow()); QVERIFY(!client->effectWindow()->internalWindow()); QCOMPARE(client->internalId().isNull(), false); const auto uuid = client->internalId(); QUuid deletedUuid; QCOMPARE(deletedUuid.isNull(), true); connect(client, &XdgShellClient::windowClosed, this, [&deletedUuid] (Toplevel *, Deleted *d) { deletedUuid = d->internalId(); }); // now unmap QSignalSpy hiddenSpy(client, &XdgShellClient::windowHidden); QVERIFY(hiddenSpy.isValid()); QSignalSpy windowClosedSpy(client, &XdgShellClient::windowClosed); QVERIFY(windowClosedSpy.isValid()); surface->attachBuffer(Buffer::Ptr()); surface->commit(Surface::CommitFlag::None); QVERIFY(hiddenSpy.wait()); QCOMPARE(client->readyForPainting(), true); QCOMPARE(client->isHiddenInternal(), true); QVERIFY(windowClosedSpy.isEmpty()); QVERIFY(!workspace()->activeClient()); QCOMPARE(effectsWindowHiddenSpy.count(), 1); QCOMPARE(effectsWindowHiddenSpy.first().first().value(), client->effectWindow()); QSignalSpy windowShownSpy(client, &XdgShellClient::windowShown); QVERIFY(windowShownSpy.isValid()); Test::render(surface.data(), QSize(100, 50), Qt::blue, QImage::Format_RGB32); QCOMPARE(clientAddedSpy.count(), 1); QVERIFY(windowShownSpy.wait()); QCOMPARE(windowShownSpy.count(), 1); QCOMPARE(clientAddedSpy.count(), 1); QCOMPARE(client->readyForPainting(), true); QCOMPARE(client->isHiddenInternal(), false); QCOMPARE(client->depth(), 24); QVERIFY(!client->hasAlpha()); QCOMPARE(workspace()->activeClient(), client); QCOMPARE(effectsWindowShownSpy.count(), 1); QCOMPARE(effectsWindowShownSpy.first().first().value(), client->effectWindow()); // let's unmap again surface->attachBuffer(Buffer::Ptr()); surface->commit(Surface::CommitFlag::None); QVERIFY(hiddenSpy.wait()); QCOMPARE(hiddenSpy.count(), 2); QCOMPARE(client->readyForPainting(), true); QCOMPARE(client->isHiddenInternal(), true); QCOMPARE(client->internalId(), uuid); QVERIFY(windowClosedSpy.isEmpty()); QCOMPARE(effectsWindowHiddenSpy.count(), 2); QCOMPARE(effectsWindowHiddenSpy.last().first().value(), client->effectWindow()); shellSurface.reset(); surface.reset(); QVERIFY(windowClosedSpy.wait()); QCOMPARE(windowClosedSpy.count(), 1); QCOMPARE(effectsWindowHiddenSpy.count(), 2); QCOMPARE(deletedUuid.isNull(), false); QCOMPARE(deletedUuid, uuid); } void TestXdgShellClient::testDesktopPresenceChanged() { // this test verifies that the desktop presence changed signals are properly emitted QScopedPointer surface(Test::createSurface()); QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); auto c = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); QVERIFY(c); QCOMPARE(c->desktop(), 1); effects->setNumberOfDesktops(4); QSignalSpy desktopPresenceChangedClientSpy(c, &XdgShellClient::desktopPresenceChanged); QVERIFY(desktopPresenceChangedClientSpy.isValid()); QSignalSpy desktopPresenceChangedWorkspaceSpy(workspace(), &Workspace::desktopPresenceChanged); QVERIFY(desktopPresenceChangedWorkspaceSpy.isValid()); QSignalSpy desktopPresenceChangedEffectsSpy(effects, &EffectsHandler::desktopPresenceChanged); QVERIFY(desktopPresenceChangedEffectsSpy.isValid()); // let's change the desktop workspace()->sendClientToDesktop(c, 2, false); QCOMPARE(c->desktop(), 2); QCOMPARE(desktopPresenceChangedClientSpy.count(), 1); QCOMPARE(desktopPresenceChangedWorkspaceSpy.count(), 1); QCOMPARE(desktopPresenceChangedEffectsSpy.count(), 1); // verify the arguments QCOMPARE(desktopPresenceChangedClientSpy.first().at(0).value(), c); QCOMPARE(desktopPresenceChangedClientSpy.first().at(1).toInt(), 1); QCOMPARE(desktopPresenceChangedWorkspaceSpy.first().at(0).value(), c); QCOMPARE(desktopPresenceChangedWorkspaceSpy.first().at(1).toInt(), 1); QCOMPARE(desktopPresenceChangedEffectsSpy.first().at(0).value(), c->effectWindow()); QCOMPARE(desktopPresenceChangedEffectsSpy.first().at(1).toInt(), 1); QCOMPARE(desktopPresenceChangedEffectsSpy.first().at(2).toInt(), 2); } void TestXdgShellClient::testTransientPositionAfterRemap() { // this test simulates the situation that a transient window gets reused and the parent window // moved between the two usages QScopedPointer surface(Test::createSurface()); QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); auto c = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); QVERIFY(c); // create the Transient window XdgPositioner positioner(QSize(50, 40), QRect(0, 0, 5, 10)); positioner.setAnchorEdge(Qt::BottomEdge | Qt::RightEdge); positioner.setGravity(Qt::BottomEdge | Qt::RightEdge); QScopedPointer transientSurface(Test::createSurface()); QScopedPointer transientShellSurface(Test::createXdgShellStablePopup(transientSurface.data(), shellSurface.data(), positioner)); auto transient = Test::renderAndWaitForShown(transientSurface.data(), positioner.initialSize(), Qt::blue); QVERIFY(transient); QCOMPARE(transient->frameGeometry(), QRect(c->frameGeometry().topLeft() + QPoint(5, 10), QSize(50, 40))); // unmap the transient QSignalSpy windowHiddenSpy(transient, &XdgShellClient::windowHidden); QVERIFY(windowHiddenSpy.isValid()); transientSurface->attachBuffer(Buffer::Ptr()); transientSurface->commit(Surface::CommitFlag::None); QVERIFY(windowHiddenSpy.wait()); // now move the parent surface c->setFrameGeometry(c->frameGeometry().translated(5, 10)); // now map the transient again QSignalSpy windowShownSpy(transient, &XdgShellClient::windowShown); QVERIFY(windowShownSpy.isValid()); Test::render(transientSurface.data(), QSize(50, 40), Qt::blue); QVERIFY(windowShownSpy.wait()); QCOMPARE(transient->frameGeometry(), QRect(c->frameGeometry().topLeft() + QPoint(5, 10), QSize(50, 40))); } void TestXdgShellClient::testWindowOutputs_data() { QTest::addColumn("type"); QTest::newRow("xdgShellV6") << Test::XdgShellSurfaceType::XdgShellV6; QTest::newRow("xdgWmBase") << Test::XdgShellSurfaceType::XdgShellStable; } void TestXdgShellClient::testWindowOutputs() { QScopedPointer surface(Test::createSurface()); QFETCH(Test::XdgShellSurfaceType, type); QScopedPointer shellSurface(Test::createXdgShellSurface(type, surface.data())); auto size = QSize(200,200); QSignalSpy outputEnteredSpy(surface.data(), &Surface::outputEntered); QSignalSpy outputLeftSpy(surface.data(), &Surface::outputLeft); auto c = Test::renderAndWaitForShown(surface.data(), size, Qt::blue); //move to be in the first screen c->setFrameGeometry(QRect(QPoint(100,100), size)); //we don't don't know where the compositor first placed this window, //this might fire, it might not outputEnteredSpy.wait(5); outputEnteredSpy.clear(); QCOMPARE(surface->outputs().count(), 1); QCOMPARE(surface->outputs().first()->globalPosition(), QPoint(0,0)); //move to overlapping both first and second screen c->setFrameGeometry(QRect(QPoint(1250,100), size)); QVERIFY(outputEnteredSpy.wait()); QCOMPARE(outputEnteredSpy.count(), 1); QCOMPARE(outputLeftSpy.count(), 0); QCOMPARE(surface->outputs().count(), 2); QVERIFY(surface->outputs()[0] != surface->outputs()[1]); //move entirely into second screen c->setFrameGeometry(QRect(QPoint(1400,100), size)); QVERIFY(outputLeftSpy.wait()); QCOMPARE(outputEnteredSpy.count(), 1); QCOMPARE(outputLeftSpy.count(), 1); QCOMPARE(surface->outputs().count(), 1); QCOMPARE(surface->outputs().first()->globalPosition(), QPoint(1280,0)); } void TestXdgShellClient::testMinimizeActiveWindow_data() { QTest::addColumn("type"); QTest::newRow("xdgShellV6") << Test::XdgShellSurfaceType::XdgShellV6; QTest::newRow("xdgWmBase") << Test::XdgShellSurfaceType::XdgShellStable; } void TestXdgShellClient::testMinimizeActiveWindow() { // this test verifies that when minimizing the active window it gets deactivated QScopedPointer surface(Test::createSurface()); QFETCH(Test::XdgShellSurfaceType, type); QScopedPointer shellSurface(Test::createXdgShellSurface(type, surface.data())); auto c = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); QVERIFY(c); QVERIFY(c->isActive()); QCOMPARE(workspace()->activeClient(), c); QVERIFY(c->wantsInput()); QVERIFY(c->wantsTabFocus()); QVERIFY(c->isShown(true)); workspace()->slotWindowMinimize(); QVERIFY(!c->isShown(true)); QVERIFY(c->wantsInput()); QVERIFY(c->wantsTabFocus()); QVERIFY(!c->isActive()); QVERIFY(!workspace()->activeClient()); QVERIFY(c->isMinimized()); // unminimize again c->unminimize(); QVERIFY(!c->isMinimized()); QVERIFY(c->isActive()); QVERIFY(c->wantsInput()); QVERIFY(c->wantsTabFocus()); QVERIFY(c->isShown(true)); QCOMPARE(workspace()->activeClient(), c); } void TestXdgShellClient::testFullscreen_data() { QTest::addColumn("type"); QTest::addColumn("decoMode"); QTest::newRow("xdgShellV6") << Test::XdgShellSurfaceType::XdgShellV6 << ServerSideDecoration::Mode::Client; QTest::newRow("xdgShellWmBase") << Test::XdgShellSurfaceType::XdgShellStable << ServerSideDecoration::Mode::Client; QTest::newRow("xdgShellV6 - deco") << Test::XdgShellSurfaceType::XdgShellV6 << ServerSideDecoration::Mode::Server; QTest::newRow("xdgShellWmBase - deco") << Test::XdgShellSurfaceType::XdgShellStable << ServerSideDecoration::Mode::Server; } void TestXdgShellClient::testFullscreen() { // this test verifies that a window can be properly fullscreened QScopedPointer surface(Test::createSurface()); QFETCH(Test::XdgShellSurfaceType, type); QScopedPointer shellSurface(Test::createXdgShellSurface(type, surface.data())); QVERIFY(shellSurface); // create deco QScopedPointer deco(Test::waylandServerSideDecoration()->create(surface.data())); QSignalSpy decoSpy(deco.data(), &ServerSideDecoration::modeChanged); QVERIFY(decoSpy.isValid()); QVERIFY(decoSpy.wait()); QFETCH(ServerSideDecoration::Mode, decoMode); deco->requestMode(decoMode); QVERIFY(decoSpy.wait()); QCOMPARE(deco->mode(), decoMode); auto c = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); QVERIFY(c); QVERIFY(c->isActive()); QCOMPARE(c->layer(), NormalLayer); QVERIFY(!c->isFullScreen()); QCOMPARE(c->clientSize(), QSize(100, 50)); QCOMPARE(c->isDecorated(), decoMode == ServerSideDecoration::Mode::Server); QCOMPARE(c->sizeForClientSize(c->clientSize()), c->frameGeometry().size()); QSignalSpy fullscreenChangedSpy(c, &XdgShellClient::fullScreenChanged); QVERIFY(fullscreenChangedSpy.isValid()); - QSignalSpy geometryChangedSpy(c, &XdgShellClient::geometryChanged); - QVERIFY(geometryChangedSpy.isValid()); + QSignalSpy frameGeometryChangedSpy(c, &XdgShellClient::frameGeometryChanged); + QVERIFY(frameGeometryChangedSpy.isValid()); QSignalSpy sizeChangeRequestedSpy(shellSurface.data(), &XdgShellSurface::sizeChanged); QVERIFY(sizeChangeRequestedSpy.isValid()); QSignalSpy configureRequestedSpy(shellSurface.data(), &XdgShellSurface::configureRequested); QVERIFY(configureRequestedSpy.isValid()); shellSurface->setFullscreen(true); QVERIFY(fullscreenChangedSpy.wait()); QVERIFY(sizeChangeRequestedSpy.wait()); QCOMPARE(sizeChangeRequestedSpy.count(), 1); QCOMPARE(sizeChangeRequestedSpy.first().first().toSize(), QSize(screens()->size(0))); // TODO: should switch to fullscreen once it's updated QVERIFY(c->isFullScreen()); QCOMPARE(c->clientSize(), QSize(100, 50)); - QVERIFY(geometryChangedSpy.isEmpty()); + QVERIFY(frameGeometryChangedSpy.isEmpty()); shellSurface->ackConfigure(configureRequestedSpy.last().at(2).value()); Test::render(surface.data(), sizeChangeRequestedSpy.first().first().toSize(), Qt::red); - QVERIFY(geometryChangedSpy.wait()); - QCOMPARE(geometryChangedSpy.count(), 1); + QVERIFY(frameGeometryChangedSpy.wait()); + QCOMPARE(frameGeometryChangedSpy.count(), 1); QVERIFY(c->isFullScreen()); QVERIFY(!c->isDecorated()); QCOMPARE(c->frameGeometry(), QRect(QPoint(0, 0), sizeChangeRequestedSpy.first().first().toSize())); QCOMPARE(c->layer(), ActiveLayer); // swap back to normal shellSurface->setFullscreen(false); QVERIFY(fullscreenChangedSpy.wait()); QVERIFY(sizeChangeRequestedSpy.wait()); QCOMPARE(sizeChangeRequestedSpy.count(), 2); QCOMPARE(sizeChangeRequestedSpy.last().first().toSize(), QSize(100, 50)); // TODO: should switch to fullscreen once it's updated QVERIFY(!c->isFullScreen()); QCOMPARE(c->layer(), NormalLayer); QCOMPARE(c->isDecorated(), decoMode == ServerSideDecoration::Mode::Server); } void TestXdgShellClient::testFullscreenRestore_data() { QTest::addColumn("type"); QTest::newRow("xdgShellV6") << Test::XdgShellSurfaceType::XdgShellV6; QTest::newRow("xdgShellWmBase") << Test::XdgShellSurfaceType::XdgShellStable; } void TestXdgShellClient::testFullscreenRestore() { // this test verifies that windows created fullscreen can be later properly restored QScopedPointer surface(Test::createSurface()); QFETCH(Test::XdgShellSurfaceType, type); XdgShellSurface *xdgShellSurface = Test::createXdgShellSurface(type, surface.data(), surface.data(), Test::CreationSetup::CreateOnly); QSignalSpy configureRequestedSpy(xdgShellSurface, &XdgShellSurface::configureRequested); // fullscreen the window xdgShellSurface->setFullscreen(true); surface->commit(Surface::CommitFlag::None); configureRequestedSpy.wait(); QCOMPARE(configureRequestedSpy.count(), 1); const auto size = configureRequestedSpy.first()[0].value(); const auto state = configureRequestedSpy.first()[1].value(); QCOMPARE(size, screens()->size(0)); QVERIFY(state & KWayland::Client::XdgShellSurface::State::Fullscreen); xdgShellSurface->ackConfigure(configureRequestedSpy.first()[2].toUInt()); auto c = Test::renderAndWaitForShown(surface.data(), size, Qt::blue); QVERIFY(c); QVERIFY(c->isFullScreen()); configureRequestedSpy.wait(100); QSignalSpy fullscreenChangedSpy(c, &XdgShellClient::fullScreenChanged); QVERIFY(fullscreenChangedSpy.isValid()); - QSignalSpy geometryChangedSpy(c, &XdgShellClient::geometryChanged); - QVERIFY(geometryChangedSpy.isValid()); + QSignalSpy frameGeometryChangedSpy(c, &XdgShellClient::frameGeometryChanged); + QVERIFY(frameGeometryChangedSpy.isValid()); // swap back to normal configureRequestedSpy.clear(); xdgShellSurface->setFullscreen(false); QVERIFY(fullscreenChangedSpy.wait()); QVERIFY(configureRequestedSpy.wait()); QCOMPARE(configureRequestedSpy.last().first().toSize(), QSize(0, 0)); QVERIFY(!c->isFullScreen()); for (const auto &it: configureRequestedSpy) { xdgShellSurface->ackConfigure(it[2].toUInt()); } Test::render(surface.data(), QSize(100, 50), Qt::red); - QVERIFY(geometryChangedSpy.wait()); - QCOMPARE(geometryChangedSpy.count(), 1); + QVERIFY(frameGeometryChangedSpy.wait()); + QCOMPARE(frameGeometryChangedSpy.count(), 1); QVERIFY(!c->isFullScreen()); QCOMPARE(c->frameGeometry().size(), QSize(100, 50)); } void TestXdgShellClient::testUserCanSetFullscreen_data() { QTest::addColumn("type"); QTest::newRow("xdgShellV6") << Test::XdgShellSurfaceType::XdgShellV6; QTest::newRow("xdgWmBase") << Test::XdgShellSurfaceType::XdgShellStable; } void TestXdgShellClient::testUserCanSetFullscreen() { QScopedPointer surface(Test::createSurface()); QFETCH(Test::XdgShellSurfaceType, type); QScopedPointer shellSurface(Test::createXdgShellSurface(type, surface.data())); auto c = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); QVERIFY(c); QVERIFY(c->isActive()); QVERIFY(!c->isFullScreen()); QVERIFY(c->userCanSetFullScreen()); } void TestXdgShellClient::testUserSetFullscreen_data() { QTest::addColumn("type"); QTest::newRow("xdgShellV6") << Test::XdgShellSurfaceType::XdgShellV6; QTest::newRow("xdgWmBase") << Test::XdgShellSurfaceType::XdgShellStable; } void TestXdgShellClient::testUserSetFullscreen() { QScopedPointer surface(Test::createSurface()); QFETCH(Test::XdgShellSurfaceType, type); QScopedPointer shellSurface(Test::createXdgShellSurface( type, surface.data(), surface.data(), Test::CreationSetup::CreateOnly)); QVERIFY(!shellSurface.isNull()); // wait for the initial configure event QSignalSpy configureRequestedSpy(shellSurface.data(), &XdgShellSurface::configureRequested); QVERIFY(configureRequestedSpy.isValid()); surface->commit(Surface::CommitFlag::None); QVERIFY(configureRequestedSpy.wait()); QCOMPARE(configureRequestedSpy.count(), 1); shellSurface->ackConfigure(configureRequestedSpy.last().at(2).value()); auto c = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); QVERIFY(c); QVERIFY(c->isActive()); QVERIFY(!c->isFullScreen()); // The client gets activated, which gets another configure event. Though that's not relevant to the test configureRequestedSpy.wait(10); QSignalSpy fullscreenChangedSpy(c, &AbstractClient::fullScreenChanged); QVERIFY(fullscreenChangedSpy.isValid()); c->setFullScreen(true); QCOMPARE(c->isFullScreen(), true); configureRequestedSpy.clear(); QVERIFY(configureRequestedSpy.wait()); QCOMPARE(configureRequestedSpy.count(), 1); QCOMPARE(configureRequestedSpy.first().at(0).toSize(), screens()->size(0)); const auto states = configureRequestedSpy.first().at(1).value(); QVERIFY(states.testFlag(KWayland::Client::XdgShellSurface::State::Fullscreen)); QVERIFY(states.testFlag(KWayland::Client::XdgShellSurface::State::Activated)); QVERIFY(!states.testFlag(KWayland::Client::XdgShellSurface::State::Maximized)); QVERIFY(!states.testFlag(KWayland::Client::XdgShellSurface::State::Resizing)); QCOMPARE(fullscreenChangedSpy.count(), 1); QVERIFY(c->isFullScreen()); shellSurface->ackConfigure(configureRequestedSpy.first().at(2).value()); // unset fullscreen again c->setFullScreen(false); QCOMPARE(c->isFullScreen(), false); configureRequestedSpy.clear(); QVERIFY(configureRequestedSpy.wait()); QCOMPARE(configureRequestedSpy.count(), 1); QCOMPARE(configureRequestedSpy.first().at(0).toSize(), QSize(100, 50)); QVERIFY(!configureRequestedSpy.first().at(1).value().testFlag(KWayland::Client::XdgShellSurface::State::Fullscreen)); QCOMPARE(fullscreenChangedSpy.count(), 2); QVERIFY(!c->isFullScreen()); } void TestXdgShellClient::testMaximizedToFullscreen_data() { QTest::addColumn("type"); QTest::addColumn("decoMode"); QTest::newRow("xdgShellV6") << Test::XdgShellSurfaceType::XdgShellV6 << ServerSideDecoration::Mode::Client; QTest::newRow("xdgShellWmBase") << Test::XdgShellSurfaceType::XdgShellStable << ServerSideDecoration::Mode::Client; QTest::newRow("xdgShellV6 - deco") << Test::XdgShellSurfaceType::XdgShellV6 << ServerSideDecoration::Mode::Server; QTest::newRow("xdgShellWmBase - deco") << Test::XdgShellSurfaceType::XdgShellStable << ServerSideDecoration::Mode::Server; } void TestXdgShellClient::testMaximizedToFullscreen() { // this test verifies that a window can be properly fullscreened after maximizing QScopedPointer surface(Test::createSurface()); QFETCH(Test::XdgShellSurfaceType, type); QScopedPointer shellSurface(Test::createXdgShellSurface(type, surface.data())); QVERIFY(shellSurface); // create deco QScopedPointer deco(Test::waylandServerSideDecoration()->create(surface.data())); QSignalSpy decoSpy(deco.data(), &ServerSideDecoration::modeChanged); QVERIFY(decoSpy.isValid()); QVERIFY(decoSpy.wait()); QFETCH(ServerSideDecoration::Mode, decoMode); deco->requestMode(decoMode); QVERIFY(decoSpy.wait()); QCOMPARE(deco->mode(), decoMode); auto c = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); QVERIFY(c); QVERIFY(c->isActive()); QVERIFY(!c->isFullScreen()); QCOMPARE(c->clientSize(), QSize(100, 50)); QCOMPARE(c->isDecorated(), decoMode == ServerSideDecoration::Mode::Server); QSignalSpy fullscreenChangedSpy(c, &XdgShellClient::fullScreenChanged); QVERIFY(fullscreenChangedSpy.isValid()); - QSignalSpy geometryChangedSpy(c, &XdgShellClient::geometryChanged); - QVERIFY(geometryChangedSpy.isValid()); + QSignalSpy frameGeometryChangedSpy(c, &XdgShellClient::frameGeometryChanged); + QVERIFY(frameGeometryChangedSpy.isValid()); QSignalSpy sizeChangeRequestedSpy(shellSurface.data(), &XdgShellSurface::sizeChanged); QVERIFY(sizeChangeRequestedSpy.isValid()); QSignalSpy configureRequestedSpy(shellSurface.data(), &XdgShellSurface::configureRequested); QVERIFY(configureRequestedSpy.isValid()); shellSurface->setMaximized(true); QVERIFY(sizeChangeRequestedSpy.wait()); QCOMPARE(sizeChangeRequestedSpy.count(), 1); shellSurface->ackConfigure(configureRequestedSpy.last().at(2).value()); Test::render(surface.data(), sizeChangeRequestedSpy.last().first().toSize(), Qt::red); - QVERIFY(geometryChangedSpy.wait()); + QVERIFY(frameGeometryChangedSpy.wait()); QCOMPARE(c->maximizeMode(), MaximizeFull); - QCOMPARE(geometryChangedSpy.isEmpty(), false); - geometryChangedSpy.clear(); + QCOMPARE(frameGeometryChangedSpy.isEmpty(), false); + frameGeometryChangedSpy.clear(); // fullscreen the window shellSurface->setFullscreen(true); QVERIFY(fullscreenChangedSpy.wait()); if (decoMode == ServerSideDecoration::Mode::Server) { QVERIFY(sizeChangeRequestedSpy.wait()); QCOMPARE(sizeChangeRequestedSpy.count(), 2); } QCOMPARE(sizeChangeRequestedSpy.last().first().toSize(), QSize(screens()->size(0))); // TODO: should switch to fullscreen once it's updated QVERIFY(c->isFullScreen()); // render at the new size shellSurface->ackConfigure(configureRequestedSpy.last().at(2).value()); Test::render(surface.data(), sizeChangeRequestedSpy.last().first().toSize(), Qt::red); QVERIFY(c->isFullScreen()); QVERIFY(!c->isDecorated()); QCOMPARE(c->frameGeometry(), QRect(QPoint(0, 0), sizeChangeRequestedSpy.last().first().toSize())); sizeChangeRequestedSpy.clear(); // swap back to normal shellSurface->setFullscreen(false); shellSurface->setMaximized(false); QVERIFY(fullscreenChangedSpy.wait()); if (decoMode == ServerSideDecoration::Mode::Server) { QVERIFY(sizeChangeRequestedSpy.wait()); // XDG will legitimately get two updates. They might be batched if (shellSurface && sizeChangeRequestedSpy.count() == 1) { QVERIFY(sizeChangeRequestedSpy.wait()); } QCOMPARE(sizeChangeRequestedSpy.last().first().toSize(), QSize(100, 50)); } // TODO: should switch to fullscreen once it's updated QVERIFY(!c->isFullScreen()); QCOMPARE(c->isDecorated(), decoMode == ServerSideDecoration::Mode::Server); } void TestXdgShellClient::testWindowOpensLargerThanScreen_data() { QTest::addColumn("type"); QTest::newRow("xdgShellV6") << Test::XdgShellSurfaceType::XdgShellV6; QTest::newRow("xdgWmBase") << Test::XdgShellSurfaceType::XdgShellStable; } void TestXdgShellClient::testWindowOpensLargerThanScreen() { // this test creates a window which is as large as the screen, but is decorated // the window should get resized to fit into the screen, BUG: 366632 QScopedPointer surface(Test::createSurface()); QFETCH(Test::XdgShellSurfaceType, type); QScopedPointer shellSurface(Test::createXdgShellSurface(type, surface.data())); QSignalSpy sizeChangeRequestedSpy(shellSurface.data(), SIGNAL(sizeChanged(QSize))); QVERIFY(sizeChangeRequestedSpy.isValid()); // create deco QScopedPointer deco(Test::waylandServerSideDecoration()->create(surface.data())); QSignalSpy decoSpy(deco.data(), &ServerSideDecoration::modeChanged); QVERIFY(decoSpy.isValid()); QVERIFY(decoSpy.wait()); deco->requestMode(ServerSideDecoration::Mode::Server); QVERIFY(decoSpy.wait()); QCOMPARE(deco->mode(), ServerSideDecoration::Mode::Server); auto c = Test::renderAndWaitForShown(surface.data(), screens()->size(0), Qt::blue); QVERIFY(c); QVERIFY(c->isActive()); QCOMPARE(c->clientSize(), screens()->size(0)); QVERIFY(c->isDecorated()); QEXPECT_FAIL("", "BUG 366632", Continue); QVERIFY(sizeChangeRequestedSpy.wait(10)); } void TestXdgShellClient::testHidden_data() { QTest::addColumn("type"); QTest::newRow("xdgShellV6") << Test::XdgShellSurfaceType::XdgShellV6; QTest::newRow("xdgWmBase") << Test::XdgShellSurfaceType::XdgShellStable; } void TestXdgShellClient::testHidden() { // this test verifies that when hiding window it doesn't get shown QScopedPointer surface(Test::createSurface()); QFETCH(Test::XdgShellSurfaceType, type); QScopedPointer shellSurface(Test::createXdgShellSurface(type, surface.data())); auto c = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); QVERIFY(c); QVERIFY(c->isActive()); QCOMPARE(workspace()->activeClient(), c); QVERIFY(c->wantsInput()); QVERIFY(c->wantsTabFocus()); QVERIFY(c->isShown(true)); c->hideClient(true); QVERIFY(!c->isShown(true)); QVERIFY(!c->isActive()); QVERIFY(c->wantsInput()); QVERIFY(c->wantsTabFocus()); // unhide again c->hideClient(false); QVERIFY(c->isShown(true)); QVERIFY(c->wantsInput()); QVERIFY(c->wantsTabFocus()); //QCOMPARE(workspace()->activeClient(), c); } void TestXdgShellClient::testDesktopFileName() { QIcon::setThemeName(QStringLiteral("breeze")); // this test verifies that desktop file name is passed correctly to the window QScopedPointer surface(Test::createSurface()); // only xdg-shell as ShellSurface misses the setter QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); shellSurface->setAppId(QByteArrayLiteral("org.kde.foo")); auto c = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); QVERIFY(c); QCOMPARE(c->desktopFileName(), QByteArrayLiteral("org.kde.foo")); QCOMPARE(c->resourceClass(), QByteArrayLiteral("org.kde.foo")); QVERIFY(c->resourceName().startsWith("testXdgShellClient")); // the desktop file does not exist, so icon should be generic Wayland QCOMPARE(c->icon().name(), QStringLiteral("wayland")); QSignalSpy desktopFileNameChangedSpy(c, &AbstractClient::desktopFileNameChanged); QVERIFY(desktopFileNameChangedSpy.isValid()); QSignalSpy iconChangedSpy(c, &XdgShellClient::iconChanged); QVERIFY(iconChangedSpy.isValid()); shellSurface->setAppId(QByteArrayLiteral("org.kde.bar")); QVERIFY(desktopFileNameChangedSpy.wait()); QCOMPARE(c->desktopFileName(), QByteArrayLiteral("org.kde.bar")); QCOMPARE(c->resourceClass(), QByteArrayLiteral("org.kde.bar")); QVERIFY(c->resourceName().startsWith("testXdgShellClient")); // icon should still be wayland QCOMPARE(c->icon().name(), QStringLiteral("wayland")); QVERIFY(iconChangedSpy.isEmpty()); const QString dfPath = QFINDTESTDATA("data/example.desktop"); shellSurface->setAppId(dfPath.toUtf8()); QVERIFY(desktopFileNameChangedSpy.wait()); QCOMPARE(iconChangedSpy.count(), 1); QCOMPARE(QString::fromUtf8(c->desktopFileName()), dfPath); QCOMPARE(c->icon().name(), QStringLiteral("kwin")); } void TestXdgShellClient::testCaptionSimplified() { // this test verifies that caption is properly trimmed // see BUG 323798 comment #12 QScopedPointer surface(Test::createSurface()); // only done for xdg-shell as ShellSurface misses the setter QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); const QString origTitle = QString::fromUtf8(QByteArrayLiteral("Was tun, wenn Schüler Autismus haben?\342\200\250\342\200\250\342\200\250 – Marlies Hübner - Mozilla Firefox")); shellSurface->setTitle(origTitle); auto c = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); QVERIFY(c); QVERIFY(c->caption() != origTitle); QCOMPARE(c->caption(), origTitle.simplified()); } void TestXdgShellClient::testCaptionMultipleWindows() { QScopedPointer surface(Test::createSurface()); QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); shellSurface->setTitle(QStringLiteral("foo")); auto c = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); QVERIFY(c); QCOMPARE(c->caption(), QStringLiteral("foo")); QCOMPARE(c->captionNormal(), QStringLiteral("foo")); QCOMPARE(c->captionSuffix(), QString()); QScopedPointer surface2(Test::createSurface()); QScopedPointer shellSurface2(Test::createXdgShellStableSurface(surface2.data())); shellSurface2->setTitle(QStringLiteral("foo")); auto c2 = Test::renderAndWaitForShown(surface2.data(), QSize(100, 50), Qt::blue); QVERIFY(c2); QCOMPARE(c2->caption(), QStringLiteral("foo <2>")); QCOMPARE(c2->captionNormal(), QStringLiteral("foo")); QCOMPARE(c2->captionSuffix(), QStringLiteral(" <2>")); QScopedPointer surface3(Test::createSurface()); QScopedPointer shellSurface3(Test::createXdgShellStableSurface(surface3.data())); shellSurface3->setTitle(QStringLiteral("foo")); auto c3 = Test::renderAndWaitForShown(surface3.data(), QSize(100, 50), Qt::blue); QVERIFY(c3); QCOMPARE(c3->caption(), QStringLiteral("foo <3>")); QCOMPARE(c3->captionNormal(), QStringLiteral("foo")); QCOMPARE(c3->captionSuffix(), QStringLiteral(" <3>")); QScopedPointer surface4(Test::createSurface()); QScopedPointer shellSurface4(Test::createXdgShellStableSurface(surface4.data())); shellSurface4->setTitle(QStringLiteral("bar")); auto c4 = Test::renderAndWaitForShown(surface4.data(), QSize(100, 50), Qt::blue); QVERIFY(c4); QCOMPARE(c4->caption(), QStringLiteral("bar")); QCOMPARE(c4->captionNormal(), QStringLiteral("bar")); QCOMPARE(c4->captionSuffix(), QString()); QSignalSpy captionChangedSpy(c4, &XdgShellClient::captionChanged); QVERIFY(captionChangedSpy.isValid()); shellSurface4->setTitle(QStringLiteral("foo")); QVERIFY(captionChangedSpy.wait()); QCOMPARE(captionChangedSpy.count(), 1); QCOMPARE(c4->caption(), QStringLiteral("foo <4>")); QCOMPARE(c4->captionNormal(), QStringLiteral("foo")); QCOMPARE(c4->captionSuffix(), QStringLiteral(" <4>")); } void TestXdgShellClient::testUnresponsiveWindow_data() { QTest::addColumn("shellInterface");//see env selection in qwaylandintegration.cpp QTest::addColumn("socketMode"); QTest::newRow("xdgv6 display") << "xdg-shell-v6" << false; QTest::newRow("xdgv6 socket") << "xdg-shell-v6" << true; //TODO add XDG WM Base when Kwin relies on Qt 5.12 } void TestXdgShellClient::testUnresponsiveWindow() { // this test verifies that killWindow properly terminates a process // for this an external binary is launched const QString kill = QFINDTESTDATA(QStringLiteral("kill")); QVERIFY(!kill.isEmpty()); QSignalSpy shellClientAddedSpy(waylandServer(), &WaylandServer::shellClientAdded); QVERIFY(shellClientAddedSpy.isValid()); QScopedPointer process(new QProcess); QProcessEnvironment env = QProcessEnvironment::systemEnvironment(); QFETCH(QString, shellInterface); QFETCH(bool, socketMode); env.insert("QT_WAYLAND_SHELL_INTEGRATION", shellInterface); if (socketMode) { int sx[2]; QVERIFY(socketpair(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0, sx) >= 0); waylandServer()->display()->createClient(sx[0]); int socket = dup(sx[1]); QVERIFY(socket != -1); env.insert(QStringLiteral("WAYLAND_SOCKET"), QByteArray::number(socket)); env.remove("WAYLAND_DISPLAY"); } else { env.insert("WAYLAND_DISPLAY", s_socketName); } process->setProcessEnvironment(env); process->setProcessChannelMode(QProcess::ForwardedChannels); process->setProgram(kill); QSignalSpy processStartedSpy{process.data(), &QProcess::started}; QVERIFY(processStartedSpy.isValid()); process->start(); QVERIFY(processStartedSpy.wait()); AbstractClient *killClient = nullptr; if (shellClientAddedSpy.isEmpty()) { QVERIFY(shellClientAddedSpy.wait()); } ::kill(process->processId(), SIGUSR1); // send a signal to freeze the process killClient = shellClientAddedSpy.first().first().value(); QVERIFY(killClient); QSignalSpy unresponsiveSpy(killClient, &AbstractClient::unresponsiveChanged); QSignalSpy killedSpy(process.data(), static_cast(&QProcess::finished)); QSignalSpy deletedSpy(killClient, &QObject::destroyed); qint64 startTime = QDateTime::currentMSecsSinceEpoch(); //wait for the process to be frozen QTest::qWait(10); //pretend the user clicked the close button killClient->closeWindow(); //client should not yet be marked unresponsive nor killed QVERIFY(!killClient->unresponsive()); QVERIFY(killedSpy.isEmpty()); QVERIFY(unresponsiveSpy.wait()); //client should be marked unresponsive but not killed auto elapsed1 = QDateTime::currentMSecsSinceEpoch() - startTime; QVERIFY(elapsed1 > 900 && elapsed1 < 1200); //ping timer is 1s, but coarse timers on a test across two processes means we need a fuzzy compare QVERIFY(killClient->unresponsive()); QVERIFY(killedSpy.isEmpty()); QVERIFY(deletedSpy.wait()); if (!socketMode) { //process was killed - because we're across process this could happen in either order QVERIFY(killedSpy.count() || killedSpy.wait()); } auto elapsed2 = QDateTime::currentMSecsSinceEpoch() - startTime; QVERIFY(elapsed2 > 1800); //second ping comes in a second later } void TestXdgShellClient::testX11WindowId_data() { QTest::addColumn("type"); QTest::newRow("xdgShellV6") << Test::XdgShellSurfaceType::XdgShellV6; QTest::newRow("xdgWmBase") << Test::XdgShellSurfaceType::XdgShellStable; } void TestXdgShellClient::testX11WindowId() { QScopedPointer surface(Test::createSurface()); QFETCH(Test::XdgShellSurfaceType, type); QScopedPointer shellSurface(Test::createXdgShellSurface(type, surface.data())); auto c = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); QVERIFY(c); QVERIFY(c->windowId() != 0); QCOMPARE(c->window(), 0u); } void TestXdgShellClient::testAppMenu() { //register a faux appmenu client QVERIFY (QDBusConnection::sessionBus().registerService("org.kde.kappmenu")); QScopedPointer surface(Test::createSurface()); QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); auto c = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); QVERIFY(c); QScopedPointer menu(Test::waylandAppMenuManager()->create(surface.data())); QSignalSpy spy(c, &XdgShellClient::hasApplicationMenuChanged); menu->setAddress("service.name", "object/path"); spy.wait(); QCOMPARE(c->hasApplicationMenu(), true); QCOMPARE(c->applicationMenuServiceName(), QString("service.name")); QCOMPARE(c->applicationMenuObjectPath(), QString("object/path")); QVERIFY (QDBusConnection::sessionBus().unregisterService("org.kde.kappmenu")); } void TestXdgShellClient::testNoDecorationModeRequested_data() { QTest::addColumn("type"); QTest::newRow("xdgShellV6") << Test::XdgShellSurfaceType::XdgShellV6; QTest::newRow("xdgWmBase") << Test::XdgShellSurfaceType::XdgShellStable; } void TestXdgShellClient::testNoDecorationModeRequested() { // this test verifies that the decoration follows the default mode if no mode is explicitly requested QScopedPointer surface(Test::createSurface()); QFETCH(Test::XdgShellSurfaceType, type); QScopedPointer shellSurface(Test::createXdgShellSurface(type, surface.data())); QScopedPointer deco(Test::waylandServerSideDecoration()->create(surface.data())); QSignalSpy decoSpy(deco.data(), &ServerSideDecoration::modeChanged); QVERIFY(decoSpy.isValid()); if (deco->mode() != ServerSideDecoration::Mode::Server) { QVERIFY(decoSpy.wait()); } QCOMPARE(deco->mode(), ServerSideDecoration::Mode::Server); auto c = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); QVERIFY(c); QCOMPARE(c->noBorder(), false); QCOMPARE(c->isDecorated(), true); } void TestXdgShellClient::testSendClientWithTransientToDesktop_data() { QTest::addColumn("type"); QTest::newRow("xdgShellV6") << Test::XdgShellSurfaceType::XdgShellV6; QTest::newRow("xdgWmBase") << Test::XdgShellSurfaceType::XdgShellStable; } void TestXdgShellClient::testSendClientWithTransientToDesktop() { // this test verifies that when sending a client to a desktop all transients are also send to that desktop VirtualDesktopManager::self()->setCount(2); QScopedPointer surface{Test::createSurface()}; QFETCH(Test::XdgShellSurfaceType, type); QScopedPointer shellSurface(Test::createXdgShellSurface(type, surface.data())); auto c = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); QVERIFY(c); // let's create a transient window QScopedPointer transientSurface{Test::createSurface()}; QScopedPointer transientShellSurface(Test::createXdgShellSurface(type, transientSurface.data())); transientShellSurface->setTransientFor(shellSurface.data()); auto transient = Test::renderAndWaitForShown(transientSurface.data(), QSize(100, 50), Qt::blue); QVERIFY(transient); QCOMPARE(workspace()->activeClient(), transient); QCOMPARE(transient->transientFor(), c); QVERIFY(c->transients().contains(transient)); QCOMPARE(c->desktop(), 1); QVERIFY(!c->isOnAllDesktops()); QCOMPARE(transient->desktop(), 1); QVERIFY(!transient->isOnAllDesktops()); workspace()->slotWindowToDesktop(2); QCOMPARE(c->desktop(), 1); QCOMPARE(transient->desktop(), 2); // activate c workspace()->activateClient(c); QCOMPARE(workspace()->activeClient(), c); QVERIFY(c->isActive()); // and send it to the desktop it's already on QCOMPARE(c->desktop(), 1); QCOMPARE(transient->desktop(), 2); workspace()->slotWindowToDesktop(1); // which should move the transient back to the desktop QCOMPARE(c->desktop(), 1); QCOMPARE(transient->desktop(), 1); } void TestXdgShellClient::testMinimizeWindowWithTransients_data() { QTest::addColumn("type"); QTest::newRow("xdgShellV6") << Test::XdgShellSurfaceType::XdgShellV6; QTest::newRow("xdgWmBase") << Test::XdgShellSurfaceType::XdgShellStable; } void TestXdgShellClient::testMinimizeWindowWithTransients() { // this test verifies that when minimizing/unminimizing a window all its // transients will be minimized/unminimized as well // create the main window QScopedPointer surface(Test::createSurface()); QFETCH(Test::XdgShellSurfaceType, type); QScopedPointer shellSurface(Test::createXdgShellSurface(type, surface.data())); auto c = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); QVERIFY(c); QVERIFY(!c->isMinimized()); // create a transient window QScopedPointer transientSurface(Test::createSurface()); QScopedPointer transientShellSurface(Test::createXdgShellSurface(type, transientSurface.data())); transientShellSurface->setTransientFor(shellSurface.data()); auto transient = Test::renderAndWaitForShown(transientSurface.data(), QSize(100, 50), Qt::red); QVERIFY(transient); QVERIFY(!transient->isMinimized()); QCOMPARE(transient->transientFor(), c); QVERIFY(c->hasTransient(transient, false)); // minimize the main window, the transient should be minimized as well c->minimize(); QVERIFY(c->isMinimized()); QVERIFY(transient->isMinimized()); // unminimize the main window, the transient should be unminimized as well c->unminimize(); QVERIFY(!c->isMinimized()); QVERIFY(!transient->isMinimized()); } void TestXdgShellClient::testXdgDecoration_data() { QTest::addColumn("requestedMode"); QTest::addColumn("expectedMode"); QTest::newRow("client side requested") << XdgDecoration::Mode::ClientSide << XdgDecoration::Mode::ClientSide; QTest::newRow("server side requested") << XdgDecoration::Mode::ServerSide << XdgDecoration::Mode::ServerSide; } void TestXdgShellClient::testXdgDecoration() { QScopedPointer surface(Test::createSurface()); QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); QScopedPointer deco(Test::xdgDecorationManager()->getToplevelDecoration(shellSurface.data())); QSignalSpy decorationConfiguredSpy(deco.data(), &XdgDecoration::modeChanged); QSignalSpy configureRequestedSpy(shellSurface.data(), &XdgShellSurface::configureRequested); QFETCH(KWayland::Client::XdgDecoration::Mode, requestedMode); QFETCH(KWayland::Client::XdgDecoration::Mode, expectedMode); //request a mode deco->setMode(requestedMode); //kwin will send a configure decorationConfiguredSpy.wait(); configureRequestedSpy.wait(); QCOMPARE(decorationConfiguredSpy.count(), 1); QCOMPARE(decorationConfiguredSpy.first()[0].value(), expectedMode); QVERIFY(configureRequestedSpy.count() > 0); shellSurface->ackConfigure(configureRequestedSpy.last()[2].toInt()); auto c = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); QCOMPARE(c->userCanSetNoBorder(), expectedMode == XdgDecoration::Mode::ServerSide); QCOMPARE(c->isDecorated(), expectedMode == XdgDecoration::Mode::ServerSide); } void TestXdgShellClient::testXdgNeverCommitted() { //check we don't crash if we create a shell object but delete the XdgShellClient before committing it QScopedPointer surface(Test::createSurface()); QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data(), nullptr, Test::CreationSetup::CreateOnly)); } void TestXdgShellClient::testXdgInitialState() { QScopedPointer surface(Test::createSurface()); QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data(), nullptr, Test::CreationSetup::CreateOnly)); QSignalSpy configureRequestedSpy(shellSurface.data(), &XdgShellSurface::configureRequested); surface->commit(Surface::CommitFlag::None); configureRequestedSpy.wait(); QCOMPARE(configureRequestedSpy.count(), 1); const auto size = configureRequestedSpy.first()[0].value(); QCOMPARE(size, QSize(0, 0)); //client should chose it's preferred size shellSurface->ackConfigure(configureRequestedSpy.first()[2].toUInt()); auto c = Test::renderAndWaitForShown(surface.data(), QSize(200,100), Qt::blue); QCOMPARE(c->size(), QSize(200, 100)); } void TestXdgShellClient::testXdgInitiallyMaximised() { QScopedPointer surface(Test::createSurface()); QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data(), nullptr, Test::CreationSetup::CreateOnly)); QSignalSpy configureRequestedSpy(shellSurface.data(), &XdgShellSurface::configureRequested); shellSurface->setMaximized(true); surface->commit(Surface::CommitFlag::None); configureRequestedSpy.wait(); QCOMPARE(configureRequestedSpy.count(), 1); const auto size = configureRequestedSpy.first()[0].value(); const auto state = configureRequestedSpy.first()[1].value(); QCOMPARE(size, QSize(1280, 1024)); QVERIFY(state & KWayland::Client::XdgShellSurface::State::Maximized); shellSurface->ackConfigure(configureRequestedSpy.first()[2].toUInt()); auto c = Test::renderAndWaitForShown(surface.data(), size, Qt::blue); QCOMPARE(c->maximizeMode(), MaximizeFull); QCOMPARE(c->size(), QSize(1280, 1024)); } void TestXdgShellClient::testXdgInitiallyFullscreen() { QScopedPointer surface(Test::createSurface()); QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data(), nullptr, Test::CreationSetup::CreateOnly)); QSignalSpy configureRequestedSpy(shellSurface.data(), &XdgShellSurface::configureRequested); shellSurface->setFullscreen(true); surface->commit(Surface::CommitFlag::None); configureRequestedSpy.wait(); QCOMPARE(configureRequestedSpy.count(), 1); const auto size = configureRequestedSpy.first()[0].value(); const auto state = configureRequestedSpy.first()[1].value(); QCOMPARE(size, QSize(1280, 1024)); QVERIFY(state & KWayland::Client::XdgShellSurface::State::Fullscreen); shellSurface->ackConfigure(configureRequestedSpy.first()[2].toUInt()); auto c = Test::renderAndWaitForShown(surface.data(), size, Qt::blue); QCOMPARE(c->isFullScreen(), true); QCOMPARE(c->size(), QSize(1280, 1024)); } void TestXdgShellClient::testXdgInitiallyMinimized() { QScopedPointer surface(Test::createSurface()); QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data(), nullptr, Test::CreationSetup::CreateOnly)); QSignalSpy configureRequestedSpy(shellSurface.data(), &XdgShellSurface::configureRequested); shellSurface->requestMinimize(); surface->commit(Surface::CommitFlag::None); configureRequestedSpy.wait(); QCOMPARE(configureRequestedSpy.count(), 1); const auto size = configureRequestedSpy.first()[0].value(); const auto state = configureRequestedSpy.first()[1].value(); QCOMPARE(size, QSize(0, 0)); QCOMPARE(state, 0); shellSurface->ackConfigure(configureRequestedSpy.first()[2].toUInt()); QEXPECT_FAIL("", "Client created in a minimised state is not exposed to kwin bug 404838", Abort); auto c = Test::renderAndWaitForShown(surface.data(), size, Qt::blue, QImage::Format_ARGB32, 10); QVERIFY(c); QVERIFY(c->isMinimized()); } void TestXdgShellClient::testXdgWindowGeometryIsntSet() { // This test verifies that the effective window geometry corresponds to the // bounding rectangle of the main surface and its sub-surfaces if no window // geometry is set by the client. QScopedPointer surface(Test::createSurface()); QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); XdgShellClient *client = Test::renderAndWaitForShown(surface.data(), QSize(200, 100), Qt::red); QVERIFY(client); QCOMPARE(client->bufferGeometry().size(), QSize(200, 100)); QCOMPARE(client->frameGeometry().size(), QSize(200, 100)); const QPoint oldPosition = client->pos(); - QSignalSpy geometryChangedSpy(client, &AbstractClient::geometryChanged); - QVERIFY(geometryChangedSpy.isValid()); + QSignalSpy frameGeometryChangedSpy(client, &AbstractClient::frameGeometryChanged); + QVERIFY(frameGeometryChangedSpy.isValid()); Test::render(surface.data(), QSize(100, 50), Qt::blue); - QVERIFY(geometryChangedSpy.wait()); + QVERIFY(frameGeometryChangedSpy.wait()); QCOMPARE(client->frameGeometry().topLeft(), oldPosition); QCOMPARE(client->frameGeometry().size(), QSize(100, 50)); QCOMPARE(client->bufferGeometry().topLeft(), oldPosition); QCOMPARE(client->bufferGeometry().size(), QSize(100, 50)); QScopedPointer childSurface(Test::createSurface()); QScopedPointer subSurface(Test::createSubSurface(childSurface.data(), surface.data())); QVERIFY(subSurface); subSurface->setPosition(QPoint(-20, -10)); Test::render(childSurface.data(), QSize(100, 50), Qt::blue); surface->commit(Surface::CommitFlag::None); - QVERIFY(geometryChangedSpy.wait()); + QVERIFY(frameGeometryChangedSpy.wait()); QCOMPARE(client->frameGeometry().topLeft(), oldPosition); QCOMPARE(client->frameGeometry().size(), QSize(120, 60)); QCOMPARE(client->bufferGeometry().topLeft(), oldPosition + QPoint(20, 10)); QCOMPARE(client->bufferGeometry().size(), QSize(100, 50)); } void TestXdgShellClient::testXdgWindowGeometryAttachBuffer() { // This test verifies that the effective window geometry remains the same when // a new buffer is attached and xdg_surface.set_window_geometry is not called // again. Notice that the window geometry must remain the same even if the new // buffer is smaller. QScopedPointer surface(Test::createSurface()); QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); XdgShellClient *client = Test::renderAndWaitForShown(surface.data(), QSize(200, 100), Qt::red); QVERIFY(client); QCOMPARE(client->bufferGeometry().size(), QSize(200, 100)); QCOMPARE(client->frameGeometry().size(), QSize(200, 100)); const QPoint oldPosition = client->pos(); - QSignalSpy geometryChangedSpy(client, &AbstractClient::geometryChanged); - QVERIFY(geometryChangedSpy.isValid()); + QSignalSpy frameGeometryChangedSpy(client, &AbstractClient::frameGeometryChanged); + QVERIFY(frameGeometryChangedSpy.isValid()); shellSurface->setWindowGeometry(QRect(10, 10, 180, 80)); surface->commit(Surface::CommitFlag::None); - QVERIFY(geometryChangedSpy.wait()); + QVERIFY(frameGeometryChangedSpy.wait()); + QCOMPARE(frameGeometryChangedSpy.count(), 1); QCOMPARE(client->frameGeometry().topLeft(), oldPosition); QCOMPARE(client->frameGeometry().size(), QSize(180, 80)); QCOMPARE(client->bufferGeometry().topLeft(), oldPosition - QPoint(10, 10)); QCOMPARE(client->bufferGeometry().size(), QSize(200, 100)); Test::render(surface.data(), QSize(100, 50), Qt::blue); - QVERIFY(geometryChangedSpy.wait()); + QVERIFY(frameGeometryChangedSpy.wait()); + QCOMPARE(frameGeometryChangedSpy.count(), 2); QCOMPARE(client->frameGeometry().topLeft(), oldPosition); - QEXPECT_FAIL("", "Ask on wayland-devel what effective window geometry should be here", Continue); - QCOMPARE(client->frameGeometry().size(), QSize(180, 80)); + QCOMPARE(client->frameGeometry().size(), QSize(100, 50)); QCOMPARE(client->bufferGeometry().topLeft(), oldPosition - QPoint(10, 10)); QCOMPARE(client->bufferGeometry().size(), QSize(100, 50)); shellSurface->setWindowGeometry(QRect(5, 5, 90, 40)); surface->commit(Surface::CommitFlag::None); - QVERIFY(geometryChangedSpy.wait()); + QVERIFY(frameGeometryChangedSpy.wait()); + QCOMPARE(frameGeometryChangedSpy.count(), 3); QCOMPARE(client->frameGeometry().topLeft(), oldPosition); QCOMPARE(client->frameGeometry().size(), QSize(90, 40)); QCOMPARE(client->bufferGeometry().topLeft(), oldPosition - QPoint(5, 5)); QCOMPARE(client->bufferGeometry().size(), QSize(100, 50)); shellSurface.reset(); QVERIFY(Test::waitForWindowDestroyed(client)); } void TestXdgShellClient::testXdgWindowGeometryAttachSubSurface() { // This test verifies that the effective window geometry remains the same // when a new sub-surface is added and xdg_surface.set_window_geometry is // not called again. QScopedPointer surface(Test::createSurface()); QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); XdgShellClient *client = Test::renderAndWaitForShown(surface.data(), QSize(200, 100), Qt::red); QVERIFY(client); QCOMPARE(client->bufferGeometry().size(), QSize(200, 100)); QCOMPARE(client->frameGeometry().size(), QSize(200, 100)); const QPoint oldPosition = client->pos(); - QSignalSpy geometryChangedSpy(client, &XdgShellClient::geometryChanged); - QVERIFY(geometryChangedSpy.isValid()); + QSignalSpy frameGeometryChangedSpy(client, &XdgShellClient::frameGeometryChanged); + QVERIFY(frameGeometryChangedSpy.isValid()); shellSurface->setWindowGeometry(QRect(10, 10, 180, 80)); surface->commit(Surface::CommitFlag::None); - QVERIFY(geometryChangedSpy.wait()); + QVERIFY(frameGeometryChangedSpy.wait()); QCOMPARE(client->frameGeometry().topLeft(), oldPosition); QCOMPARE(client->frameGeometry().size(), QSize(180, 80)); QCOMPARE(client->bufferGeometry().topLeft(), oldPosition - QPoint(10, 10)); QCOMPARE(client->bufferGeometry().size(), QSize(200, 100)); QScopedPointer childSurface(Test::createSurface()); QScopedPointer subSurface(Test::createSubSurface(childSurface.data(), surface.data())); QVERIFY(subSurface); subSurface->setPosition(QPoint(-20, -20)); Test::render(childSurface.data(), QSize(100, 50), Qt::blue); surface->commit(Surface::CommitFlag::None); QCOMPARE(client->frameGeometry().topLeft(), oldPosition); QCOMPARE(client->frameGeometry().size(), QSize(180, 80)); QCOMPARE(client->bufferGeometry().topLeft(), oldPosition - QPoint(10, 10)); QCOMPARE(client->bufferGeometry().size(), QSize(200, 100)); shellSurface->setWindowGeometry(QRect(-15, -15, 50, 40)); surface->commit(Surface::CommitFlag::None); - QVERIFY(geometryChangedSpy.wait()); + QVERIFY(frameGeometryChangedSpy.wait()); QCOMPARE(client->frameGeometry().topLeft(), oldPosition); QCOMPARE(client->frameGeometry().size(), QSize(50, 40)); QCOMPARE(client->bufferGeometry().topLeft(), oldPosition - QPoint(-15, -15)); QCOMPARE(client->bufferGeometry().size(), QSize(200, 100)); } void TestXdgShellClient::testXdgWindowGeometryInteractiveResize() { // This test verifies that correct window geometry is provided along each // configure event when an xdg-shell is being interactively resized. QScopedPointer surface(Test::createSurface()); QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); XdgShellClient *client = Test::renderAndWaitForShown(surface.data(), QSize(200, 100), Qt::red); QVERIFY(client); QVERIFY(client->isActive()); QCOMPARE(client->bufferGeometry().size(), QSize(200, 100)); QCOMPARE(client->frameGeometry().size(), QSize(200, 100)); QSignalSpy configureRequestedSpy(shellSurface.data(), &XdgShellSurface::configureRequested); QVERIFY(configureRequestedSpy.isValid()); QVERIFY(configureRequestedSpy.wait()); QCOMPARE(configureRequestedSpy.count(), 1); - QSignalSpy geometryChangedSpy(client, &AbstractClient::geometryChanged); - QVERIFY(geometryChangedSpy.isValid()); + QSignalSpy frameGeometryChangedSpy(client, &AbstractClient::frameGeometryChanged); + QVERIFY(frameGeometryChangedSpy.isValid()); shellSurface->setWindowGeometry(QRect(10, 10, 180, 80)); surface->commit(Surface::CommitFlag::None); - QVERIFY(geometryChangedSpy.wait()); + QVERIFY(frameGeometryChangedSpy.wait()); QCOMPARE(client->bufferGeometry().size(), QSize(200, 100)); QCOMPARE(client->frameGeometry().size(), QSize(180, 80)); QSignalSpy clientStartMoveResizedSpy(client, &AbstractClient::clientStartUserMovedResized); QVERIFY(clientStartMoveResizedSpy.isValid()); QSignalSpy clientStepUserMovedResizedSpy(client, &AbstractClient::clientStepUserMovedResized); QVERIFY(clientStepUserMovedResizedSpy.isValid()); QSignalSpy clientFinishUserMovedResizedSpy(client, &AbstractClient::clientFinishUserMovedResized); QVERIFY(clientFinishUserMovedResizedSpy.isValid()); // Start interactively resizing the client. QCOMPARE(workspace()->moveResizeClient(), nullptr); workspace()->slotWindowResize(); QCOMPARE(workspace()->moveResizeClient(), client); QCOMPARE(clientStartMoveResizedSpy.count(), 1); QVERIFY(configureRequestedSpy.wait()); QCOMPARE(configureRequestedSpy.count(), 2); XdgShellSurface::States states = configureRequestedSpy.last().at(1).value(); QVERIFY(states.testFlag(XdgShellSurface::State::Resizing)); // Go right. QPoint cursorPos = KWin::Cursor::pos(); client->keyPressEvent(Qt::Key_Right); client->updateMoveResize(KWin::Cursor::pos()); QCOMPARE(KWin::Cursor::pos(), cursorPos + QPoint(8, 0)); QVERIFY(configureRequestedSpy.wait()); QCOMPARE(configureRequestedSpy.count(), 3); states = configureRequestedSpy.last().at(1).value(); QVERIFY(states.testFlag(XdgShellSurface::State::Resizing)); QCOMPARE(configureRequestedSpy.last().at(0).toSize(), QSize(188, 80)); shellSurface->setWindowGeometry(QRect(10, 10, 188, 80)); shellSurface->ackConfigure(configureRequestedSpy.last().at(2).value()); Test::render(surface.data(), QSize(208, 100), Qt::blue); - QVERIFY(geometryChangedSpy.wait()); + QVERIFY(frameGeometryChangedSpy.wait()); QCOMPARE(clientStepUserMovedResizedSpy.count(), 1); QCOMPARE(client->bufferGeometry().size(), QSize(208, 100)); QCOMPARE(client->frameGeometry().size(), QSize(188, 80)); // Go down. cursorPos = KWin::Cursor::pos(); client->keyPressEvent(Qt::Key_Down); client->updateMoveResize(KWin::Cursor::pos()); QCOMPARE(KWin::Cursor::pos(), cursorPos + QPoint(0, 8)); QVERIFY(configureRequestedSpy.wait()); QCOMPARE(configureRequestedSpy.count(), 4); states = configureRequestedSpy.last().at(1).value(); QVERIFY(states.testFlag(XdgShellSurface::State::Resizing)); QCOMPARE(configureRequestedSpy.last().at(0).toSize(), QSize(188, 88)); shellSurface->setWindowGeometry(QRect(10, 10, 188, 88)); shellSurface->ackConfigure(configureRequestedSpy.last().at(2).value()); Test::render(surface.data(), QSize(208, 108), Qt::blue); - QVERIFY(geometryChangedSpy.wait()); + QVERIFY(frameGeometryChangedSpy.wait()); QCOMPARE(clientStepUserMovedResizedSpy.count(), 2); QCOMPARE(client->bufferGeometry().size(), QSize(208, 108)); QCOMPARE(client->frameGeometry().size(), QSize(188, 88)); // Finish resizing the client. client->keyPressEvent(Qt::Key_Enter); QCOMPARE(clientFinishUserMovedResizedSpy.count(), 1); QCOMPARE(workspace()->moveResizeClient(), nullptr); #if 0 QEXPECT_FAIL("", "XdgShellClient currently doesn't send final configure event", Abort); QVERIFY(configureRequestedSpy.wait()); QCOMPARE(configureRequestedSpy.count(), 5); states = configureRequestedSpy.last().at(1).value(); QVERIFY(!states.testFlag(XdgShellSurface::State::Resizing)); #endif shellSurface.reset(); QVERIFY(Test::waitForWindowDestroyed(client)); } void TestXdgShellClient::testXdgWindowGeometryFullScreen() { // This test verifies that an xdg-shell receives correct window geometry when // its fullscreen state gets changed. QScopedPointer surface(Test::createSurface()); QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); XdgShellClient *client = Test::renderAndWaitForShown(surface.data(), QSize(200, 100), Qt::red); QVERIFY(client); QVERIFY(client->isActive()); QCOMPARE(client->bufferGeometry().size(), QSize(200, 100)); QCOMPARE(client->frameGeometry().size(), QSize(200, 100)); QSignalSpy configureRequestedSpy(shellSurface.data(), &XdgShellSurface::configureRequested); QVERIFY(configureRequestedSpy.isValid()); QVERIFY(configureRequestedSpy.wait()); QCOMPARE(configureRequestedSpy.count(), 1); - QSignalSpy geometryChangedSpy(client, &AbstractClient::geometryChanged); - QVERIFY(geometryChangedSpy.isValid()); + QSignalSpy frameGeometryChangedSpy(client, &AbstractClient::frameGeometryChanged); + QVERIFY(frameGeometryChangedSpy.isValid()); shellSurface->setWindowGeometry(QRect(10, 10, 180, 80)); surface->commit(Surface::CommitFlag::None); - QVERIFY(geometryChangedSpy.wait()); + QVERIFY(frameGeometryChangedSpy.wait()); QCOMPARE(client->bufferGeometry().size(), QSize(200, 100)); QCOMPARE(client->frameGeometry().size(), QSize(180, 80)); workspace()->slotWindowFullScreen(); QVERIFY(configureRequestedSpy.wait()); QCOMPARE(configureRequestedSpy.count(), 2); QCOMPARE(configureRequestedSpy.last().at(0).toSize(), QSize(1280, 1024)); XdgShellSurface::States states = configureRequestedSpy.last().at(1).value(); QVERIFY(states.testFlag(XdgShellSurface::State::Fullscreen)); shellSurface->setWindowGeometry(QRect(0, 0, 1280, 1024)); shellSurface->ackConfigure(configureRequestedSpy.last().at(2).value()); Test::render(surface.data(), QSize(1280, 1024), Qt::blue); - QVERIFY(geometryChangedSpy.wait()); + QVERIFY(frameGeometryChangedSpy.wait()); QCOMPARE(client->bufferGeometry().size(), QSize(1280, 1024)); QCOMPARE(client->frameGeometry().size(), QSize(1280, 1024)); workspace()->slotWindowFullScreen(); QVERIFY(configureRequestedSpy.wait()); QCOMPARE(configureRequestedSpy.count(), 3); QCOMPARE(configureRequestedSpy.last().at(0).toSize(), QSize(180, 80)); states = configureRequestedSpy.last().at(1).value(); QVERIFY(!states.testFlag(XdgShellSurface::State::Fullscreen)); shellSurface->setWindowGeometry(QRect(10, 10, 180, 80)); shellSurface->ackConfigure(configureRequestedSpy.last().at(2).value()); Test::render(surface.data(), QSize(200, 100), Qt::blue); - QVERIFY(geometryChangedSpy.wait()); + QVERIFY(frameGeometryChangedSpy.wait()); QCOMPARE(client->bufferGeometry().size(), QSize(200, 100)); QCOMPARE(client->frameGeometry().size(), QSize(180, 80)); shellSurface.reset(); QVERIFY(Test::waitForWindowDestroyed(client)); } void TestXdgShellClient::testXdgWindowGeometryMaximize() { // This test verifies that an xdg-shell receives correct window geometry when // its maximized state gets changed. QScopedPointer surface(Test::createSurface()); QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); XdgShellClient *client = Test::renderAndWaitForShown(surface.data(), QSize(200, 100), Qt::red); QVERIFY(client); QVERIFY(client->isActive()); QCOMPARE(client->bufferGeometry().size(), QSize(200, 100)); QCOMPARE(client->frameGeometry().size(), QSize(200, 100)); QSignalSpy configureRequestedSpy(shellSurface.data(), &XdgShellSurface::configureRequested); QVERIFY(configureRequestedSpy.isValid()); QVERIFY(configureRequestedSpy.wait()); QCOMPARE(configureRequestedSpy.count(), 1); - QSignalSpy geometryChangedSpy(client, &AbstractClient::geometryChanged); - QVERIFY(geometryChangedSpy.isValid()); + QSignalSpy frameGeometryChangedSpy(client, &AbstractClient::frameGeometryChanged); + QVERIFY(frameGeometryChangedSpy.isValid()); shellSurface->setWindowGeometry(QRect(10, 10, 180, 80)); surface->commit(Surface::CommitFlag::None); - QVERIFY(geometryChangedSpy.wait()); + QVERIFY(frameGeometryChangedSpy.wait()); QCOMPARE(client->bufferGeometry().size(), QSize(200, 100)); QCOMPARE(client->frameGeometry().size(), QSize(180, 80)); workspace()->slotWindowMaximize(); QVERIFY(configureRequestedSpy.wait()); QCOMPARE(configureRequestedSpy.count(), 2); QCOMPARE(configureRequestedSpy.last().at(0).toSize(), QSize(1280, 1024)); XdgShellSurface::States states = configureRequestedSpy.last().at(1).value(); QVERIFY(states.testFlag(XdgShellSurface::State::Maximized)); shellSurface->setWindowGeometry(QRect(0, 0, 1280, 1024)); shellSurface->ackConfigure(configureRequestedSpy.last().at(2).value()); Test::render(surface.data(), QSize(1280, 1024), Qt::blue); - QVERIFY(geometryChangedSpy.wait()); + QVERIFY(frameGeometryChangedSpy.wait()); QCOMPARE(client->bufferGeometry().size(), QSize(1280, 1024)); QCOMPARE(client->frameGeometry().size(), QSize(1280, 1024)); workspace()->slotWindowMaximize(); QVERIFY(configureRequestedSpy.wait()); QCOMPARE(configureRequestedSpy.count(), 3); QCOMPARE(configureRequestedSpy.last().at(0).toSize(), QSize(180, 80)); states = configureRequestedSpy.last().at(1).value(); QVERIFY(!states.testFlag(XdgShellSurface::State::Maximized)); shellSurface->setWindowGeometry(QRect(10, 10, 180, 80)); shellSurface->ackConfigure(configureRequestedSpy.last().at(2).value()); Test::render(surface.data(), QSize(200, 100), Qt::blue); - QVERIFY(geometryChangedSpy.wait()); + QVERIFY(frameGeometryChangedSpy.wait()); QCOMPARE(client->bufferGeometry().size(), QSize(200, 100)); QCOMPARE(client->frameGeometry().size(), QSize(180, 80)); shellSurface.reset(); QVERIFY(Test::waitForWindowDestroyed(client)); } WAYLANDTEST_MAIN(TestXdgShellClient) #include "xdgshellclient_test.moc" diff --git a/decorations/decoratedclient.cpp b/decorations/decoratedclient.cpp index 6e150d804..3637f3a41 100644 --- a/decorations/decoratedclient.cpp +++ b/decorations/decoratedclient.cpp @@ -1,346 +1,346 @@ /******************************************************************** 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 "decoratedclient.h" #include "decorationbridge.h" #include "decorationpalette.h" #include "decorationrenderer.h" #include "abstract_client.h" #include "composite.h" #include "cursor.h" #include "options.h" #include "platform.h" #include "workspace.h" #include #include #include #include #include namespace KWin { namespace Decoration { DecoratedClientImpl::DecoratedClientImpl(AbstractClient *client, KDecoration2::DecoratedClient *decoratedClient, KDecoration2::Decoration *decoration) : QObject() , ApplicationMenuEnabledDecoratedClientPrivate(decoratedClient, decoration) , m_client(client) , m_clientSize(client->clientSize()) , m_renderer(nullptr) { createRenderer(); client->setDecoratedClient(QPointer(this)); connect(client, &AbstractClient::activeChanged, this, [decoratedClient, client]() { emit decoratedClient->activeChanged(client->isActive()); } ); - connect(client, &AbstractClient::geometryChanged, this, + connect(client, &AbstractClient::frameGeometryChanged, this, [decoratedClient, this]() { if (m_client->clientSize() == m_clientSize) { return; } const auto oldSize = m_clientSize; m_clientSize = m_client->clientSize(); if (oldSize.width() != m_clientSize.width()) { emit decoratedClient->widthChanged(m_clientSize.width()); } if (oldSize.height() != m_clientSize.height()) { emit decoratedClient->heightChanged(m_clientSize.height()); } emit decoratedClient->sizeChanged(m_clientSize); } ); connect(client, &AbstractClient::desktopChanged, this, [decoratedClient, client]() { emit decoratedClient->onAllDesktopsChanged(client->isOnAllDesktops()); } ); connect(client, &AbstractClient::captionChanged, this, [decoratedClient, client]() { emit decoratedClient->captionChanged(client->caption()); } ); connect(client, &AbstractClient::iconChanged, this, [decoratedClient, client]() { emit decoratedClient->iconChanged(client->icon()); } ); connect(client, &AbstractClient::shadeChanged, this, &Decoration::DecoratedClientImpl::signalShadeChange); connect(client, &AbstractClient::keepAboveChanged, decoratedClient, &KDecoration2::DecoratedClient::keepAboveChanged); connect(client, &AbstractClient::keepBelowChanged, decoratedClient, &KDecoration2::DecoratedClient::keepBelowChanged); connect(Compositor::self(), &Compositor::aboutToToggleCompositing, this, &DecoratedClientImpl::destroyRenderer); m_compositorToggledConnection = connect(Compositor::self(), &Compositor::compositingToggled, this, [this, decoration]() { createRenderer(); decoration->update(); } ); connect(Compositor::self(), &Compositor::aboutToDestroy, this, [this] { disconnect(m_compositorToggledConnection); m_compositorToggledConnection = QMetaObject::Connection(); } ); connect(client, &AbstractClient::quickTileModeChanged, decoratedClient, [this, decoratedClient]() { emit decoratedClient->adjacentScreenEdgesChanged(adjacentScreenEdges()); } ); connect(client, &AbstractClient::closeableChanged, decoratedClient, &KDecoration2::DecoratedClient::closeableChanged); connect(client, &AbstractClient::shadeableChanged, decoratedClient, &KDecoration2::DecoratedClient::shadeableChanged); connect(client, &AbstractClient::minimizeableChanged, decoratedClient, &KDecoration2::DecoratedClient::minimizeableChanged); connect(client, &AbstractClient::maximizeableChanged, decoratedClient, &KDecoration2::DecoratedClient::maximizeableChanged); connect(client, &AbstractClient::paletteChanged, decoratedClient, &KDecoration2::DecoratedClient::paletteChanged); connect(client, &AbstractClient::hasApplicationMenuChanged, decoratedClient, &KDecoration2::DecoratedClient::hasApplicationMenuChanged); connect(client, &AbstractClient::applicationMenuActiveChanged, decoratedClient, &KDecoration2::DecoratedClient::applicationMenuActiveChanged); m_toolTipWakeUp.setSingleShot(true); connect(&m_toolTipWakeUp, &QTimer::timeout, this, [this]() { int fallAsleepDelay = QApplication::style()->styleHint(QStyle::SH_ToolTip_FallAsleepDelay); this->m_toolTipFallAsleep.setRemainingTime(fallAsleepDelay); QToolTip::showText(Cursor::pos(), this->m_toolTipText); m_toolTipShowing = true; } ); } DecoratedClientImpl::~DecoratedClientImpl() { if (m_toolTipShowing) { requestHideToolTip(); } } void DecoratedClientImpl::signalShadeChange() { emit decoratedClient()->shadedChanged(m_client->isShade()); } #define DELEGATE(type, name, clientName) \ type DecoratedClientImpl::name() const \ { \ return m_client->clientName(); \ } #define DELEGATE2(type, name) DELEGATE(type, name, name) DELEGATE2(QString, caption) DELEGATE2(bool, isActive) DELEGATE2(bool, isCloseable) DELEGATE(bool, isMaximizeable, isMaximizable) DELEGATE(bool, isMinimizeable, isMinimizable) DELEGATE2(bool, isModal) DELEGATE(bool, isMoveable, isMovable) DELEGATE(bool, isResizeable, isResizable) DELEGATE2(bool, isShadeable) DELEGATE2(bool, providesContextHelp) DELEGATE2(int, desktop) DELEGATE2(bool, isOnAllDesktops) DELEGATE2(QPalette, palette) DELEGATE2(QIcon, icon) #undef DELEGATE2 #undef DELEGATE #define DELEGATE(type, name, clientName) \ type DecoratedClientImpl::name() const \ { \ return m_client->clientName(); \ } DELEGATE(bool, isKeepAbove, keepAbove) DELEGATE(bool, isKeepBelow, keepBelow) DELEGATE(bool, isShaded, isShade) DELEGATE(WId, windowId, windowId) DELEGATE(WId, decorationId, frameId) #undef DELEGATE #define DELEGATE(name, op) \ void DecoratedClientImpl::name() \ { \ Workspace::self()->performWindowOperation(m_client, Options::op); \ } DELEGATE(requestToggleShade, ShadeOp) DELEGATE(requestToggleOnAllDesktops, OnAllDesktopsOp) DELEGATE(requestToggleKeepAbove, KeepAboveOp) DELEGATE(requestToggleKeepBelow, KeepBelowOp) #undef DELEGATE #define DELEGATE(name, clientName) \ void DecoratedClientImpl::name() \ { \ m_client->clientName(); \ } DELEGATE(requestContextHelp, showContextHelp) DELEGATE(requestMinimize, minimize) #undef DELEGATE void DecoratedClientImpl::requestClose() { QMetaObject::invokeMethod(m_client, "closeWindow", Qt::QueuedConnection); } QColor DecoratedClientImpl::color(KDecoration2::ColorGroup group, KDecoration2::ColorRole role) const { auto dp = m_client->decorationPalette(); if (dp) { return dp->color(group, role); } return QColor(); } void DecoratedClientImpl::requestShowToolTip(const QString &text) { if (!DecorationBridge::self()->showToolTips()) { return; } m_toolTipText = text; int wakeUpDelay = QApplication::style()->styleHint(QStyle::SH_ToolTip_WakeUpDelay); m_toolTipWakeUp.start(m_toolTipFallAsleep.hasExpired() ? wakeUpDelay : 20); } void DecoratedClientImpl::requestHideToolTip() { m_toolTipWakeUp.stop(); QToolTip::hideText(); m_toolTipShowing = false; } void DecoratedClientImpl::requestShowWindowMenu() { // TODO: add rect to requestShowWindowMenu Workspace::self()->showWindowMenu(QRect(Cursor::pos(), Cursor::pos()), m_client); } void DecoratedClientImpl::requestShowApplicationMenu(const QRect &rect, int actionId) { Workspace::self()->showApplicationMenu(rect, m_client, actionId); } void DecoratedClientImpl::showApplicationMenu(int actionId) { decoration()->showApplicationMenu(actionId); } void DecoratedClientImpl::requestToggleMaximization(Qt::MouseButtons buttons) { QMetaObject::invokeMethod(this, "delayedRequestToggleMaximization", Qt::QueuedConnection, Q_ARG(Options::WindowOperation, options->operationMaxButtonClick(buttons))); } void DecoratedClientImpl::delayedRequestToggleMaximization(Options::WindowOperation operation) { Workspace::self()->performWindowOperation(m_client, operation); } int DecoratedClientImpl::width() const { return m_clientSize.width(); } int DecoratedClientImpl::height() const { return m_clientSize.height(); } QSize DecoratedClientImpl::size() const { return m_clientSize; } bool DecoratedClientImpl::isMaximizedVertically() const { return m_client->requestedMaximizeMode() & MaximizeVertical; } bool DecoratedClientImpl::isMaximized() const { return isMaximizedHorizontally() && isMaximizedVertically(); } bool DecoratedClientImpl::isMaximizedHorizontally() const { return m_client->requestedMaximizeMode() & MaximizeHorizontal; } Qt::Edges DecoratedClientImpl::adjacentScreenEdges() const { Qt::Edges edges; const QuickTileMode mode = m_client->quickTileMode(); if (mode.testFlag(QuickTileFlag::Left)) { edges |= Qt::LeftEdge; if (!mode.testFlag(QuickTileFlag::Top) && !mode.testFlag(QuickTileFlag::Bottom)) { // using complete side edges |= Qt::TopEdge | Qt::BottomEdge; } } if (mode.testFlag(QuickTileFlag::Top)) { edges |= Qt::TopEdge; } if (mode.testFlag(QuickTileFlag::Right)) { edges |= Qt::RightEdge; if (!mode.testFlag(QuickTileFlag::Top) && !mode.testFlag(QuickTileFlag::Bottom)) { // using complete side edges |= Qt::TopEdge | Qt::BottomEdge; } } if (mode.testFlag(QuickTileFlag::Bottom)) { edges |= Qt::BottomEdge; } return edges; } bool DecoratedClientImpl::hasApplicationMenu() const { return m_client->hasApplicationMenu(); } bool DecoratedClientImpl::isApplicationMenuActive() const { return m_client->applicationMenuActive(); } void DecoratedClientImpl::createRenderer() { m_renderer = kwinApp()->platform()->createDecorationRenderer(this); } void DecoratedClientImpl::destroyRenderer() { delete m_renderer; m_renderer = nullptr; } } } diff --git a/events.cpp b/events.cpp index 0b919646f..0cd659a58 100644 --- a/events.cpp +++ b/events.cpp @@ -1,1356 +1,1356 @@ /******************************************************************** KWin - the KDE window manager This file is part of the KDE project. Copyright (C) 1999, 2000 Matthias Ettrich Copyright (C) 2003 Lubos Lunak This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . *********************************************************************/ /* This file contains things relevant to handling incoming events. */ #include "x11client.h" #include "cursor.h" #include "focuschain.h" #include "netinfo.h" #include "workspace.h" #include "atoms.h" #ifdef KWIN_BUILD_TABBOX #include "tabbox.h" #endif #include "group.h" #include "rules.h" #include "unmanaged.h" #include "useractions.h" #include "effects.h" #include "screens.h" #include "xcbutils.h" #include #include #include #include #include #include #include #include #include #include #ifdef XCB_ICCCM_FOUND #include #endif #include "composite.h" #include "x11eventfilter.h" #include "wayland_server.h" #include #ifndef XCB_GE_GENERIC #define XCB_GE_GENERIC 35 typedef struct xcb_ge_generic_event_t { uint8_t response_type; /**< */ uint8_t extension; /**< */ uint16_t sequence; /**< */ uint32_t length; /**< */ uint16_t event_type; /**< */ uint8_t pad0[22]; /**< */ uint32_t full_sequence; /**< */ } xcb_ge_generic_event_t; #endif namespace KWin { // **************************************** // Workspace // **************************************** static xcb_window_t findEventWindow(xcb_generic_event_t *event) { const uint8_t eventType = event->response_type & ~0x80; switch(eventType) { case XCB_KEY_PRESS: case XCB_KEY_RELEASE: return reinterpret_cast(event)->event; case XCB_BUTTON_PRESS: case XCB_BUTTON_RELEASE: return reinterpret_cast(event)->event; case XCB_MOTION_NOTIFY: return reinterpret_cast(event)->event; case XCB_ENTER_NOTIFY: case XCB_LEAVE_NOTIFY: return reinterpret_cast(event)->event; case XCB_FOCUS_IN: case XCB_FOCUS_OUT: return reinterpret_cast(event)->event; case XCB_EXPOSE: return reinterpret_cast(event)->window; case XCB_GRAPHICS_EXPOSURE: return reinterpret_cast(event)->drawable; case XCB_NO_EXPOSURE: return reinterpret_cast(event)->drawable; case XCB_VISIBILITY_NOTIFY: return reinterpret_cast(event)->window; case XCB_CREATE_NOTIFY: return reinterpret_cast(event)->window; case XCB_DESTROY_NOTIFY: return reinterpret_cast(event)->window; case XCB_UNMAP_NOTIFY: return reinterpret_cast(event)->window; case XCB_MAP_NOTIFY: return reinterpret_cast(event)->window; case XCB_MAP_REQUEST: return reinterpret_cast(event)->window; case XCB_REPARENT_NOTIFY: return reinterpret_cast(event)->window; case XCB_CONFIGURE_NOTIFY: return reinterpret_cast(event)->window; case XCB_CONFIGURE_REQUEST: return reinterpret_cast(event)->window; case XCB_GRAVITY_NOTIFY: return reinterpret_cast(event)->window; case XCB_RESIZE_REQUEST: return reinterpret_cast(event)->window; case XCB_CIRCULATE_NOTIFY: case XCB_CIRCULATE_REQUEST: return reinterpret_cast(event)->window; case XCB_PROPERTY_NOTIFY: return reinterpret_cast(event)->window; case XCB_COLORMAP_NOTIFY: return reinterpret_cast(event)->window; case XCB_CLIENT_MESSAGE: return reinterpret_cast(event)->window; default: // extension handling if (eventType == Xcb::Extensions::self()->shapeNotifyEvent()) { return reinterpret_cast(event)->affected_window; } if (eventType == Xcb::Extensions::self()->damageNotifyEvent()) { return reinterpret_cast(event)->drawable; } return XCB_WINDOW_NONE; } } QVector s_xcbEerrors({ QByteArrayLiteral("Success"), QByteArrayLiteral("BadRequest"), QByteArrayLiteral("BadValue"), QByteArrayLiteral("BadWindow"), QByteArrayLiteral("BadPixmap"), QByteArrayLiteral("BadAtom"), QByteArrayLiteral("BadCursor"), QByteArrayLiteral("BadFont"), QByteArrayLiteral("BadMatch"), QByteArrayLiteral("BadDrawable"), QByteArrayLiteral("BadAccess"), QByteArrayLiteral("BadAlloc"), QByteArrayLiteral("BadColor"), QByteArrayLiteral("BadGC"), QByteArrayLiteral("BadIDChoice"), QByteArrayLiteral("BadName"), QByteArrayLiteral("BadLength"), QByteArrayLiteral("BadImplementation"), QByteArrayLiteral("Unknown")}); void Workspace::registerEventFilter(X11EventFilter *filter) { if (filter->isGenericEvent()) m_genericEventFilters.append(filter); else m_eventFilters.append(filter); } void Workspace::unregisterEventFilter(X11EventFilter *filter) { if (filter->isGenericEvent()) m_genericEventFilters.removeOne(filter); else m_eventFilters.removeOne(filter); } /** * Handles workspace specific XCB event */ bool Workspace::workspaceEvent(xcb_generic_event_t *e) { const uint8_t eventType = e->response_type & ~0x80; if (!eventType) { // let's check whether it's an error from one of the extensions KWin uses xcb_generic_error_t *error = reinterpret_cast(e); const QVector extensions = Xcb::Extensions::self()->extensions(); for (const auto &extension : extensions) { if (error->major_code == extension.majorOpcode) { QByteArray errorName; if (error->error_code < s_xcbEerrors.size()) { errorName = s_xcbEerrors.at(error->error_code); } else if (error->error_code >= extension.errorBase) { const int index = error->error_code - extension.errorBase; if (index >= 0 && index < extension.errorCodes.size()) { errorName = extension.errorCodes.at(index); } } if (errorName.isEmpty()) { errorName = QByteArrayLiteral("Unknown"); } qCWarning(KWIN_CORE, "XCB error: %d (%s), sequence: %d, resource id: %d, major code: %d (%s), minor code: %d (%s)", int(error->error_code), errorName.constData(), int(error->sequence), int(error->resource_id), int(error->major_code), extension.name.constData(), int(error->minor_code), extension.opCodes.size() > error->minor_code ? extension.opCodes.at(error->minor_code).constData() : "Unknown"); return true; } } return false; } if (eventType == XCB_GE_GENERIC) { xcb_ge_generic_event_t *ge = reinterpret_cast(e); foreach (X11EventFilter *filter, m_genericEventFilters) { if (filter->extension() == ge->extension && filter->genericEventTypes().contains(ge->event_type) && filter->event(e)) { return true; } } } else { foreach (X11EventFilter *filter, m_eventFilters) { if (filter->eventTypes().contains(eventType) && filter->event(e)) { return true; } } } if (effects && static_cast< EffectsHandlerImpl* >(effects)->hasKeyboardGrab() && (eventType == XCB_KEY_PRESS || eventType == XCB_KEY_RELEASE)) return false; // let Qt process it, it'll be intercepted again in eventFilter() // events that should be handled before Clients can get them switch (eventType) { case XCB_CONFIGURE_NOTIFY: if (reinterpret_cast(e)->event == rootWindow()) markXStackingOrderAsDirty(); break; }; const xcb_window_t eventWindow = findEventWindow(e); if (eventWindow != XCB_WINDOW_NONE) { if (X11Client *c = findClient(Predicate::WindowMatch, eventWindow)) { if (c->windowEvent(e)) return true; } else if (X11Client *c = findClient(Predicate::WrapperIdMatch, eventWindow)) { if (c->windowEvent(e)) return true; } else if (X11Client *c = findClient(Predicate::FrameIdMatch, eventWindow)) { if (c->windowEvent(e)) return true; } else if (X11Client *c = findClient(Predicate::InputIdMatch, eventWindow)) { if (c->windowEvent(e)) return true; } else if (Unmanaged* c = findUnmanaged(eventWindow)) { if (c->windowEvent(e)) return true; } } switch (eventType) { case XCB_CREATE_NOTIFY: { const auto *event = reinterpret_cast(e); if (event->parent == rootWindow() && !QWidget::find(event->window) && !event->override_redirect) { // see comments for allowClientActivation() updateXTime(); const xcb_timestamp_t t = xTime(); xcb_change_property(connection(), XCB_PROP_MODE_REPLACE, event->window, atoms->kde_net_wm_user_creation_time, XCB_ATOM_CARDINAL, 32, 1, &t); } break; } case XCB_UNMAP_NOTIFY: { const auto *event = reinterpret_cast(e); return (event->event != event->window); // hide wm typical event from Qt } case XCB_REPARENT_NOTIFY: { //do not confuse Qt with these events. After all, _we_ are the //window manager who does the reparenting. return true; } case XCB_MAP_REQUEST: { updateXTime(); const auto *event = reinterpret_cast(e); if (X11Client *c = findClient(Predicate::WindowMatch, event->window)) { // e->xmaprequest.window is different from e->xany.window // TODO this shouldn't be necessary now c->windowEvent(e); FocusChain::self()->update(c, FocusChain::Update); } else if ( true /*|| e->xmaprequest.parent != root */ ) { // NOTICE don't check for the parent being the root window, this breaks when some app unmaps // a window, changes something and immediately maps it back, without giving KWin // a chance to reparent it back to root // since KWin can get MapRequest only for root window children and // children of WindowWrapper (=clients), the check is AFAIK useless anyway // NOTICE: The save-set support in X11Client::mapRequestEvent() actually requires that // this code doesn't check the parent to be root. if (!createClient(event->window, false)) { xcb_map_window(connection(), event->window); const uint32_t values[] = { XCB_STACK_MODE_ABOVE }; xcb_configure_window(connection(), event->window, XCB_CONFIG_WINDOW_STACK_MODE, values); } } return true; } case XCB_MAP_NOTIFY: { const auto *event = reinterpret_cast(e); if (event->override_redirect) { Unmanaged* c = findUnmanaged(event->window); if (c == nullptr) c = createUnmanaged(event->window); if (c) { // if hasScheduledRelease is true, it means a unamp and map sequence has occurred. // since release is scheduled after map notify, this old Unmanaged will get released // before KWIN has chance to remanage it again. so release it right now. if (c->hasScheduledRelease()) { c->release(); c = createUnmanaged(event->window); } if (c) return c->windowEvent(e); } } return (event->event != event->window); // hide wm typical event from Qt } case XCB_CONFIGURE_REQUEST: { const auto *event = reinterpret_cast(e); if (event->parent == rootWindow()) { uint32_t values[5] = { 0, 0, 0, 0, 0}; const uint32_t value_mask = event->value_mask & (XCB_CONFIG_WINDOW_X | XCB_CONFIG_WINDOW_Y | XCB_CONFIG_WINDOW_WIDTH | XCB_CONFIG_WINDOW_HEIGHT | XCB_CONFIG_WINDOW_BORDER_WIDTH); int i = 0; if (value_mask & XCB_CONFIG_WINDOW_X) { values[i++] = event->x; } if (value_mask & XCB_CONFIG_WINDOW_Y) { values[i++] = event->y; } if (value_mask & XCB_CONFIG_WINDOW_WIDTH) { values[i++] = event->width; } if (value_mask & XCB_CONFIG_WINDOW_HEIGHT) { values[i++] = event->height; } if (value_mask & XCB_CONFIG_WINDOW_BORDER_WIDTH) { values[i++] = event->border_width; } xcb_configure_window(connection(), event->window, value_mask, values); return true; } break; } case XCB_FOCUS_IN: { const auto *event = reinterpret_cast(e); if (event->event == rootWindow() && (event->detail == XCB_NOTIFY_DETAIL_NONE || event->detail == XCB_NOTIFY_DETAIL_POINTER_ROOT || event->detail == XCB_NOTIFY_DETAIL_INFERIOR)) { Xcb::CurrentInput currentInput; updateXTime(); // focusToNull() uses xTime(), which is old now (FocusIn has no timestamp) // it seems we can "loose" focus reversions when the closing client hold a grab // => catch the typical pattern (though we don't want the focus on the root anyway) #348935 const bool lostFocusPointerToRoot = currentInput->focus == rootWindow() && event->detail == XCB_NOTIFY_DETAIL_INFERIOR; if (!currentInput.isNull() && (currentInput->focus == XCB_WINDOW_NONE || currentInput->focus == XCB_INPUT_FOCUS_POINTER_ROOT || lostFocusPointerToRoot)) { //kWarning( 1212 ) << "X focus set to None/PointerRoot, reseting focus" ; AbstractClient *c = mostRecentlyActivatedClient(); if (c != nullptr) requestFocus(c, true); else if (activateNextClient(nullptr)) ; // ok, activated else focusToNull(); } } } // fall through case XCB_FOCUS_OUT: return true; // always eat these, they would tell Qt that KWin is the active app default: break; } return false; } // Used only to filter events that need to be processed by Qt first // (e.g. keyboard input to be composed), otherwise events are // handle by the XEvent filter above bool Workspace::workspaceEvent(QEvent* e) { if ((e->type() == QEvent::KeyPress || e->type() == QEvent::KeyRelease || e->type() == QEvent::ShortcutOverride) && effects && static_cast< EffectsHandlerImpl* >(effects)->hasKeyboardGrab()) { static_cast< EffectsHandlerImpl* >(effects)->grabbedKeyboardEvent(static_cast< QKeyEvent* >(e)); return true; } return false; } // **************************************** // Client // **************************************** /** * General handler for XEvents concerning the client window */ bool X11Client::windowEvent(xcb_generic_event_t *e) { if (findEventWindow(e) == window()) { // avoid doing stuff on frame or wrapper NET::Properties dirtyProperties; NET::Properties2 dirtyProperties2; double old_opacity = opacity(); info->event(e, &dirtyProperties, &dirtyProperties2); // pass through the NET stuff if ((dirtyProperties & NET::WMName) != 0) fetchName(); if ((dirtyProperties & NET::WMIconName) != 0) fetchIconicName(); if ((dirtyProperties & NET::WMStrut) != 0 || (dirtyProperties2 & NET::WM2ExtendedStrut) != 0) { workspace()->updateClientArea(); } if ((dirtyProperties & NET::WMIcon) != 0) getIcons(); // Note there's a difference between userTime() and info->userTime() // info->userTime() is the value of the property, userTime() also includes // updates of the time done by KWin (ButtonPress on windowrapper etc.). if ((dirtyProperties2 & NET::WM2UserTime) != 0) { workspace()->setWasUserInteraction(); updateUserTime(info->userTime()); } if ((dirtyProperties2 & NET::WM2StartupId) != 0) startupIdChanged(); if (dirtyProperties2 & NET::WM2Opacity) { if (compositing()) { addRepaintFull(); emit opacityChanged(this, old_opacity); } else { // forward to the frame if there's possibly another compositing manager running NETWinInfo i(connection(), frameId(), rootWindow(), NET::Properties(), NET::Properties2()); i.setOpacity(info->opacity()); } } if (dirtyProperties2 & NET::WM2FrameOverlap) { // ### Inform the decoration } if (dirtyProperties2.testFlag(NET::WM2WindowRole)) { emit windowRoleChanged(); } if (dirtyProperties2.testFlag(NET::WM2WindowClass)) { getResourceClass(); } if (dirtyProperties2.testFlag(NET::WM2BlockCompositing)) { setBlockingCompositing(info->isBlockingCompositing()); } if (dirtyProperties2.testFlag(NET::WM2GroupLeader)) { checkGroup(); updateAllowedActions(); // Group affects isMinimizable() } if (dirtyProperties2.testFlag(NET::WM2Urgency)) { updateUrgency(); } if (dirtyProperties2 & NET::WM2OpaqueRegion) { getWmOpaqueRegion(); } if (dirtyProperties2 & NET::WM2DesktopFileName) { setDesktopFileName(QByteArray(info->desktopFileName())); } if (dirtyProperties2 & NET::WM2GTKFrameExtents) { setClientFrameExtents(info->gtkFrameExtents()); } } const uint8_t eventType = e->response_type & ~0x80; switch(eventType) { case XCB_UNMAP_NOTIFY: unmapNotifyEvent(reinterpret_cast(e)); break; case XCB_DESTROY_NOTIFY: destroyNotifyEvent(reinterpret_cast(e)); break; case XCB_MAP_REQUEST: // this one may pass the event to workspace return mapRequestEvent(reinterpret_cast(e)); case XCB_CONFIGURE_REQUEST: configureRequestEvent(reinterpret_cast(e)); break; case XCB_PROPERTY_NOTIFY: propertyNotifyEvent(reinterpret_cast(e)); break; case XCB_KEY_PRESS: updateUserTime(reinterpret_cast(e)->time); break; case XCB_BUTTON_PRESS: { const auto *event = reinterpret_cast(e); updateUserTime(event->time); buttonPressEvent(event->event, event->detail, event->state, event->event_x, event->event_y, event->root_x, event->root_y, event->time); break; } case XCB_KEY_RELEASE: // don't update user time on releases // e.g. if the user presses Alt+F2, the Alt release // would appear as user input to the currently active window break; case XCB_BUTTON_RELEASE: { const auto *event = reinterpret_cast(e); // don't update user time on releases // e.g. if the user presses Alt+F2, the Alt release // would appear as user input to the currently active window buttonReleaseEvent(event->event, event->detail, event->state, event->event_x, event->event_y, event->root_x, event->root_y); break; } case XCB_MOTION_NOTIFY: { const auto *event = reinterpret_cast(e); motionNotifyEvent(event->event, event->state, event->event_x, event->event_y, event->root_x, event->root_y); workspace()->updateFocusMousePosition(QPoint(event->root_x, event->root_y)); break; } case XCB_ENTER_NOTIFY: { auto *event = reinterpret_cast(e); enterNotifyEvent(event); // MotionNotify is guaranteed to be generated only if the mouse // move start and ends in the window; for cases when it only // starts or only ends there, Enter/LeaveNotify are generated. // Fake a MotionEvent in such cases to make handle of mouse // events simpler (Qt does that too). motionNotifyEvent(event->event, event->state, event->event_x, event->event_y, event->root_x, event->root_y); workspace()->updateFocusMousePosition(QPoint(event->root_x, event->root_y)); break; } case XCB_LEAVE_NOTIFY: { auto *event = reinterpret_cast(e); motionNotifyEvent(event->event, event->state, event->event_x, event->event_y, event->root_x, event->root_y); leaveNotifyEvent(event); // not here, it'd break following enter notify handling // workspace()->updateFocusMousePosition( QPoint( e->xcrossing.x_root, e->xcrossing.y_root )); break; } case XCB_FOCUS_IN: focusInEvent(reinterpret_cast(e)); break; case XCB_FOCUS_OUT: focusOutEvent(reinterpret_cast(e)); break; case XCB_REPARENT_NOTIFY: break; case XCB_CLIENT_MESSAGE: clientMessageEvent(reinterpret_cast(e)); break; case XCB_EXPOSE: { xcb_expose_event_t *event = reinterpret_cast(e); if (event->window == frameId() && !Compositor::self()->isActive()) { // TODO: only repaint required areas triggerDecorationRepaint(); } break; } default: if (eventType == Xcb::Extensions::self()->shapeNotifyEvent() && reinterpret_cast(e)->affected_window == window()) { detectShape(window()); // workaround for #19644 updateShape(); } if (eventType == Xcb::Extensions::self()->damageNotifyEvent() && reinterpret_cast(e)->drawable == frameId()) damageNotifyEvent(); break; } return true; // eat all events } /** * Handles map requests of the client window */ bool X11Client::mapRequestEvent(xcb_map_request_event_t *e) { if (e->window != window()) { // Special support for the save-set feature, which is a bit broken. // If there's a window from one client embedded in another one, // e.g. using XEMBED, and the embedder suddenly loses its X connection, // save-set will reparent the embedded window to its closest ancestor // that will remains. Unfortunately, with reparenting window managers, // this is not the root window, but the frame (or in KWin's case, // it's the wrapper for the client window). In this case, // the wrapper will get ReparentNotify for a window it won't know, // which will be ignored, and then it gets MapRequest, as save-set // always maps. Returning true here means that Workspace::workspaceEvent() // will handle this MapRequest and manage this window (i.e. act as if // it was reparented to root window). if (e->parent == wrapperId()) return false; return true; // no messing with frame etc. } // also copied in clientMessage() if (isMinimized()) unminimize(); if (isShade()) setShade(ShadeNone); if (!isOnCurrentDesktop()) { if (workspace()->allowClientActivation(this)) workspace()->activateClient(this); else demandAttention(); } return true; } /** * Handles unmap notify events of the client window */ void X11Client::unmapNotifyEvent(xcb_unmap_notify_event_t *e) { if (e->window != window()) return; if (e->event != wrapperId()) { // most probably event from root window when initially reparenting bool ignore = true; if (e->event == rootWindow() && (e->response_type & 0x80)) ignore = false; // XWithdrawWindow() if (ignore) return; } // check whether this is result of an XReparentWindow - client then won't be parented by wrapper // in this case do not release the client (causes reparent to root, removal from saveSet and what not) // but just destroy the client Xcb::Tree tree(m_client); xcb_window_t daddy = tree.parent(); if (daddy == m_wrapper) { releaseWindow(); // unmapped from a regular client state } else { destroyClient(); // the client was moved to some other parent } } void X11Client::destroyNotifyEvent(xcb_destroy_notify_event_t *e) { if (e->window != window()) return; destroyClient(); } /** * Handles client messages for the client window */ void X11Client::clientMessageEvent(xcb_client_message_event_t *e) { Toplevel::clientMessageEvent(e); if (e->window != window()) return; // ignore frame/wrapper // WM_STATE if (e->type == atoms->wm_change_state) { if (e->data.data32[0] == XCB_ICCCM_WM_STATE_ICONIC) minimize(); return; } } /** * Handles configure requests of the client window */ void X11Client::configureRequestEvent(xcb_configure_request_event_t *e) { if (e->window != window()) return; // ignore frame/wrapper if (isResize() || isMove()) return; // we have better things to do right now if (m_fullscreenMode == FullScreenNormal) { // refuse resizing of fullscreen windows // but allow resizing fullscreen hacks in order to let them cancel fullscreen mode sendSyntheticConfigureNotify(); return; } if (isSplash()) { // no manipulations with splashscreens either sendSyntheticConfigureNotify(); return; } if (e->value_mask & XCB_CONFIG_WINDOW_BORDER_WIDTH) { // first, get rid of a window border m_client.setBorderWidth(0); } if (e->value_mask & (XCB_CONFIG_WINDOW_X | XCB_CONFIG_WINDOW_Y | XCB_CONFIG_WINDOW_HEIGHT | XCB_CONFIG_WINDOW_WIDTH)) configureRequest(e->value_mask, e->x, e->y, e->width, e->height, 0, false); if (e->value_mask & XCB_CONFIG_WINDOW_STACK_MODE) restackWindow(e->sibling, e->stack_mode, NET::FromApplication, userTime(), false); // Sending a synthetic configure notify always is fine, even in cases where // the ICCCM doesn't require this - it can be though of as 'the WM decided to move // the window later'. The client should not cause that many configure request, // so this should not have any significant impact. With user moving/resizing // the it should be optimized though (see also X11Client::setGeometry()/plainResize()/move()). sendSyntheticConfigureNotify(); // SELI TODO accept configure requests for isDesktop windows (because kdesktop // may get XRANDR resize event before kwin), but check it's still at the bottom? } /** * Handles property changes of the client window */ void X11Client::propertyNotifyEvent(xcb_property_notify_event_t *e) { Toplevel::propertyNotifyEvent(e); if (e->window != window()) return; // ignore frame/wrapper switch(e->atom) { case XCB_ATOM_WM_NORMAL_HINTS: getWmNormalHints(); break; case XCB_ATOM_WM_NAME: fetchName(); break; case XCB_ATOM_WM_ICON_NAME: fetchIconicName(); break; case XCB_ATOM_WM_TRANSIENT_FOR: readTransient(); break; case XCB_ATOM_WM_HINTS: getIcons(); // because KWin::icon() uses WMHints as fallback break; default: if (e->atom == atoms->motif_wm_hints) { getMotifHints(); } else if (e->atom == atoms->net_wm_sync_request_counter) getSyncCounter(); else if (e->atom == atoms->activities) checkActivities(); else if (e->atom == atoms->kde_first_in_window_list) updateFirstInTabBox(); else if (e->atom == atoms->kde_color_sheme) updateColorScheme(); else if (e->atom == atoms->kde_screen_edge_show) updateShowOnScreenEdge(); else if (e->atom == atoms->kde_net_wm_appmenu_service_name) checkApplicationMenuServiceName(); else if (e->atom == atoms->kde_net_wm_appmenu_object_path) checkApplicationMenuObjectPath(); break; } } void X11Client::enterNotifyEvent(xcb_enter_notify_event_t *e) { if (e->event != frameId()) return; // care only about entering the whole frame #define MOUSE_DRIVEN_FOCUS (!options->focusPolicyIsReasonable() || \ (options->focusPolicy() == Options::FocusFollowsMouse && options->isNextFocusPrefersMouse())) if (e->mode == XCB_NOTIFY_MODE_NORMAL || (e->mode == XCB_NOTIFY_MODE_UNGRAB && MOUSE_DRIVEN_FOCUS)) { if (options->isShadeHover()) { cancelShadeHoverTimer(); if (isShade()) { shadeHoverTimer = new QTimer(this); connect(shadeHoverTimer, SIGNAL(timeout()), this, SLOT(shadeHover())); shadeHoverTimer->setSingleShot(true); shadeHoverTimer->start(options->shadeHoverInterval()); } } #undef MOUSE_DRIVEN_FOCUS enterEvent(QPoint(e->root_x, e->root_y)); return; } } void X11Client::leaveNotifyEvent(xcb_leave_notify_event_t *e) { if (e->event != frameId()) return; // care only about leaving the whole frame if (e->mode == XCB_NOTIFY_MODE_NORMAL) { if (!isMoveResizePointerButtonDown()) { setMoveResizePointerMode(PositionCenter); updateCursor(); } bool lostMouse = !rect().contains(QPoint(e->event_x, e->event_y)); // 'lostMouse' wouldn't work with e.g. B2 or Keramik, which have non-rectangular decorations // (i.e. the LeaveNotify event comes before leaving the rect and no LeaveNotify event // comes after leaving the rect) - so lets check if the pointer is really outside the window // TODO this still sucks if a window appears above this one - it should lose the mouse // if this window is another client, but not if it's a popup ... maybe after KDE3.1 :( // (repeat after me 'AARGHL!') if (!lostMouse && e->detail != XCB_NOTIFY_DETAIL_INFERIOR) { Xcb::Pointer pointer(frameId()); if (!pointer || !pointer->same_screen || pointer->child == XCB_WINDOW_NONE) { // really lost the mouse lostMouse = true; } } if (lostMouse) { leaveEvent(); cancelShadeHoverTimer(); if (shade_mode == ShadeHover && !isMoveResize() && !isMoveResizePointerButtonDown()) { shadeHoverTimer = new QTimer(this); connect(shadeHoverTimer, SIGNAL(timeout()), this, SLOT(shadeUnhover())); shadeHoverTimer->setSingleShot(true); shadeHoverTimer->start(options->shadeHoverInterval()); } if (isDecorated()) { // sending a move instead of a leave. With leave we need to send proper coords, with move it's handled internally QHoverEvent leaveEvent(QEvent::HoverMove, QPointF(-1, -1), QPointF(-1, -1), Qt::NoModifier); QCoreApplication::sendEvent(decoration(), &leaveEvent); } } if (options->focusPolicy() == Options::FocusStrictlyUnderMouse && isActive() && lostMouse) { workspace()->requestDelayFocus(nullptr); } return; } } #define XCapL KKeyServer::modXLock() #define XNumL KKeyServer::modXNumLock() #define XScrL KKeyServer::modXScrollLock() void X11Client::grabButton(int modifier) { unsigned int mods[ 8 ] = { 0, XCapL, XNumL, XNumL | XCapL, XScrL, XScrL | XCapL, XScrL | XNumL, XScrL | XNumL | XCapL }; for (int i = 0; i < 8; ++i) m_wrapper.grabButton(XCB_GRAB_MODE_SYNC, XCB_GRAB_MODE_ASYNC, modifier | mods[ i ]); } void X11Client::ungrabButton(int modifier) { unsigned int mods[ 8 ] = { 0, XCapL, XNumL, XNumL | XCapL, XScrL, XScrL | XCapL, XScrL | XNumL, XScrL | XNumL | XCapL }; for (int i = 0; i < 8; ++i) m_wrapper.ungrabButton(modifier | mods[ i ]); } #undef XCapL #undef XNumL #undef XScrL /** * Releases the passive grab for some modifier combinations when a * window becomes active. This helps broken X programs that * missinterpret LeaveNotify events in grab mode to work properly * (Motif, AWT, Tk, ...) */ void X11Client::updateMouseGrab() { if (workspace()->globalShortcutsDisabled()) { m_wrapper.ungrabButton(); // keep grab for the simple click without modifiers if needed (see below) bool not_obscured = workspace()->topClientOnDesktop(VirtualDesktopManager::self()->current(), -1, true, false) == this; if (!(!options->isClickRaise() || not_obscured)) grabButton(XCB_NONE); return; } if (isActive() && !TabBox::TabBox::self()->forcedGlobalMouseGrab()) { // see TabBox::establishTabBoxGrab() // first grab all modifier combinations m_wrapper.grabButton(XCB_GRAB_MODE_SYNC, XCB_GRAB_MODE_ASYNC); // remove the grab for no modifiers only if the window // is unobscured or if the user doesn't want click raise // (it is unobscured if it the topmost in the unconstrained stacking order, i.e. it is // the most recently raised window) bool not_obscured = workspace()->topClientOnDesktop(VirtualDesktopManager::self()->current(), -1, true, false) == this; if (!options->isClickRaise() || not_obscured) ungrabButton(XCB_NONE); else grabButton(XCB_NONE); ungrabButton(XCB_MOD_MASK_SHIFT); ungrabButton(XCB_MOD_MASK_CONTROL); ungrabButton(XCB_MOD_MASK_CONTROL | XCB_MOD_MASK_SHIFT); } else { m_wrapper.ungrabButton(); // simply grab all modifier combinations m_wrapper.grabButton(XCB_GRAB_MODE_SYNC, XCB_GRAB_MODE_ASYNC); } } static bool modKeyDown(int state) { const uint keyModX = (options->keyCmdAllModKey() == Qt::Key_Meta) ? KKeyServer::modXMeta() : KKeyServer::modXAlt(); return keyModX && (state & KKeyServer::accelModMaskX()) == keyModX; } // return value matters only when filtering events before decoration gets them bool X11Client::buttonPressEvent(xcb_window_t w, int button, int state, int x, int y, int x_root, int y_root, xcb_timestamp_t time) { if (isMoveResizePointerButtonDown()) { if (w == wrapperId()) xcb_allow_events(connection(), XCB_ALLOW_SYNC_POINTER, XCB_TIME_CURRENT_TIME); //xTime()); return true; } if (w == wrapperId() || w == frameId() || w == inputId()) { // FRAME neco s tohohle by se melo zpracovat, nez to dostane dekorace updateUserTime(time); const bool bModKeyHeld = modKeyDown(state); if (isSplash() && button == XCB_BUTTON_INDEX_1 && !bModKeyHeld) { // hide splashwindow if the user clicks on it hideClient(true); if (w == wrapperId()) xcb_allow_events(connection(), XCB_ALLOW_SYNC_POINTER, XCB_TIME_CURRENT_TIME); //xTime()); return true; } Options::MouseCommand com = Options::MouseNothing; bool was_action = false; if (bModKeyHeld) { was_action = true; switch(button) { case XCB_BUTTON_INDEX_1: com = options->commandAll1(); break; case XCB_BUTTON_INDEX_2: com = options->commandAll2(); break; case XCB_BUTTON_INDEX_3: com = options->commandAll3(); break; case XCB_BUTTON_INDEX_4: case XCB_BUTTON_INDEX_5: com = options->operationWindowMouseWheel(button == XCB_BUTTON_INDEX_4 ? 120 : -120); break; } } else { if (w == wrapperId()) { if (button < 4) { com = getMouseCommand(x11ToQtMouseButton(button), &was_action); } else if (button < 6) { com = getWheelCommand(Qt::Vertical, &was_action); } } } if (was_action) { bool replay = performMouseCommand(com, QPoint(x_root, y_root)); if (isSpecialWindow()) replay = true; if (w == wrapperId()) // these can come only from a grab xcb_allow_events(connection(), replay ? XCB_ALLOW_REPLAY_POINTER : XCB_ALLOW_SYNC_POINTER, XCB_TIME_CURRENT_TIME); //xTime()); return true; } } if (w == wrapperId()) { // these can come only from a grab xcb_allow_events(connection(), XCB_ALLOW_REPLAY_POINTER, XCB_TIME_CURRENT_TIME); //xTime()); return true; } if (w == inputId()) { x = x_root - frameGeometry().x(); y = y_root - frameGeometry().y(); // New API processes core events FIRST and only passes unused ones to the decoration QMouseEvent ev(QMouseEvent::MouseButtonPress, QPoint(x, y), QPoint(x_root, y_root), x11ToQtMouseButton(button), x11ToQtMouseButtons(state), Qt::KeyboardModifiers()); return processDecorationButtonPress(&ev, true); } if (w == frameId() && isDecorated()) { if (button >= 4 && button <= 7) { const Qt::KeyboardModifiers modifiers = x11ToQtKeyboardModifiers(state); // Logic borrowed from qapplication_x11.cpp const int delta = 120 * ((button == 4 || button == 6) ? 1 : -1); const bool hor = (((button == 4 || button == 5) && (modifiers & Qt::AltModifier)) || (button == 6 || button == 7)); const QPoint angle = hor ? QPoint(delta, 0) : QPoint(0, delta); QWheelEvent event(QPointF(x, y), QPointF(x_root, y_root), QPoint(), angle, delta, hor ? Qt::Horizontal : Qt::Vertical, x11ToQtMouseButtons(state), modifiers); event.setAccepted(false); QCoreApplication::sendEvent(decoration(), &event); if (!event.isAccepted() && !hor) { if (titlebarPositionUnderMouse()) { performMouseCommand(options->operationTitlebarMouseWheel(delta), QPoint(x_root, y_root)); } } } else { QMouseEvent event(QEvent::MouseButtonPress, QPointF(x, y), QPointF(x_root, y_root), x11ToQtMouseButton(button), x11ToQtMouseButtons(state), x11ToQtKeyboardModifiers(state)); event.setAccepted(false); QCoreApplication::sendEvent(decoration(), &event); if (!event.isAccepted()) { processDecorationButtonPress(&event); } } return true; } return true; } // return value matters only when filtering events before decoration gets them bool X11Client::buttonReleaseEvent(xcb_window_t w, int button, int state, int x, int y, int x_root, int y_root) { if (w == frameId() && isDecorated()) { // wheel handled on buttonPress if (button < 4 || button > 7) { QMouseEvent event(QEvent::MouseButtonRelease, QPointF(x, y), QPointF(x_root, y_root), x11ToQtMouseButton(button), x11ToQtMouseButtons(state) & ~x11ToQtMouseButton(button), x11ToQtKeyboardModifiers(state)); event.setAccepted(false); QCoreApplication::sendEvent(decoration(), &event); if (event.isAccepted() || !titlebarPositionUnderMouse()) { invalidateDecorationDoubleClickTimer(); // click was for the deco and shall not init a doubleclick } } } if (w == wrapperId()) { xcb_allow_events(connection(), XCB_ALLOW_SYNC_POINTER, XCB_TIME_CURRENT_TIME); //xTime()); return true; } if (w != frameId() && w != inputId() && w != moveResizeGrabWindow()) return true; if (w == frameId() && workspace()->userActionsMenu() && workspace()->userActionsMenu()->isShown()) { const_cast(workspace()->userActionsMenu())->grabInput(); } x = this->x(); // translate from grab window to local coords y = this->y(); // Check whether other buttons are still left pressed int buttonMask = XCB_BUTTON_MASK_1 | XCB_BUTTON_MASK_2 | XCB_BUTTON_MASK_3; if (button == XCB_BUTTON_INDEX_1) buttonMask &= ~XCB_BUTTON_MASK_1; else if (button == XCB_BUTTON_INDEX_2) buttonMask &= ~XCB_BUTTON_MASK_2; else if (button == XCB_BUTTON_INDEX_3) buttonMask &= ~XCB_BUTTON_MASK_3; if ((state & buttonMask) == 0) { endMoveResize(); } return true; } // return value matters only when filtering events before decoration gets them bool X11Client::motionNotifyEvent(xcb_window_t w, int state, int x, int y, int x_root, int y_root) { if (w == frameId() && isDecorated() && !isMinimized()) { // TODO Mouse move event dependent on state QHoverEvent event(QEvent::HoverMove, QPointF(x, y), QPointF(x, y)); QCoreApplication::instance()->sendEvent(decoration(), &event); } if (w != frameId() && w != inputId() && w != moveResizeGrabWindow()) return true; // care only about the whole frame if (!isMoveResizePointerButtonDown()) { if (w == inputId()) { int x = x_root - frameGeometry().x();// + padding_left; int y = y_root - frameGeometry().y();// + padding_top; if (isDecorated()) { QHoverEvent event(QEvent::HoverMove, QPointF(x, y), QPointF(x, y)); QCoreApplication::instance()->sendEvent(decoration(), &event); } } Position newmode = modKeyDown(state) ? PositionCenter : mousePosition(); if (newmode != moveResizePointerMode()) { setMoveResizePointerMode(newmode); updateCursor(); } return false; } if (w == moveResizeGrabWindow()) { x = this->x(); // translate from grab window to local coords y = this->y(); } handleMoveResize(QPoint(x, y), QPoint(x_root, y_root)); return true; } void X11Client::focusInEvent(xcb_focus_in_event_t *e) { if (e->event != window()) return; // only window gets focus if (e->mode == XCB_NOTIFY_MODE_UNGRAB) return; // we don't care if (e->detail == XCB_NOTIFY_DETAIL_POINTER) return; // we don't care if (!isShown(false) || !isOnCurrentDesktop()) // we unmapped it, but it got focus meanwhile -> return; // activateNextClient() already transferred focus elsewhere workspace()->forEachClient([](X11Client *client) { client->cancelFocusOutTimer(); }); // check if this client is in should_get_focus list or if activation is allowed bool activate = workspace()->allowClientActivation(this, -1U, true); workspace()->gotFocusIn(this); // remove from should_get_focus list if (activate) setActive(true); else { workspace()->restoreFocus(); demandAttention(); } } void X11Client::focusOutEvent(xcb_focus_out_event_t *e) { if (e->event != window()) return; // only window gets focus if (e->mode == XCB_NOTIFY_MODE_GRAB) return; // we don't care if (isShade()) return; // here neither if (e->detail != XCB_NOTIFY_DETAIL_NONLINEAR && e->detail != XCB_NOTIFY_DETAIL_NONLINEAR_VIRTUAL) // SELI check all this return; // hack for motif apps like netscape if (QApplication::activePopupWidget()) return; // When a client loses focus, FocusOut events are usually immediatelly // followed by FocusIn events for another client that gains the focus // (unless the focus goes to another screen, or to the nofocus widget). // Without this check, the former focused client would have to be // deactivated, and after that, the new one would be activated, with // a short time when there would be no active client. This can cause // flicker sometimes, e.g. when a fullscreen is shown, and focus is transferred // from it to its transient, the fullscreen would be kept in the Active layer // at the beginning and at the end, but not in the middle, when the active // client would be temporarily none (see X11Client::belongToLayer() ). // Therefore the setActive(false) call is moved to the end of the current // event queue. If there is a matching FocusIn event in the current queue // this will be processed before the setActive(false) call and the activation // of the Client which gained FocusIn will automatically deactivate the // previously active client. if (!m_focusOutTimer) { m_focusOutTimer = new QTimer(this); m_focusOutTimer->setSingleShot(true); m_focusOutTimer->setInterval(0); connect(m_focusOutTimer, &QTimer::timeout, [this]() { setActive(false); }); } m_focusOutTimer->start(); } // performs _NET_WM_MOVERESIZE void X11Client::NETMoveResize(int x_root, int y_root, NET::Direction direction) { if (direction == NET::Move) { // move cursor to the provided position to prevent the window jumping there on first movement // the expectation is that the cursor is already at the provided position, // thus it's more a safety measurement Cursor::setPos(QPoint(x_root, y_root)); performMouseCommand(Options::MouseMove, QPoint(x_root, y_root)); } else if (isMoveResize() && direction == NET::MoveResizeCancel) { finishMoveResize(true); setMoveResizePointerButtonDown(false); updateCursor(); } else if (direction >= NET::TopLeft && direction <= NET::Left) { static const Position convert[] = { PositionTopLeft, PositionTop, PositionTopRight, PositionRight, PositionBottomRight, PositionBottom, PositionBottomLeft, PositionLeft }; if (!isResizable() || isShade()) return; if (isMoveResize()) finishMoveResize(false); setMoveResizePointerButtonDown(true); setMoveOffset(QPoint(x_root - x(), y_root - y())); // map from global setInvertedMoveOffset(rect().bottomRight() - moveOffset()); setUnrestrictedMoveResize(false); setMoveResizePointerMode(convert[ direction ]); if (!startMoveResize()) setMoveResizePointerButtonDown(false); updateCursor(); } else if (direction == NET::KeyboardMove) { // ignore mouse coordinates given in the message, mouse position is used by the moving algorithm Cursor::setPos(frameGeometry().center()); performMouseCommand(Options::MouseUnrestrictedMove, frameGeometry().center()); } else if (direction == NET::KeyboardSize) { // ignore mouse coordinates given in the message, mouse position is used by the resizing algorithm Cursor::setPos(frameGeometry().bottomRight()); performMouseCommand(Options::MouseUnrestrictedResize, frameGeometry().bottomRight()); } } void X11Client::keyPressEvent(uint key_code, xcb_timestamp_t time) { updateUserTime(time); AbstractClient::keyPressEvent(key_code); } // **************************************** // Unmanaged // **************************************** bool Unmanaged::windowEvent(xcb_generic_event_t *e) { double old_opacity = opacity(); NET::Properties dirtyProperties; NET::Properties2 dirtyProperties2; info->event(e, &dirtyProperties, &dirtyProperties2); // pass through the NET stuff if (dirtyProperties2 & NET::WM2Opacity) { if (compositing()) { addRepaintFull(); emit opacityChanged(this, old_opacity); } } if (dirtyProperties2 & NET::WM2OpaqueRegion) { getWmOpaqueRegion(); } if (dirtyProperties2.testFlag(NET::WM2WindowRole)) { emit windowRoleChanged(); } if (dirtyProperties2.testFlag(NET::WM2WindowClass)) { getResourceClass(); } const uint8_t eventType = e->response_type & ~0x80; switch (eventType) { case XCB_DESTROY_NOTIFY: release(ReleaseReason::Destroyed); break; case XCB_UNMAP_NOTIFY:{ workspace()->updateFocusMousePosition(Cursor::pos()); // may cause leave event // unmap notify might have been emitted due to a destroy notify // but unmap notify gets emitted before the destroy notify, nevertheless at this // point the window is already destroyed. This means any XCB request with the window // will cause an error. // To not run into these errors we try to wait for the destroy notify. For this we // generate a round trip to the X server and wait a very short time span before // handling the release. updateXTime(); // using 1 msec to not just move it at the end of the event loop but add an very short // timespan to cover cases like unmap() followed by destroy(). The only other way to // ensure that the window is not destroyed when we do the release handling is to grab // the XServer which we do not want to do for an Unmanaged. The timespan of 1 msec is // short enough to not cause problems in the close window animations. // It's of course still possible that we miss the destroy in which case non-fatal // X errors are reported to the event loop and logged by Qt. m_scheduledRelease = true; QTimer::singleShot(1, this, SLOT(release())); break; } case XCB_CONFIGURE_NOTIFY: configureNotifyEvent(reinterpret_cast(e)); break; case XCB_PROPERTY_NOTIFY: propertyNotifyEvent(reinterpret_cast(e)); break; case XCB_CLIENT_MESSAGE: clientMessageEvent(reinterpret_cast(e)); break; default: { if (eventType == Xcb::Extensions::self()->shapeNotifyEvent()) { detectShape(window()); addRepaintFull(); addWorkspaceRepaint(frameGeometry()); // in case shape change removes part of this window emit geometryShapeChanged(this, frameGeometry()); } if (eventType == Xcb::Extensions::self()->damageNotifyEvent()) damageNotifyEvent(); break; } } return false; // don't eat events, even our own unmanaged widgets are tracked } void Unmanaged::configureNotifyEvent(xcb_configure_notify_event_t *e) { if (effects) static_cast(effects)->checkInputWindowStacking(); // keep them on top QRect newgeom(e->x, e->y, e->width, e->height); if (newgeom != m_frameGeometry) { addWorkspaceRepaint(visibleRect()); // damage old area QRect old = m_frameGeometry; m_frameGeometry = newgeom; - emit geometryChanged(); // update shadow region + emit frameGeometryChanged(this, old); // update shadow region addRepaintFull(); if (old.size() != m_frameGeometry.size()) discardWindowPixmap(); emit geometryShapeChanged(this, old); } } // **************************************** // Toplevel // **************************************** void Toplevel::propertyNotifyEvent(xcb_property_notify_event_t *e) { if (e->window != window()) return; // ignore frame/wrapper switch(e->atom) { default: if (e->atom == atoms->wm_client_leader) getWmClientLeader(); else if (e->atom == atoms->kde_net_wm_shadow) updateShadow(); else if (e->atom == atoms->kde_skip_close_animation) getSkipCloseAnimation(); break; } } void Toplevel::clientMessageEvent(xcb_client_message_event_t *e) { if (e->type == atoms->wl_surface_id) { m_surfaceId = e->data.data32[0]; if (auto w = waylandServer()) { if (auto s = KWayland::Server::SurfaceInterface::get(m_surfaceId, w->xWaylandConnection())) { setSurface(s); } } emit surfaceIdChanged(m_surfaceId); } } } // namespace diff --git a/internal_client.cpp b/internal_client.cpp index 4695914f4..76b45f573 100644 --- a/internal_client.cpp +++ b/internal_client.cpp @@ -1,682 +1,684 @@ /******************************************************************** KWin - the KDE window manager This file is part of the KDE project. Copyright (C) 2019 Martin Flöser Copyright (C) 2019 Vlad Zahorodnii This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . *********************************************************************/ #include "internal_client.h" #include "decorations/decorationbridge.h" #include "deleted.h" #include "workspace.h" #include #include #include Q_DECLARE_METATYPE(NET::WindowType) static const QByteArray s_skipClosePropertyName = QByteArrayLiteral("KWIN_SKIP_CLOSE_ANIMATION"); static const QByteArray s_shadowEnabledPropertyName = QByteArrayLiteral("kwin_shadow_enabled"); namespace KWin { InternalClient::InternalClient(QWindow *window) : m_internalWindow(window) , m_clientSize(window->size()) , m_windowId(window->winId()) , m_internalWindowFlags(window->flags()) { connect(m_internalWindow, &QWindow::xChanged, this, &InternalClient::updateInternalWindowGeometry); connect(m_internalWindow, &QWindow::yChanged, this, &InternalClient::updateInternalWindowGeometry); connect(m_internalWindow, &QWindow::widthChanged, this, &InternalClient::updateInternalWindowGeometry); connect(m_internalWindow, &QWindow::heightChanged, this, &InternalClient::updateInternalWindowGeometry); connect(m_internalWindow, &QWindow::windowTitleChanged, this, &InternalClient::setCaption); connect(m_internalWindow, &QWindow::opacityChanged, this, &InternalClient::setOpacity); connect(m_internalWindow, &QWindow::destroyed, this, &InternalClient::destroyClient); connect(this, &InternalClient::opacityChanged, this, &InternalClient::addRepaintFull); const QVariant windowType = m_internalWindow->property("kwin_windowType"); if (!windowType.isNull()) { m_windowType = windowType.value(); } setCaption(m_internalWindow->title()); setIcon(QIcon::fromTheme(QStringLiteral("kwin"))); setOnAllDesktops(true); setOpacity(m_internalWindow->opacity()); setSkipCloseAnimation(m_internalWindow->property(s_skipClosePropertyName).toBool()); // Create scene window, effect window, and update server-side shadow. setupCompositing(); updateColorScheme(); blockGeometryUpdates(true); commitGeometry(m_internalWindow->geometry()); updateDecoration(true); setFrameGeometry(clientRectToFrameRect(m_internalWindow->geometry())); setGeometryRestore(frameGeometry()); blockGeometryUpdates(false); m_internalWindow->installEventFilter(this); } InternalClient::~InternalClient() { } bool InternalClient::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()); } if (pe->propertyName() == s_shadowEnabledPropertyName) { updateShadow(); } if (pe->propertyName() == "kwin_windowType") { m_windowType = m_internalWindow->property("kwin_windowType").value(); workspace()->updateClientArea(); } } return false; } QRect InternalClient::bufferGeometry() const { return frameGeometry() - frameMargins(); } QStringList InternalClient::activities() const { return QStringList(); } void InternalClient::blockActivityUpdates(bool b) { Q_UNUSED(b) // Internal clients do not support activities. } qreal InternalClient::bufferScale() const { if (m_internalWindow) { return m_internalWindow->devicePixelRatio(); } return 1; } QString InternalClient::captionNormal() const { return m_captionNormal; } QString InternalClient::captionSuffix() const { return m_captionSuffix; } QPoint InternalClient::clientContentPos() const { return -1 * clientPos(); } QSize InternalClient::clientSize() const { return m_clientSize; } void InternalClient::debug(QDebug &stream) const { stream.nospace() << "\'InternalClient:" << m_internalWindow << "\'"; } QRect InternalClient::transparentRect() const { return QRect(); } NET::WindowType InternalClient::windowType(bool direct, int supported_types) const { Q_UNUSED(direct) Q_UNUSED(supported_types) return m_windowType; } double InternalClient::opacity() const { return m_opacity; } void InternalClient::setOpacity(double opacity) { if (m_opacity == opacity) { return; } const double oldOpacity = m_opacity; m_opacity = opacity; emit opacityChanged(this, oldOpacity); } void InternalClient::killWindow() { // We don't kill our internal windows. } bool InternalClient::isPopupWindow() const { if (AbstractClient::isPopupWindow()) { return true; } return m_internalWindowFlags.testFlag(Qt::Popup); } QByteArray InternalClient::windowRole() const { return QByteArray(); } void InternalClient::closeWindow() { if (m_internalWindow) { m_internalWindow->hide(); } } bool InternalClient::isCloseable() const { return true; } bool InternalClient::isFullScreenable() const { return false; } bool InternalClient::isFullScreen() const { return false; } bool InternalClient::isMaximizable() const { return false; } bool InternalClient::isMinimizable() const { return false; } bool InternalClient::isMovable() const { return true; } bool InternalClient::isMovableAcrossScreens() const { return true; } bool InternalClient::isResizable() const { return true; } bool InternalClient::noBorder() const { return m_userNoBorder || m_internalWindowFlags.testFlag(Qt::FramelessWindowHint) || m_internalWindowFlags.testFlag(Qt::Popup); } bool InternalClient::userCanSetNoBorder() const { return !m_internalWindowFlags.testFlag(Qt::FramelessWindowHint) || m_internalWindowFlags.testFlag(Qt::Popup); } bool InternalClient::wantsInput() const { return false; } bool InternalClient::isInternal() const { return true; } bool InternalClient::isLockScreen() const { if (m_internalWindow) { return m_internalWindow->property("org_kde_ksld_emergency").toBool(); } return false; } bool InternalClient::isInputMethod() const { if (m_internalWindow) { return m_internalWindow->property("__kwin_input_method").toBool(); } return false; } bool InternalClient::isOutline() const { if (m_internalWindow) { return m_internalWindow->property("__kwin_outline").toBool(); } return false; } quint32 InternalClient::windowId() const { return m_windowId; } MaximizeMode InternalClient::maximizeMode() const { return MaximizeRestore; } QRect InternalClient::geometryRestore() const { return m_maximizeRestoreGeometry; } bool InternalClient::isShown(bool shaded_is_shown) const { Q_UNUSED(shaded_is_shown) return readyForPainting(); } bool InternalClient::isHiddenInternal() const { return false; } void InternalClient::hideClient(bool hide) { Q_UNUSED(hide) } void InternalClient::resizeWithChecks(int w, int h, ForceGeometry_t force) { Q_UNUSED(force) if (!m_internalWindow) { return; } 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(); } setFrameGeometry(QRect(x(), y(), w, h)); } void InternalClient::setFrameGeometry(int x, int y, int w, int h, ForceGeometry_t force) { const QRect rect(x, y, w, h); if (areGeometryUpdatesBlocked()) { m_frameGeometry = rect; 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. m_frameGeometry = frameGeometryBeforeUpdateBlocking(); } if (m_frameGeometry == rect) { return; } const QRect newClientGeometry = frameRectToClientRect(rect); if (m_clientSize == newClientGeometry.size()) { commitGeometry(rect); } else { requestGeometry(rect); } } void InternalClient::setGeometryRestore(const QRect &rect) { m_maximizeRestoreGeometry = rect; } bool InternalClient::supportsWindowRules() const { return false; } AbstractClient *InternalClient::findModal(bool allow_itself) { Q_UNUSED(allow_itself) return nullptr; } void InternalClient::setOnAllActivities(bool set) { Q_UNUSED(set) // Internal clients do not support activities. } void InternalClient::takeFocus() { } bool InternalClient::userCanSetFullScreen() const { return false; } void InternalClient::setFullScreen(bool set, bool user) { Q_UNUSED(set) Q_UNUSED(user) } void InternalClient::setNoBorder(bool set) { if (!userCanSetNoBorder()) { return; } if (m_userNoBorder == set) { return; } m_userNoBorder = set; updateDecoration(true); } void InternalClient::updateDecoration(bool check_workspace_pos, bool force) { if (!force && isDecorated() == !noBorder()) { return; } const QRect oldFrameGeometry = frameGeometry(); const QRect oldClientGeometry = oldFrameGeometry - frameMargins(); GeometryUpdatesBlocker blocker(this); if (force) { destroyDecoration(); } if (!noBorder()) { createDecoration(oldClientGeometry); } else { destroyDecoration(); } updateShadow(); if (check_workspace_pos) { checkWorkspacePosition(oldFrameGeometry, -2, oldClientGeometry); } } void InternalClient::updateColorScheme() { AbstractClient::updateColorScheme(QString()); } void InternalClient::showOnScreenEdge() { } void InternalClient::destroyClient() { if (isMoveResize()) { leaveMoveResize(); } Deleted *deleted = Deleted::create(this); emit windowClosed(this, deleted); destroyDecoration(); workspace()->removeInternalClient(this); deleted->unrefWindow(); m_internalWindow = nullptr; delete this; } void InternalClient::present(const QSharedPointer fbo) { Q_ASSERT(m_internalImage.isNull()); const QSize bufferSize = fbo->size() / bufferScale(); commitGeometry(QRect(pos(), sizeForClientSize(bufferSize))); markAsMapped(); if (m_internalFBO != fbo) { discardWindowPixmap(); m_internalFBO = fbo; } setDepth(32); addDamageFull(); addRepaintFull(); } void InternalClient::present(const QImage &image, const QRegion &damage) { Q_ASSERT(m_internalFBO.isNull()); const QSize bufferSize = image.size() / bufferScale(); commitGeometry(QRect(pos(), sizeForClientSize(bufferSize))); markAsMapped(); if (m_internalImage.size() != image.size()) { discardWindowPixmap(); } m_internalImage = image; setDepth(32); addDamage(damage); addRepaint(damage.translated(borderLeft(), borderTop())); } QWindow *InternalClient::internalWindow() const { return m_internalWindow; } bool InternalClient::acceptsFocus() const { return false; } bool InternalClient::belongsToSameApplication(const AbstractClient *other, SameApplicationChecks checks) const { Q_UNUSED(checks) return qobject_cast(other) != nullptr; } void InternalClient::changeMaximize(bool horizontal, bool vertical, bool adjust) { Q_UNUSED(horizontal) Q_UNUSED(vertical) Q_UNUSED(adjust) // Internal clients are not maximizable. } void InternalClient::destroyDecoration() { if (!isDecorated()) { return; } const QRect clientGeometry = frameRectToClientRect(frameGeometry()); AbstractClient::destroyDecoration(); setFrameGeometry(clientGeometry); } void InternalClient::doMove(int x, int y) { Q_UNUSED(x) Q_UNUSED(y) syncGeometryToInternalWindow(); } void InternalClient::doResizeSync() { requestGeometry(moveResizeGeometry()); } void InternalClient::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 InternalClient::createDecoration(const QRect &rect) { KDecoration2::Decoration *decoration = Decoration::DecorationBridge::self()->createDecoration(this); if (decoration) { QMetaObject::invokeMethod(decoration, "update", Qt::QueuedConnection); connect(decoration, &KDecoration2::Decoration::shadowChanged, this, &Toplevel::updateShadow); connect(decoration, &KDecoration2::Decoration::bordersChanged, this, [this]() { GeometryUpdatesBlocker blocker(this); const QRect oldGeometry = frameGeometry(); if (!isShade()) { checkWorkspacePosition(oldGeometry); } emit geometryShapeChanged(this, oldGeometry); } ); } const QRect oldFrameGeometry = frameGeometry(); setDecoration(decoration); setFrameGeometry(clientRectToFrameRect(rect)); emit geometryShapeChanged(this, oldFrameGeometry); } void InternalClient::requestGeometry(const QRect &rect) { if (m_internalWindow) { m_internalWindow->setGeometry(frameRectToClientRect(rect)); } } void InternalClient::commitGeometry(const QRect &rect) { if (m_frameGeometry == rect && pendingGeometryUpdate() == PendingGeometryNone) { return; } m_frameGeometry = rect; m_clientSize = frameRectToClientRect(frameGeometry()).size(); addWorkspaceRepaint(visibleRect()); syncGeometryToInternalWindow(); - const QRect oldGeometry = frameGeometryBeforeUpdateBlocking(); + if (frameGeometryBeforeUpdateBlocking() != frameGeometry()) { + emit frameGeometryChanged(this, frameGeometryBeforeUpdateBlocking()); + } + emit geometryShapeChanged(this, frameGeometryBeforeUpdateBlocking()); updateGeometryBeforeUpdateBlocking(); - emit geometryShapeChanged(this, oldGeometry); if (isResize()) { performMoveResize(); } } void InternalClient::setCaption(const QString &caption) { if (m_captionNormal == caption) { return; } m_captionNormal = caption; const QString oldCaptionSuffix = m_captionSuffix; updateCaption(); if (m_captionSuffix == oldCaptionSuffix) { emit captionChanged(); } } void InternalClient::markAsMapped() { if (!ready_for_painting) { setReadyForPainting(); workspace()->addInternalClient(this); } } void InternalClient::syncGeometryToInternalWindow() { if (m_internalWindow->geometry() == frameRectToClientRect(frameGeometry())) { return; } QTimer::singleShot(0, this, [this] { requestGeometry(frameGeometry()); }); } void InternalClient::updateInternalWindowGeometry() { if (isMoveResize()) { return; } commitGeometry(clientRectToFrameRect(m_internalWindow->geometry())); } } diff --git a/pointer_input.cpp b/pointer_input.cpp index a2dfdac05..386a0238e 100644 --- a/pointer_input.cpp +++ b/pointer_input.cpp @@ -1,1427 +1,1427 @@ /******************************************************************** KWin - the KDE window manager This file is part of the KDE project. Copyright (C) 2013, 2016 Martin Gräßlin Copyright (C) 2018 Roman Gilg Copyright (C) 2019 Vlad Zahorodnii This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . *********************************************************************/ #include "pointer_input.h" #include "platform.h" #include "x11client.h" #include "effects.h" #include "input_event.h" #include "input_event_spy.h" #include "osd.h" #include "screens.h" #include "xdgshellclient.h" #include "wayland_cursor_theme.h" #include "wayland_server.h" #include "workspace.h" #include "decorations/decoratedclient.h" #include "screens.h" // KDecoration #include // KWayland #include #include #include #include #include #include #include #include // screenlocker #include #include #include #include #include // Wayland #include #include namespace KWin { static const QHash s_buttonToQtMouseButton = { { BTN_LEFT , Qt::LeftButton }, { BTN_MIDDLE , Qt::MiddleButton }, { BTN_RIGHT , Qt::RightButton }, // in QtWayland mapped like that { BTN_SIDE , Qt::ExtraButton1 }, // in QtWayland mapped like that { BTN_EXTRA , Qt::ExtraButton2 }, { BTN_BACK , Qt::BackButton }, { BTN_FORWARD , Qt::ForwardButton }, { BTN_TASK , Qt::TaskButton }, // mapped like that in QtWayland { 0x118 , Qt::ExtraButton6 }, { 0x119 , Qt::ExtraButton7 }, { 0x11a , Qt::ExtraButton8 }, { 0x11b , Qt::ExtraButton9 }, { 0x11c , Qt::ExtraButton10 }, { 0x11d , Qt::ExtraButton11 }, { 0x11e , Qt::ExtraButton12 }, { 0x11f , Qt::ExtraButton13 }, }; uint32_t qtMouseButtonToButton(Qt::MouseButton button) { return s_buttonToQtMouseButton.key(button); } static Qt::MouseButton buttonToQtMouseButton(uint32_t button) { // all other values get mapped to ExtraButton24 // this is actually incorrect but doesn't matter in our usage // KWin internally doesn't use these high extra buttons anyway // it's only needed for recognizing whether buttons are pressed // if multiple buttons are mapped to the value the evaluation whether // buttons are pressed is correct and that's all we care about. return s_buttonToQtMouseButton.value(button, Qt::ExtraButton24); } static bool screenContainsPos(const QPointF &pos) { for (int i = 0; i < screens()->count(); ++i) { if (screens()->geometry(i).contains(pos.toPoint())) { return true; } } return false; } static QPointF confineToBoundingBox(const QPointF &pos, const QRectF &boundingBox) { return QPointF( qBound(boundingBox.left(), pos.x(), boundingBox.right() - 1.0), qBound(boundingBox.top(), pos.y(), boundingBox.bottom() - 1.0) ); } PointerInputRedirection::PointerInputRedirection(InputRedirection* parent) : InputDeviceHandler(parent) , m_cursor(nullptr) , m_supportsWarping(Application::usesLibinput()) { } PointerInputRedirection::~PointerInputRedirection() = default; void PointerInputRedirection::init() { Q_ASSERT(!inited()); m_cursor = new CursorImage(this); setInited(true); InputDeviceHandler::init(); connect(m_cursor, &CursorImage::changed, kwinApp()->platform(), &Platform::cursorChanged); emit m_cursor->changed(); connect(screens(), &Screens::changed, this, &PointerInputRedirection::updateAfterScreenChange); if (waylandServer()->hasScreenLockerIntegration()) { connect(ScreenLocker::KSldApp::self(), &ScreenLocker::KSldApp::lockStateChanged, this, [this] { waylandServer()->seat()->cancelPointerPinchGesture(); waylandServer()->seat()->cancelPointerSwipeGesture(); update(); } ); } connect(workspace(), &QObject::destroyed, this, [this] { setInited(false); }); connect(waylandServer(), &QObject::destroyed, this, [this] { setInited(false); }); connect(waylandServer()->seat(), &KWayland::Server::SeatInterface::dragEnded, this, [this] { // need to force a focused pointer change waylandServer()->seat()->setFocusedPointerSurface(nullptr); setFocus(nullptr); update(); } ); // connect the move resize of all window auto setupMoveResizeConnection = [this] (AbstractClient *c) { connect(c, &AbstractClient::clientStartUserMovedResized, this, &PointerInputRedirection::updateOnStartMoveResize); connect(c, &AbstractClient::clientFinishUserMovedResized, this, &PointerInputRedirection::update); }; const auto clients = workspace()->allClientList(); std::for_each(clients.begin(), clients.end(), setupMoveResizeConnection); connect(workspace(), &Workspace::clientAdded, this, setupMoveResizeConnection); connect(waylandServer(), &WaylandServer::shellClientAdded, this, setupMoveResizeConnection); // warp the cursor to center of screen warp(screens()->geometry().center()); updateAfterScreenChange(); } void PointerInputRedirection::updateOnStartMoveResize() { breakPointerConstraints(focus() ? focus()->surface() : nullptr); disconnectPointerConstraintsConnection(); setFocus(nullptr); waylandServer()->seat()->setFocusedPointerSurface(nullptr); } void PointerInputRedirection::updateToReset() { if (internalWindow()) { disconnect(m_internalWindowConnection); m_internalWindowConnection = QMetaObject::Connection(); QEvent event(QEvent::Leave); QCoreApplication::sendEvent(internalWindow().data(), &event); setInternalWindow(nullptr); } if (decoration()) { QHoverEvent event(QEvent::HoverLeave, QPointF(), QPointF()); QCoreApplication::instance()->sendEvent(decoration()->decoration(), &event); setDecoration(nullptr); } if (focus()) { if (AbstractClient *c = qobject_cast(focus().data())) { c->leaveEvent(); } disconnect(m_focusGeometryConnection); m_focusGeometryConnection = QMetaObject::Connection(); breakPointerConstraints(focus()->surface()); disconnectPointerConstraintsConnection(); setFocus(nullptr); } waylandServer()->seat()->setFocusedPointerSurface(nullptr); } void PointerInputRedirection::processMotion(const QPointF &pos, uint32_t time, LibInput::Device *device) { processMotion(pos, QSizeF(), QSizeF(), time, 0, device); } class PositionUpdateBlocker { public: PositionUpdateBlocker(PointerInputRedirection *pointer) : m_pointer(pointer) { s_counter++; } ~PositionUpdateBlocker() { s_counter--; if (s_counter == 0) { if (!s_scheduledPositions.isEmpty()) { const auto pos = s_scheduledPositions.takeFirst(); m_pointer->processMotion(pos.pos, pos.delta, pos.deltaNonAccelerated, pos.time, pos.timeUsec, nullptr); } } } static bool isPositionBlocked() { return s_counter > 0; } static void schedulePosition(const QPointF &pos, const QSizeF &delta, const QSizeF &deltaNonAccelerated, uint32_t time, quint64 timeUsec) { s_scheduledPositions.append({pos, delta, deltaNonAccelerated, time, timeUsec}); } private: static int s_counter; struct ScheduledPosition { QPointF pos; QSizeF delta; QSizeF deltaNonAccelerated; quint32 time; quint64 timeUsec; }; static QVector s_scheduledPositions; PointerInputRedirection *m_pointer; }; int PositionUpdateBlocker::s_counter = 0; QVector PositionUpdateBlocker::s_scheduledPositions; void PointerInputRedirection::processMotion(const QPointF &pos, const QSizeF &delta, const QSizeF &deltaNonAccelerated, uint32_t time, quint64 timeUsec, LibInput::Device *device) { if (!inited()) { return; } if (PositionUpdateBlocker::isPositionBlocked()) { PositionUpdateBlocker::schedulePosition(pos, delta, deltaNonAccelerated, time, timeUsec); return; } PositionUpdateBlocker blocker(this); updatePosition(pos); MouseEvent event(QEvent::MouseMove, m_pos, Qt::NoButton, m_qtButtons, input()->keyboardModifiers(), time, delta, deltaNonAccelerated, timeUsec, device); event.setModifiersRelevantForGlobalShortcuts(input()->modifiersRelevantForGlobalShortcuts()); update(); input()->processSpies(std::bind(&InputEventSpy::pointerEvent, std::placeholders::_1, &event)); input()->processFilters(std::bind(&InputEventFilter::pointerEvent, std::placeholders::_1, &event, 0)); } void PointerInputRedirection::processButton(uint32_t button, InputRedirection::PointerButtonState state, uint32_t time, LibInput::Device *device) { QEvent::Type type; switch (state) { case InputRedirection::PointerButtonReleased: type = QEvent::MouseButtonRelease; break; case InputRedirection::PointerButtonPressed: type = QEvent::MouseButtonPress; update(); break; default: Q_UNREACHABLE(); return; } updateButton(button, state); MouseEvent event(type, m_pos, buttonToQtMouseButton(button), m_qtButtons, input()->keyboardModifiers(), time, QSizeF(), QSizeF(), 0, device); event.setModifiersRelevantForGlobalShortcuts(input()->modifiersRelevantForGlobalShortcuts()); event.setNativeButton(button); input()->processSpies(std::bind(&InputEventSpy::pointerEvent, std::placeholders::_1, &event)); if (!inited()) { return; } input()->processFilters(std::bind(&InputEventFilter::pointerEvent, std::placeholders::_1, &event, button)); if (state == InputRedirection::PointerButtonReleased) { update(); } } void PointerInputRedirection::processAxis(InputRedirection::PointerAxis axis, qreal delta, qint32 discreteDelta, InputRedirection::PointerAxisSource source, uint32_t time, LibInput::Device *device) { update(); emit input()->pointerAxisChanged(axis, delta); WheelEvent wheelEvent(m_pos, delta, discreteDelta, (axis == InputRedirection::PointerAxisHorizontal) ? Qt::Horizontal : Qt::Vertical, m_qtButtons, input()->keyboardModifiers(), source, time, device); wheelEvent.setModifiersRelevantForGlobalShortcuts(input()->modifiersRelevantForGlobalShortcuts()); input()->processSpies(std::bind(&InputEventSpy::wheelEvent, std::placeholders::_1, &wheelEvent)); if (!inited()) { return; } input()->processFilters(std::bind(&InputEventFilter::wheelEvent, std::placeholders::_1, &wheelEvent)); } void PointerInputRedirection::processSwipeGestureBegin(int fingerCount, quint32 time, KWin::LibInput::Device *device) { Q_UNUSED(device) if (!inited()) { return; } input()->processSpies(std::bind(&InputEventSpy::swipeGestureBegin, std::placeholders::_1, fingerCount, time)); input()->processFilters(std::bind(&InputEventFilter::swipeGestureBegin, std::placeholders::_1, fingerCount, time)); } void PointerInputRedirection::processSwipeGestureUpdate(const QSizeF &delta, quint32 time, KWin::LibInput::Device *device) { Q_UNUSED(device) if (!inited()) { return; } update(); input()->processSpies(std::bind(&InputEventSpy::swipeGestureUpdate, std::placeholders::_1, delta, time)); input()->processFilters(std::bind(&InputEventFilter::swipeGestureUpdate, std::placeholders::_1, delta, time)); } void PointerInputRedirection::processSwipeGestureEnd(quint32 time, KWin::LibInput::Device *device) { Q_UNUSED(device) if (!inited()) { return; } update(); input()->processSpies(std::bind(&InputEventSpy::swipeGestureEnd, std::placeholders::_1, time)); input()->processFilters(std::bind(&InputEventFilter::swipeGestureEnd, std::placeholders::_1, time)); } void PointerInputRedirection::processSwipeGestureCancelled(quint32 time, KWin::LibInput::Device *device) { Q_UNUSED(device) if (!inited()) { return; } update(); input()->processSpies(std::bind(&InputEventSpy::swipeGestureCancelled, std::placeholders::_1, time)); input()->processFilters(std::bind(&InputEventFilter::swipeGestureCancelled, std::placeholders::_1, time)); } void PointerInputRedirection::processPinchGestureBegin(int fingerCount, quint32 time, KWin::LibInput::Device *device) { Q_UNUSED(device) if (!inited()) { return; } update(); input()->processSpies(std::bind(&InputEventSpy::pinchGestureBegin, std::placeholders::_1, fingerCount, time)); input()->processFilters(std::bind(&InputEventFilter::pinchGestureBegin, std::placeholders::_1, fingerCount, time)); } void PointerInputRedirection::processPinchGestureUpdate(qreal scale, qreal angleDelta, const QSizeF &delta, quint32 time, KWin::LibInput::Device *device) { Q_UNUSED(device) if (!inited()) { return; } update(); input()->processSpies(std::bind(&InputEventSpy::pinchGestureUpdate, std::placeholders::_1, scale, angleDelta, delta, time)); input()->processFilters(std::bind(&InputEventFilter::pinchGestureUpdate, std::placeholders::_1, scale, angleDelta, delta, time)); } void PointerInputRedirection::processPinchGestureEnd(quint32 time, KWin::LibInput::Device *device) { Q_UNUSED(device) if (!inited()) { return; } update(); input()->processSpies(std::bind(&InputEventSpy::pinchGestureEnd, std::placeholders::_1, time)); input()->processFilters(std::bind(&InputEventFilter::pinchGestureEnd, std::placeholders::_1, time)); } void PointerInputRedirection::processPinchGestureCancelled(quint32 time, KWin::LibInput::Device *device) { Q_UNUSED(device) if (!inited()) { return; } update(); input()->processSpies(std::bind(&InputEventSpy::pinchGestureCancelled, std::placeholders::_1, time)); input()->processFilters(std::bind(&InputEventFilter::pinchGestureCancelled, std::placeholders::_1, time)); } bool PointerInputRedirection::areButtonsPressed() const { for (auto state : m_buttons) { if (state == InputRedirection::PointerButtonPressed) { return true; } } return false; } bool PointerInputRedirection::focusUpdatesBlocked() { if (!inited()) { return true; } if (waylandServer()->seat()->isDragPointer()) { // ignore during drag and drop return true; } if (waylandServer()->seat()->isTouchSequence()) { // ignore during touch operations return true; } if (input()->isSelectingWindow()) { return true; } if (areButtonsPressed()) { return true; } return false; } void PointerInputRedirection::cleanupInternalWindow(QWindow *old, QWindow *now) { disconnect(m_internalWindowConnection); m_internalWindowConnection = QMetaObject::Connection(); if (old) { // leave internal window QEvent leaveEvent(QEvent::Leave); QCoreApplication::sendEvent(old, &leaveEvent); } if (now) { m_internalWindowConnection = connect(internalWindow().data(), &QWindow::visibleChanged, this, [this] (bool visible) { if (!visible) { update(); } } ); } } void PointerInputRedirection::cleanupDecoration(Decoration::DecoratedClientImpl *old, Decoration::DecoratedClientImpl *now) { disconnect(m_decorationGeometryConnection); m_decorationGeometryConnection = QMetaObject::Connection(); workspace()->updateFocusMousePosition(position().toPoint()); if (old) { // send leave event to old decoration QHoverEvent event(QEvent::HoverLeave, QPointF(), QPointF()); QCoreApplication::instance()->sendEvent(old->decoration(), &event); } if (!now) { // left decoration return; } waylandServer()->seat()->setFocusedPointerSurface(nullptr); auto pos = m_pos - now->client()->pos(); QHoverEvent event(QEvent::HoverEnter, pos, pos); QCoreApplication::instance()->sendEvent(now->decoration(), &event); now->client()->processDecorationMove(pos.toPoint(), m_pos.toPoint()); - m_decorationGeometryConnection = connect(decoration()->client(), &AbstractClient::geometryChanged, this, + m_decorationGeometryConnection = connect(decoration()->client(), &AbstractClient::frameGeometryChanged, this, [this] { // ensure maximize button gets the leave event when maximizing/restore a window, see BUG 385140 const auto oldDeco = decoration(); update(); if (oldDeco && oldDeco == decoration() && !decoration()->client()->isMove() && !decoration()->client()->isResize() && !areButtonsPressed()) { // position of window did not change, we need to send HoverMotion manually const QPointF p = m_pos - decoration()->client()->pos(); QHoverEvent event(QEvent::HoverMove, p, p); QCoreApplication::instance()->sendEvent(decoration()->decoration(), &event); } }, Qt::QueuedConnection); } static bool s_cursorUpdateBlocking = false; void PointerInputRedirection::focusUpdate(Toplevel *focusOld, Toplevel *focusNow) { if (AbstractClient *ac = qobject_cast(focusOld)) { ac->leaveEvent(); breakPointerConstraints(ac->surface()); disconnectPointerConstraintsConnection(); } disconnect(m_focusGeometryConnection); m_focusGeometryConnection = QMetaObject::Connection(); if (AbstractClient *ac = qobject_cast(focusNow)) { ac->enterEvent(m_pos.toPoint()); workspace()->updateFocusMousePosition(m_pos.toPoint()); } if (internalWindow()) { // enter internal window const auto pos = at()->pos(); QEnterEvent enterEvent(pos, pos, m_pos); QCoreApplication::sendEvent(internalWindow().data(), &enterEvent); } auto seat = waylandServer()->seat(); if (!focusNow || !focusNow->surface() || decoration()) { // Clean up focused pointer surface if there's no client to take focus, // or the pointer is on a client without surface or on a decoration. warpXcbOnSurfaceLeft(nullptr); seat->setFocusedPointerSurface(nullptr); return; } // TODO: add convenient API to update global pos together with updating focused surface warpXcbOnSurfaceLeft(focusNow->surface()); // TODO: why? in order to reset the cursor icon? s_cursorUpdateBlocking = true; seat->setFocusedPointerSurface(nullptr); s_cursorUpdateBlocking = false; seat->setPointerPos(m_pos.toPoint()); seat->setFocusedPointerSurface(focusNow->surface(), focusNow->inputTransformation()); - m_focusGeometryConnection = connect(focusNow, &Toplevel::geometryChanged, this, + m_focusGeometryConnection = connect(focusNow, &Toplevel::frameGeometryChanged, this, [this] { // TODO: why no assert possible? if (!focus()) { return; } // TODO: can we check on the client instead? if (workspace()->moveResizeClient()) { // don't update while moving return; } auto seat = waylandServer()->seat(); if (focus()->surface() != seat->focusedPointerSurface()) { return; } seat->setFocusedPointerSurfaceTransformation(focus()->inputTransformation()); } ); m_constraintsConnection = connect(focusNow->surface(), &KWayland::Server::SurfaceInterface::pointerConstraintsChanged, this, &PointerInputRedirection::updatePointerConstraints); m_constraintsActivatedConnection = connect(workspace(), &Workspace::clientActivated, this, &PointerInputRedirection::updatePointerConstraints); updatePointerConstraints(); } void PointerInputRedirection::breakPointerConstraints(KWayland::Server::SurfaceInterface *surface) { // cancel pointer constraints if (surface) { auto c = surface->confinedPointer(); if (c && c->isConfined()) { c->setConfined(false); } auto l = surface->lockedPointer(); if (l && l->isLocked()) { l->setLocked(false); } } disconnectConfinedPointerRegionConnection(); m_confined = false; m_locked = false; } void PointerInputRedirection::disconnectConfinedPointerRegionConnection() { disconnect(m_confinedPointerRegionConnection); m_confinedPointerRegionConnection = QMetaObject::Connection(); } void PointerInputRedirection::disconnectLockedPointerAboutToBeUnboundConnection() { disconnect(m_lockedPointerAboutToBeUnboundConnection); m_lockedPointerAboutToBeUnboundConnection = QMetaObject::Connection(); } void PointerInputRedirection::disconnectPointerConstraintsConnection() { disconnect(m_constraintsConnection); m_constraintsConnection = QMetaObject::Connection(); disconnect(m_constraintsActivatedConnection); m_constraintsActivatedConnection = QMetaObject::Connection(); } template static QRegion getConstraintRegion(Toplevel *t, T *constraint) { const QRegion windowShape = t->inputShape(); const QRegion windowRegion = windowShape.isEmpty() ? QRegion(0, 0, t->clientSize().width(), t->clientSize().height()) : windowShape; const QRegion intersected = constraint->region().isEmpty() ? windowRegion : windowRegion.intersected(constraint->region()); return intersected.translated(t->pos() + t->clientPos()); } void PointerInputRedirection::setEnableConstraints(bool set) { if (m_enableConstraints == set) { return; } m_enableConstraints = set; updatePointerConstraints(); } void PointerInputRedirection::updatePointerConstraints() { if (focus().isNull()) { return; } const auto s = focus()->surface(); if (!s) { return; } if (s != waylandServer()->seat()->focusedPointerSurface()) { return; } if (!supportsWarping()) { return; } const bool canConstrain = m_enableConstraints && focus() == workspace()->activeClient(); const auto cf = s->confinedPointer(); if (cf) { if (cf->isConfined()) { if (!canConstrain) { cf->setConfined(false); m_confined = false; disconnectConfinedPointerRegionConnection(); } return; } const QRegion r = getConstraintRegion(focus().data(), cf.data()); if (canConstrain && r.contains(m_pos.toPoint())) { cf->setConfined(true); m_confined = true; m_confinedPointerRegionConnection = connect(cf.data(), &KWayland::Server::ConfinedPointerInterface::regionChanged, this, [this] { if (!focus()) { return; } const auto s = focus()->surface(); if (!s) { return; } const auto cf = s->confinedPointer(); if (!getConstraintRegion(focus().data(), cf.data()).contains(m_pos.toPoint())) { // pointer no longer in confined region, break the confinement cf->setConfined(false); m_confined = false; } else { if (!cf->isConfined()) { cf->setConfined(true); m_confined = true; } } } ); return; } } else { m_confined = false; disconnectConfinedPointerRegionConnection(); } const auto lock = s->lockedPointer(); if (lock) { if (lock->isLocked()) { if (!canConstrain) { const auto hint = lock->cursorPositionHint(); lock->setLocked(false); m_locked = false; disconnectLockedPointerAboutToBeUnboundConnection(); if (! (hint.x() < 0 || hint.y() < 0) && focus()) { processMotion(focus()->pos() - focus()->clientContentPos() + hint, waylandServer()->seat()->timestamp()); } } return; } const QRegion r = getConstraintRegion(focus().data(), lock.data()); if (canConstrain && r.contains(m_pos.toPoint())) { lock->setLocked(true); m_locked = true; // The client might cancel pointer locking from its side by unbinding the LockedPointerInterface. // In this case the cached cursor position hint must be fetched before the resource goes away m_lockedPointerAboutToBeUnboundConnection = connect(lock.data(), &KWayland::Server::LockedPointerInterface::aboutToBeUnbound, this, [this, lock]() { const auto hint = lock->cursorPositionHint(); if (hint.x() < 0 || hint.y() < 0 || !focus()) { return; } auto globalHint = focus()->pos() - focus()->clientContentPos() + hint; // When the resource finally goes away, reposition the cursor according to the hint connect(lock.data(), &KWayland::Server::LockedPointerInterface::unbound, this, [this, globalHint]() { processMotion(globalHint, waylandServer()->seat()->timestamp()); }); } ); // TODO: connect to region change - is it needed at all? If the pointer is locked it's always in the region } } else { m_locked = false; disconnectLockedPointerAboutToBeUnboundConnection(); } } void PointerInputRedirection::warpXcbOnSurfaceLeft(KWayland::Server::SurfaceInterface *newSurface) { auto xc = waylandServer()->xWaylandConnection(); if (!xc) { // No XWayland, no point in warping the x cursor return; } const auto c = kwinApp()->x11Connection(); if (!c) { return; } static bool s_hasXWayland119 = xcb_get_setup(c)->release_number >= 11900000; if (s_hasXWayland119) { return; } if (newSurface && newSurface->client() == xc) { // new window is an X window return; } auto s = waylandServer()->seat()->focusedPointerSurface(); if (!s || s->client() != xc) { // pointer was not on an X window return; } // warp pointer to 0/0 to trigger leave events on previously focused X window xcb_warp_pointer(c, XCB_WINDOW_NONE, kwinApp()->x11RootWindow(), 0, 0, 0, 0, 0, 0), xcb_flush(c); } QPointF PointerInputRedirection::applyPointerConfinement(const QPointF &pos) const { if (!focus()) { return pos; } auto s = focus()->surface(); if (!s) { return pos; } auto cf = s->confinedPointer(); if (!cf) { return pos; } if (!cf->isConfined()) { return pos; } const QRegion confinementRegion = getConstraintRegion(focus().data(), cf.data()); if (confinementRegion.contains(pos.toPoint())) { return pos; } QPointF p = pos; // allow either x or y to pass p = QPointF(m_pos.x(), pos.y()); if (confinementRegion.contains(p.toPoint())) { return p; } p = QPointF(pos.x(), m_pos.y()); if (confinementRegion.contains(p.toPoint())) { return p; } return m_pos; } void PointerInputRedirection::updatePosition(const QPointF &pos) { if (m_locked) { // locked pointer should not move return; } // verify that at least one screen contains the pointer position QPointF p = pos; if (!screenContainsPos(p)) { const QRectF unitedScreensGeometry = screens()->geometry(); p = confineToBoundingBox(p, unitedScreensGeometry); if (!screenContainsPos(p)) { const QRectF currentScreenGeometry = screens()->geometry(screens()->number(m_pos.toPoint())); p = confineToBoundingBox(p, currentScreenGeometry); } } p = applyPointerConfinement(p); if (p == m_pos) { // didn't change due to confinement return; } // verify screen confinement if (!screenContainsPos(p)) { return; } m_pos = p; emit input()->globalPointerChanged(m_pos); } void PointerInputRedirection::updateButton(uint32_t button, InputRedirection::PointerButtonState state) { m_buttons[button] = state; // update Qt buttons m_qtButtons = Qt::NoButton; for (auto it = m_buttons.constBegin(); it != m_buttons.constEnd(); ++it) { if (it.value() == InputRedirection::PointerButtonReleased) { continue; } m_qtButtons |= buttonToQtMouseButton(it.key()); } emit input()->pointerButtonStateChanged(button, state); } void PointerInputRedirection::warp(const QPointF &pos) { if (supportsWarping()) { kwinApp()->platform()->warpPointer(pos); processMotion(pos, waylandServer()->seat()->timestamp()); } } bool PointerInputRedirection::supportsWarping() const { if (!inited()) { return false; } if (m_supportsWarping) { return true; } if (kwinApp()->platform()->supportsPointerWarping()) { return true; } return false; } void PointerInputRedirection::updateAfterScreenChange() { if (!inited()) { return; } if (screenContainsPos(m_pos)) { // pointer still on a screen return; } // pointer no longer on a screen, reposition to closes screen const QPointF pos = screens()->geometry(screens()->number(m_pos.toPoint())).center(); // TODO: better way to get timestamps processMotion(pos, waylandServer()->seat()->timestamp()); } QImage PointerInputRedirection::cursorImage() const { if (!inited()) { return QImage(); } return m_cursor->image(); } QPoint PointerInputRedirection::cursorHotSpot() const { if (!inited()) { return QPoint(); } return m_cursor->hotSpot(); } void PointerInputRedirection::markCursorAsRendered() { if (!inited()) { return; } m_cursor->markAsRendered(); } QPointF PointerInputRedirection::position() const { return m_pos.toPoint(); } void PointerInputRedirection::setEffectsOverrideCursor(Qt::CursorShape shape) { if (!inited()) { return; } // current pointer focus window should get a leave event update(); m_cursor->setEffectsOverrideCursor(shape); } void PointerInputRedirection::removeEffectsOverrideCursor() { if (!inited()) { return; } // cursor position might have changed while there was an effect in place update(); m_cursor->removeEffectsOverrideCursor(); } void PointerInputRedirection::setWindowSelectionCursor(const QByteArray &shape) { if (!inited()) { return; } // send leave to current pointer focus window updateToReset(); m_cursor->setWindowSelectionCursor(shape); } void PointerInputRedirection::removeWindowSelectionCursor() { if (!inited()) { return; } update(); m_cursor->removeWindowSelectionCursor(); } CursorImage::CursorImage(PointerInputRedirection *parent) : QObject(parent) , m_pointer(parent) { connect(waylandServer()->seat(), &KWayland::Server::SeatInterface::focusedPointerChanged, this, &CursorImage::update); connect(waylandServer()->seat(), &KWayland::Server::SeatInterface::dragStarted, this, &CursorImage::updateDrag); connect(waylandServer()->seat(), &KWayland::Server::SeatInterface::dragEnded, this, [this] { disconnect(m_drag.connection); reevaluteSource(); } ); if (waylandServer()->hasScreenLockerIntegration()) { connect(ScreenLocker::KSldApp::self(), &ScreenLocker::KSldApp::lockStateChanged, this, &CursorImage::reevaluteSource); } connect(m_pointer, &PointerInputRedirection::decorationChanged, this, &CursorImage::updateDecoration); // connect the move resize of all window auto setupMoveResizeConnection = [this] (AbstractClient *c) { connect(c, &AbstractClient::moveResizedChanged, this, &CursorImage::updateMoveResize); connect(c, &AbstractClient::moveResizeCursorChanged, this, &CursorImage::updateMoveResize); }; const auto clients = workspace()->allClientList(); std::for_each(clients.begin(), clients.end(), setupMoveResizeConnection); connect(workspace(), &Workspace::clientAdded, this, setupMoveResizeConnection); connect(waylandServer(), &WaylandServer::shellClientAdded, this, setupMoveResizeConnection); loadThemeCursor(Qt::ArrowCursor, &m_fallbackCursor); if (m_cursorTheme) { connect(m_cursorTheme, &WaylandCursorTheme::themeChanged, this, [this] { m_cursors.clear(); m_cursorsByName.clear(); loadThemeCursor(Qt::ArrowCursor, &m_fallbackCursor); updateDecorationCursor(); updateMoveResize(); // TODO: update effects } ); } m_surfaceRenderedTimer.start(); } CursorImage::~CursorImage() = default; void CursorImage::markAsRendered() { if (m_currentSource == CursorSource::DragAndDrop) { // always sending a frame rendered to the drag icon surface to not freeze QtWayland (see https://bugreports.qt.io/browse/QTBUG-51599 ) if (auto ddi = waylandServer()->seat()->dragSource()) { if (auto s = ddi->icon()) { s->frameRendered(m_surfaceRenderedTimer.elapsed()); } } auto p = waylandServer()->seat()->dragPointer(); if (!p) { return; } auto c = p->cursor(); if (!c) { return; } auto cursorSurface = c->surface(); if (cursorSurface.isNull()) { return; } cursorSurface->frameRendered(m_surfaceRenderedTimer.elapsed()); return; } if (m_currentSource != CursorSource::LockScreen && m_currentSource != CursorSource::PointerSurface) { return; } auto p = waylandServer()->seat()->focusedPointer(); if (!p) { return; } auto c = p->cursor(); if (!c) { return; } auto cursorSurface = c->surface(); if (cursorSurface.isNull()) { return; } cursorSurface->frameRendered(m_surfaceRenderedTimer.elapsed()); } void CursorImage::update() { if (s_cursorUpdateBlocking) { return; } using namespace KWayland::Server; disconnect(m_serverCursor.connection); auto p = waylandServer()->seat()->focusedPointer(); if (p) { m_serverCursor.connection = connect(p, &PointerInterface::cursorChanged, this, &CursorImage::updateServerCursor); } else { m_serverCursor.connection = QMetaObject::Connection(); reevaluteSource(); } } void CursorImage::updateDecoration() { disconnect(m_decorationConnection); auto deco = m_pointer->decoration(); AbstractClient *c = deco.isNull() ? nullptr : deco->client(); if (c) { m_decorationConnection = connect(c, &AbstractClient::moveResizeCursorChanged, this, &CursorImage::updateDecorationCursor); } else { m_decorationConnection = QMetaObject::Connection(); } updateDecorationCursor(); } void CursorImage::updateDecorationCursor() { m_decorationCursor.image = QImage(); m_decorationCursor.hotSpot = QPoint(); auto deco = m_pointer->decoration(); if (AbstractClient *c = deco.isNull() ? nullptr : deco->client()) { loadThemeCursor(c->cursor(), &m_decorationCursor); if (m_currentSource == CursorSource::Decoration) { emit changed(); } } reevaluteSource(); } void CursorImage::updateMoveResize() { m_moveResizeCursor.image = QImage(); m_moveResizeCursor.hotSpot = QPoint(); if (AbstractClient *c = workspace()->moveResizeClient()) { loadThemeCursor(c->cursor(), &m_moveResizeCursor); if (m_currentSource == CursorSource::MoveResize) { emit changed(); } } reevaluteSource(); } void CursorImage::updateServerCursor() { m_serverCursor.image = QImage(); m_serverCursor.hotSpot = QPoint(); reevaluteSource(); const bool needsEmit = m_currentSource == CursorSource::LockScreen || m_currentSource == CursorSource::PointerSurface; auto p = waylandServer()->seat()->focusedPointer(); if (!p) { if (needsEmit) { emit changed(); } return; } auto c = p->cursor(); if (!c) { if (needsEmit) { emit changed(); } return; } auto cursorSurface = c->surface(); if (cursorSurface.isNull()) { if (needsEmit) { emit changed(); } return; } auto buffer = cursorSurface.data()->buffer(); if (!buffer) { if (needsEmit) { emit changed(); } return; } m_serverCursor.hotSpot = c->hotspot(); m_serverCursor.image = buffer->data().copy(); m_serverCursor.image.setDevicePixelRatio(cursorSurface->scale()); if (needsEmit) { emit changed(); } } void CursorImage::loadTheme() { if (m_cursorTheme) { return; } // check whether we can create it if (waylandServer()->internalShmPool()) { m_cursorTheme = new WaylandCursorTheme(waylandServer()->internalShmPool(), this); connect(waylandServer(), &WaylandServer::terminatingInternalClientConnection, this, [this] { delete m_cursorTheme; m_cursorTheme = nullptr; } ); } } void CursorImage::setEffectsOverrideCursor(Qt::CursorShape shape) { loadThemeCursor(shape, &m_effectsCursor); if (m_currentSource == CursorSource::EffectsOverride) { emit changed(); } reevaluteSource(); } void CursorImage::removeEffectsOverrideCursor() { reevaluteSource(); } void CursorImage::setWindowSelectionCursor(const QByteArray &shape) { if (shape.isEmpty()) { loadThemeCursor(Qt::CrossCursor, &m_windowSelectionCursor); } else { loadThemeCursor(shape, &m_windowSelectionCursor); } if (m_currentSource == CursorSource::WindowSelector) { emit changed(); } reevaluteSource(); } void CursorImage::removeWindowSelectionCursor() { reevaluteSource(); } void CursorImage::updateDrag() { using namespace KWayland::Server; disconnect(m_drag.connection); m_drag.cursor.image = QImage(); m_drag.cursor.hotSpot = QPoint(); reevaluteSource(); if (auto p = waylandServer()->seat()->dragPointer()) { m_drag.connection = connect(p, &PointerInterface::cursorChanged, this, &CursorImage::updateDragCursor); } else { m_drag.connection = QMetaObject::Connection(); } updateDragCursor(); } void CursorImage::updateDragCursor() { m_drag.cursor.image = QImage(); m_drag.cursor.hotSpot = QPoint(); const bool needsEmit = m_currentSource == CursorSource::DragAndDrop; QImage additionalIcon; if (auto ddi = waylandServer()->seat()->dragSource()) { if (auto dragIcon = ddi->icon()) { if (auto buffer = dragIcon->buffer()) { additionalIcon = buffer->data().copy(); additionalIcon.setOffset(dragIcon->offset()); } } } auto p = waylandServer()->seat()->dragPointer(); if (!p) { if (needsEmit) { emit changed(); } return; } auto c = p->cursor(); if (!c) { if (needsEmit) { emit changed(); } return; } auto cursorSurface = c->surface(); if (cursorSurface.isNull()) { if (needsEmit) { emit changed(); } return; } auto buffer = cursorSurface.data()->buffer(); if (!buffer) { if (needsEmit) { emit changed(); } return; } m_drag.cursor.hotSpot = c->hotspot(); if (additionalIcon.isNull()) { m_drag.cursor.image = buffer->data().copy(); } else { QRect cursorRect = buffer->data().rect(); QRect iconRect = additionalIcon.rect(); if (-m_drag.cursor.hotSpot.x() < additionalIcon.offset().x()) { iconRect.moveLeft(m_drag.cursor.hotSpot.x() - additionalIcon.offset().x()); } else { cursorRect.moveLeft(-additionalIcon.offset().x() - m_drag.cursor.hotSpot.x()); } if (-m_drag.cursor.hotSpot.y() < additionalIcon.offset().y()) { iconRect.moveTop(m_drag.cursor.hotSpot.y() - additionalIcon.offset().y()); } else { cursorRect.moveTop(-additionalIcon.offset().y() - m_drag.cursor.hotSpot.y()); } m_drag.cursor.image = QImage(cursorRect.united(iconRect).size(), QImage::Format_ARGB32_Premultiplied); m_drag.cursor.image.fill(Qt::transparent); QPainter p(&m_drag.cursor.image); p.drawImage(iconRect, additionalIcon); p.drawImage(cursorRect, buffer->data()); p.end(); } if (needsEmit) { emit changed(); } // TODO: add the cursor image } void CursorImage::loadThemeCursor(CursorShape shape, Image *image) { loadThemeCursor(shape, m_cursors, image); } void CursorImage::loadThemeCursor(const QByteArray &shape, Image *image) { loadThemeCursor(shape, m_cursorsByName, image); } template void CursorImage::loadThemeCursor(const T &shape, QHash &cursors, Image *image) { loadTheme(); if (!m_cursorTheme) { return; } auto it = cursors.constFind(shape); if (it == cursors.constEnd()) { image->image = QImage(); image->hotSpot = QPoint(); wl_cursor_image *cursor = m_cursorTheme->get(shape); if (!cursor) { return; } wl_buffer *b = wl_cursor_image_get_buffer(cursor); if (!b) { return; } waylandServer()->internalClientConection()->flush(); waylandServer()->dispatch(); auto buffer = KWayland::Server::BufferInterface::get(waylandServer()->internalConnection()->getResource(KWayland::Client::Buffer::getId(b))); if (!buffer) { return; } auto scale = screens()->maxScale(); int hotSpotX = qRound(cursor->hotspot_x / scale); int hotSpotY = qRound(cursor->hotspot_y / scale); QImage img = buffer->data().copy(); img.setDevicePixelRatio(scale); it = decltype(it)(cursors.insert(shape, {img, QPoint(hotSpotX, hotSpotY)})); } image->hotSpot = it.value().hotSpot; image->image = it.value().image; } void CursorImage::reevaluteSource() { if (waylandServer()->seat()->isDragPointer()) { // TODO: touch drag? setSource(CursorSource::DragAndDrop); return; } if (waylandServer()->isScreenLocked()) { setSource(CursorSource::LockScreen); return; } if (input()->isSelectingWindow()) { setSource(CursorSource::WindowSelector); return; } if (effects && static_cast(effects)->isMouseInterception()) { setSource(CursorSource::EffectsOverride); return; } if (workspace() && workspace()->moveResizeClient()) { setSource(CursorSource::MoveResize); return; } if (!m_pointer->decoration().isNull()) { setSource(CursorSource::Decoration); return; } if (!m_pointer->focus().isNull() && waylandServer()->seat()->focusedPointer()) { setSource(CursorSource::PointerSurface); return; } setSource(CursorSource::Fallback); } void CursorImage::setSource(CursorSource source) { if (m_currentSource == source) { return; } m_currentSource = source; emit changed(); } QImage CursorImage::image() const { switch (m_currentSource) { case CursorSource::EffectsOverride: return m_effectsCursor.image; case CursorSource::MoveResize: return m_moveResizeCursor.image; case CursorSource::LockScreen: case CursorSource::PointerSurface: // lockscreen also uses server cursor image return m_serverCursor.image; case CursorSource::Decoration: return m_decorationCursor.image; case CursorSource::DragAndDrop: return m_drag.cursor.image; case CursorSource::Fallback: return m_fallbackCursor.image; case CursorSource::WindowSelector: return m_windowSelectionCursor.image; default: Q_UNREACHABLE(); } } QPoint CursorImage::hotSpot() const { switch (m_currentSource) { case CursorSource::EffectsOverride: return m_effectsCursor.hotSpot; case CursorSource::MoveResize: return m_moveResizeCursor.hotSpot; case CursorSource::LockScreen: case CursorSource::PointerSurface: // lockscreen also uses server cursor image return m_serverCursor.hotSpot; case CursorSource::Decoration: return m_decorationCursor.hotSpot; case CursorSource::DragAndDrop: return m_drag.cursor.hotSpot; case CursorSource::Fallback: return m_fallbackCursor.hotSpot; case CursorSource::WindowSelector: return m_windowSelectionCursor.hotSpot; default: Q_UNREACHABLE(); } } } diff --git a/shadow.cpp b/shadow.cpp index 745c4bb68..85e9b2f46 100644 --- a/shadow.cpp +++ b/shadow.cpp @@ -1,501 +1,501 @@ /******************************************************************** KWin - the KDE window manager This file is part of the KDE project. Copyright (C) 2011 Martin Gräßlin Copyright (C) 2020 Vlad Zahorodnii This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . *********************************************************************/ #include "shadow.h" // kwin #include "atoms.h" #include "abstract_client.h" #include "composite.h" #include "effects.h" #include "internal_client.h" #include "toplevel.h" #include "wayland_server.h" #include #include #include #include #include #include Q_DECLARE_METATYPE(QMargins) namespace KWin { Shadow::Shadow(Toplevel *toplevel) : m_topLevel(toplevel) , m_cachedSize(toplevel->size()) , m_decorationShadow(nullptr) { - connect(m_topLevel, SIGNAL(geometryChanged()), SLOT(geometryChanged())); + connect(m_topLevel, &Toplevel::frameGeometryChanged, this, &Shadow::geometryChanged); } Shadow::~Shadow() { } Shadow *Shadow::createShadow(Toplevel *toplevel) { if (!effects) { return nullptr; } Shadow *shadow = createShadowFromDecoration(toplevel); if (!shadow && waylandServer()) { shadow = createShadowFromWayland(toplevel); } if (!shadow && kwinApp()->x11Connection()) { shadow = createShadowFromX11(toplevel); } if (!shadow) { shadow = createShadowFromInternalWindow(toplevel); } if (!shadow) { return nullptr; } if (toplevel->effectWindow() && toplevel->effectWindow()->sceneWindow()) { toplevel->effectWindow()->sceneWindow()->updateShadow(shadow); emit toplevel->shadowChanged(); } return shadow; } Shadow *Shadow::createShadowFromX11(Toplevel *toplevel) { auto data = Shadow::readX11ShadowProperty(toplevel->window()); if (!data.isEmpty()) { Shadow *shadow = Compositor::self()->scene()->createShadow(toplevel); if (!shadow->init(data)) { delete shadow; return nullptr; } return shadow; } else { return nullptr; } } Shadow *Shadow::createShadowFromDecoration(Toplevel *toplevel) { AbstractClient *c = qobject_cast(toplevel); if (!c) { return nullptr; } if (!c->decoration()) { return nullptr; } Shadow *shadow = Compositor::self()->scene()->createShadow(toplevel); if (!shadow->init(c->decoration())) { delete shadow; return nullptr; } return shadow; } Shadow *Shadow::createShadowFromWayland(Toplevel *toplevel) { auto surface = toplevel->surface(); if (!surface) { return nullptr; } const auto s = surface->shadow(); if (!s) { return nullptr; } Shadow *shadow = Compositor::self()->scene()->createShadow(toplevel); if (!shadow->init(s)) { delete shadow; return nullptr; } return shadow; } Shadow *Shadow::createShadowFromInternalWindow(Toplevel *toplevel) { const InternalClient *client = qobject_cast(toplevel); if (!client) { return nullptr; } const QWindow *window = client->internalWindow(); if (!window) { return nullptr; } Shadow *shadow = Compositor::self()->scene()->createShadow(toplevel); if (!shadow->init(window)) { delete shadow; return nullptr; } return shadow; } QVector< uint32_t > Shadow::readX11ShadowProperty(xcb_window_t id) { QVector ret; if (id != XCB_WINDOW_NONE) { Xcb::Property property(false, id, atoms->kde_net_wm_shadow, XCB_ATOM_CARDINAL, 0, 12); uint32_t *shadow = property.value(); if (shadow) { ret.reserve(12); for (int i=0; i<12; ++i) { ret << shadow[i]; } } } return ret; } bool Shadow::init(const QVector< uint32_t > &data) { QVector pixmapGeometries(ShadowElementsCount); QVector getImageCookies(ShadowElementsCount); auto *c = connection(); for (int i = 0; i < ShadowElementsCount; ++i) { pixmapGeometries[i] = Xcb::WindowGeometry(data[i]); } auto discardReplies = [&getImageCookies](int start) { for (int i = start; i < getImageCookies.size(); ++i) { xcb_discard_reply(connection(), getImageCookies.at(i).sequence); } }; for (int i = 0; i < ShadowElementsCount; ++i) { auto &geo = pixmapGeometries[i]; if (geo.isNull()) { discardReplies(0); return false; } getImageCookies[i] = xcb_get_image_unchecked(c, XCB_IMAGE_FORMAT_Z_PIXMAP, data[i], 0, 0, geo->width, geo->height, ~0); } for (int i = 0; i < ShadowElementsCount; ++i) { auto *reply = xcb_get_image_reply(c, getImageCookies.at(i), nullptr); if (!reply) { discardReplies(i+1); return false; } auto &geo = pixmapGeometries[i]; QImage image(xcb_get_image_data(reply), geo->width, geo->height, QImage::Format_ARGB32); m_shadowElements[i] = QPixmap::fromImage(image); free(reply); } m_topOffset = data[ShadowElementsCount]; m_rightOffset = data[ShadowElementsCount+1]; m_bottomOffset = data[ShadowElementsCount+2]; m_leftOffset = data[ShadowElementsCount+3]; updateShadowRegion(); if (!prepareBackend()) { return false; } buildQuads(); return true; } bool Shadow::init(KDecoration2::Decoration *decoration) { if (m_decorationShadow) { // disconnect previous connections disconnect(m_decorationShadow.data(), &KDecoration2::DecorationShadow::innerShadowRectChanged, m_topLevel, &Toplevel::updateShadow); disconnect(m_decorationShadow.data(), &KDecoration2::DecorationShadow::shadowChanged, m_topLevel, &Toplevel::updateShadow); disconnect(m_decorationShadow.data(), &KDecoration2::DecorationShadow::paddingChanged, m_topLevel, &Toplevel::updateShadow); } m_decorationShadow = decoration->shadow(); if (!m_decorationShadow) { return false; } // setup connections - all just mapped to recreate connect(m_decorationShadow.data(), &KDecoration2::DecorationShadow::innerShadowRectChanged, m_topLevel, &Toplevel::updateShadow); connect(m_decorationShadow.data(), &KDecoration2::DecorationShadow::shadowChanged, m_topLevel, &Toplevel::updateShadow); connect(m_decorationShadow.data(), &KDecoration2::DecorationShadow::paddingChanged, m_topLevel, &Toplevel::updateShadow); const QMargins &p = m_decorationShadow->padding(); m_topOffset = p.top(); m_rightOffset = p.right(); m_bottomOffset = p.bottom(); m_leftOffset = p.left(); updateShadowRegion(); if (!prepareBackend()) { return false; } buildQuads(); return true; } bool Shadow::init(const QPointer< KWayland::Server::ShadowInterface > &shadow) { if (!shadow) { return false; } m_shadowElements[ShadowElementTop] = shadow->top() ? QPixmap::fromImage(shadow->top()->data().copy()) : QPixmap(); m_shadowElements[ShadowElementTopRight] = shadow->topRight() ? QPixmap::fromImage(shadow->topRight()->data().copy()) : QPixmap(); m_shadowElements[ShadowElementRight] = shadow->right() ? QPixmap::fromImage(shadow->right()->data().copy()) : QPixmap(); m_shadowElements[ShadowElementBottomRight] = shadow->bottomRight() ? QPixmap::fromImage(shadow->bottomRight()->data().copy()) : QPixmap(); m_shadowElements[ShadowElementBottom] = shadow->bottom() ? QPixmap::fromImage(shadow->bottom()->data().copy()) : QPixmap(); m_shadowElements[ShadowElementBottomLeft] = shadow->bottomLeft() ? QPixmap::fromImage(shadow->bottomLeft()->data().copy()) : QPixmap(); m_shadowElements[ShadowElementLeft] = shadow->left() ? QPixmap::fromImage(shadow->left()->data().copy()) : QPixmap(); m_shadowElements[ShadowElementTopLeft] = shadow->topLeft() ? QPixmap::fromImage(shadow->topLeft()->data().copy()) : QPixmap(); const QMarginsF &p = shadow->offset(); m_topOffset = p.top(); m_rightOffset = p.right(); m_bottomOffset = p.bottom(); m_leftOffset = p.left(); updateShadowRegion(); if (!prepareBackend()) { return false; } buildQuads(); return true; } bool Shadow::init(const QWindow *window) { const bool isEnabled = window->property("kwin_shadow_enabled").toBool(); if (!isEnabled) { return false; } const QImage leftTile = window->property("kwin_shadow_left_tile").value(); const QImage topLeftTile = window->property("kwin_shadow_top_left_tile").value(); const QImage topTile = window->property("kwin_shadow_top_tile").value(); const QImage topRightTile = window->property("kwin_shadow_top_right_tile").value(); const QImage rightTile = window->property("kwin_shadow_right_tile").value(); const QImage bottomRightTile = window->property("kwin_shadow_bottom_right_tile").value(); const QImage bottomTile = window->property("kwin_shadow_bottom_tile").value(); const QImage bottomLeftTile = window->property("kwin_shadow_bottom_left_tile").value(); m_shadowElements[ShadowElementLeft] = QPixmap::fromImage(leftTile); m_shadowElements[ShadowElementTopLeft] = QPixmap::fromImage(topLeftTile); m_shadowElements[ShadowElementTop] = QPixmap::fromImage(topTile); m_shadowElements[ShadowElementTopRight] = QPixmap::fromImage(topRightTile); m_shadowElements[ShadowElementRight] = QPixmap::fromImage(rightTile); m_shadowElements[ShadowElementBottomRight] = QPixmap::fromImage(bottomRightTile); m_shadowElements[ShadowElementBottom] = QPixmap::fromImage(bottomTile); m_shadowElements[ShadowElementBottomLeft] = QPixmap::fromImage(bottomLeftTile); const QMargins padding = window->property("kwin_shadow_padding").value(); m_leftOffset = padding.left(); m_topOffset = padding.top(); m_rightOffset = padding.right(); m_bottomOffset = padding.bottom(); updateShadowRegion(); if (!prepareBackend()) { return false; } buildQuads(); return true; } void Shadow::updateShadowRegion() { const QRect top(0, - m_topOffset, m_topLevel->width(), m_topOffset); const QRect right(m_topLevel->width(), - m_topOffset, m_rightOffset, m_topLevel->height() + m_topOffset + m_bottomOffset); const QRect bottom(0, m_topLevel->height(), m_topLevel->width(), m_bottomOffset); const QRect left(- m_leftOffset, - m_topOffset, m_leftOffset, m_topLevel->height() + m_topOffset + m_bottomOffset); m_shadowRegion = QRegion(top).united(right).united(bottom).united(left); } void Shadow::buildQuads() { // prepare window quads m_shadowQuads.clear(); const QSize top(m_shadowElements[ShadowElementTop].size()); const QSize topRight(m_shadowElements[ShadowElementTopRight].size()); const QSize right(m_shadowElements[ShadowElementRight].size()); const QSize bottomRight(m_shadowElements[ShadowElementBottomRight].size()); const QSize bottom(m_shadowElements[ShadowElementBottom].size()); const QSize bottomLeft(m_shadowElements[ShadowElementBottomLeft].size()); const QSize left(m_shadowElements[ShadowElementLeft].size()); const QSize topLeft(m_shadowElements[ShadowElementTopLeft].size()); if ((left.width() - m_leftOffset > m_topLevel->width()) || (right.width() - m_rightOffset > m_topLevel->width()) || (top.height() - m_topOffset > m_topLevel->height()) || (bottom.height() - m_bottomOffset > m_topLevel->height())) { // if our shadow is bigger than the window, we don't render the shadow m_shadowRegion = QRegion(); return; } const QRect outerRect(QPoint(-m_leftOffset, -m_topOffset), QPoint(m_topLevel->width() + m_rightOffset, m_topLevel->height() + m_bottomOffset)); WindowQuad topLeftQuad(WindowQuadShadowTopLeft); topLeftQuad[ 0 ] = WindowVertex(outerRect.x(), outerRect.y(), 0.0, 0.0); topLeftQuad[ 1 ] = WindowVertex(outerRect.x() + topLeft.width(), outerRect.y(), 1.0, 0.0); topLeftQuad[ 2 ] = WindowVertex(outerRect.x() + topLeft.width(), outerRect.y() + topLeft.height(), 1.0, 1.0); topLeftQuad[ 3 ] = WindowVertex(outerRect.x(), outerRect.y() + topLeft.height(), 0.0, 1.0); m_shadowQuads.append(topLeftQuad); WindowQuad topQuad(WindowQuadShadowTop); topQuad[ 0 ] = WindowVertex(outerRect.x() + topLeft.width(), outerRect.y(), 0.0, 0.0); topQuad[ 1 ] = WindowVertex(outerRect.right() - topRight.width(), outerRect.y(), 1.0, 0.0); topQuad[ 2 ] = WindowVertex(outerRect.right() - topRight.width(), outerRect.y() + top.height(), 1.0, 1.0); topQuad[ 3 ] = WindowVertex(outerRect.x() + topLeft.width(), outerRect.y() + top.height(), 0.0, 1.0); m_shadowQuads.append(topQuad); WindowQuad topRightQuad(WindowQuadShadowTopRight); topRightQuad[ 0 ] = WindowVertex(outerRect.right() - topRight.width(), outerRect.y(), 0.0, 0.0); topRightQuad[ 1 ] = WindowVertex(outerRect.right(), outerRect.y(), 1.0, 0.0); topRightQuad[ 2 ] = WindowVertex(outerRect.right(), outerRect.y() + topRight.height(), 1.0, 1.0); topRightQuad[ 3 ] = WindowVertex(outerRect.right() - topRight.width(), outerRect.y() + topRight.height(), 0.0, 1.0); m_shadowQuads.append(topRightQuad); WindowQuad rightQuad(WindowQuadShadowRight); rightQuad[ 0 ] = WindowVertex(outerRect.right() - right.width(), outerRect.y() + topRight.height(), 0.0, 0.0); rightQuad[ 1 ] = WindowVertex(outerRect.right(), outerRect.y() + topRight.height(), 1.0, 0.0); rightQuad[ 2 ] = WindowVertex(outerRect.right(), outerRect.bottom() - bottomRight.height(), 1.0, 1.0); rightQuad[ 3 ] = WindowVertex(outerRect.right() - right.width(), outerRect.bottom() - bottomRight.height(), 0.0, 1.0); m_shadowQuads.append(rightQuad); WindowQuad bottomRightQuad(WindowQuadShadowBottomRight); bottomRightQuad[ 0 ] = WindowVertex(outerRect.right() - bottomRight.width(), outerRect.bottom() - bottomRight.height(), 0.0, 0.0); bottomRightQuad[ 1 ] = WindowVertex(outerRect.right(), outerRect.bottom() - bottomRight.height(), 1.0, 0.0); bottomRightQuad[ 2 ] = WindowVertex(outerRect.right(), outerRect.bottom(), 1.0, 1.0); bottomRightQuad[ 3 ] = WindowVertex(outerRect.right() - bottomRight.width(), outerRect.bottom(), 0.0, 1.0); m_shadowQuads.append(bottomRightQuad); WindowQuad bottomQuad(WindowQuadShadowBottom); bottomQuad[ 0 ] = WindowVertex(outerRect.x() + bottomLeft.width(), outerRect.bottom() - bottom.height(), 0.0, 0.0); bottomQuad[ 1 ] = WindowVertex(outerRect.right() - bottomRight.width(), outerRect.bottom() - bottom.height(), 1.0, 0.0); bottomQuad[ 2 ] = WindowVertex(outerRect.right() - bottomRight.width(), outerRect.bottom(), 1.0, 1.0); bottomQuad[ 3 ] = WindowVertex(outerRect.x() + bottomLeft.width(), outerRect.bottom(), 0.0, 1.0); m_shadowQuads.append(bottomQuad); WindowQuad bottomLeftQuad(WindowQuadShadowBottomLeft); bottomLeftQuad[ 0 ] = WindowVertex(outerRect.x(), outerRect.bottom() - bottomLeft.height(), 0.0, 0.0); bottomLeftQuad[ 1 ] = WindowVertex(outerRect.x() + bottomLeft.width(), outerRect.bottom() - bottomLeft.height(), 1.0, 0.0); bottomLeftQuad[ 2 ] = WindowVertex(outerRect.x() + bottomLeft.width(), outerRect.bottom(), 1.0, 1.0); bottomLeftQuad[ 3 ] = WindowVertex(outerRect.x(), outerRect.bottom(), 0.0, 1.0); m_shadowQuads.append(bottomLeftQuad); WindowQuad leftQuad(WindowQuadShadowLeft); leftQuad[ 0 ] = WindowVertex(outerRect.x(), outerRect.y() + topLeft.height(), 0.0, 0.0); leftQuad[ 1 ] = WindowVertex(outerRect.x() + left.width(), outerRect.y() + topLeft.height(), 1.0, 0.0); leftQuad[ 2 ] = WindowVertex(outerRect.x() + left.width(), outerRect.bottom() - bottomLeft.height(), 1.0, 1.0); leftQuad[ 3 ] = WindowVertex(outerRect.x(), outerRect.bottom() - bottomLeft.height(), 0.0, 1.0); m_shadowQuads.append(leftQuad); } bool Shadow::updateShadow() { if (!m_topLevel) { return false; } if (m_decorationShadow) { if (AbstractClient *c = qobject_cast(m_topLevel)) { if (c->decoration()) { if (init(c->decoration())) { return true; } } } return false; } if (waylandServer()) { if (m_topLevel && m_topLevel->surface()) { if (const auto &s = m_topLevel->surface()->shadow()) { if (init(s)) { return true; } } } } if (InternalClient *client = qobject_cast(m_topLevel)) { if (init(client->internalWindow())) { return true; } } auto data = Shadow::readX11ShadowProperty(m_topLevel->window()); if (data.isEmpty()) { return false; } init(data); return true; } void Shadow::setToplevel(Toplevel *topLevel) { m_topLevel = topLevel; - connect(m_topLevel, SIGNAL(geometryChanged()), SLOT(geometryChanged())); + connect(m_topLevel, &Toplevel::frameGeometryChanged, this, &Shadow::geometryChanged); } void Shadow::geometryChanged() { if (m_cachedSize == m_topLevel->size()) { return; } m_cachedSize = m_topLevel->size(); updateShadowRegion(); buildQuads(); } QImage Shadow::decorationShadowImage() const { if (!m_decorationShadow) { return QImage(); } return m_decorationShadow->shadow(); } QSize Shadow::elementSize(Shadow::ShadowElements element) const { if (m_decorationShadow) { switch (element) { case ShadowElementTop: return m_decorationShadow->topGeometry().size(); case ShadowElementTopRight: return m_decorationShadow->topRightGeometry().size(); case ShadowElementRight: return m_decorationShadow->rightGeometry().size(); case ShadowElementBottomRight: return m_decorationShadow->bottomRightGeometry().size(); case ShadowElementBottom: return m_decorationShadow->bottomGeometry().size(); case ShadowElementBottomLeft: return m_decorationShadow->bottomLeftGeometry().size(); case ShadowElementLeft: return m_decorationShadow->leftGeometry().size(); case ShadowElementTopLeft: return m_decorationShadow->topLeftGeometry().size(); default: return QSize(); } } else { return m_shadowElements[element].size(); } } void Shadow::setShadowElement(const QPixmap &shadow, Shadow::ShadowElements element) { m_shadowElements[element] = shadow; } } // namespace diff --git a/toplevel.cpp b/toplevel.cpp index 56d4668f9..387a8a50f 100644 --- a/toplevel.cpp +++ b/toplevel.cpp @@ -1,786 +1,787 @@ /******************************************************************** KWin - the KDE window manager This file is part of the KDE project. Copyright (C) 2006 Lubos Lunak This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . *********************************************************************/ #include "toplevel.h" #ifdef KWIN_BUILD_ACTIVITIES #include "activities.h" #endif #include "atoms.h" #include "client_machine.h" #include "composite.h" #include "effects.h" #include "screens.h" #include "shadow.h" #include "workspace.h" #include "xcbutils.h" #include #include namespace KWin { Toplevel::Toplevel() : m_visual(XCB_NONE) , bit_depth(24) , info(nullptr) , ready_for_painting(false) , m_isDamaged(false) , m_internalId(QUuid::createUuid()) , m_client() , damage_handle(XCB_NONE) , is_shape(false) , effect_window(nullptr) , m_clientMachine(new ClientMachine(this)) , m_wmClientLeader(XCB_WINDOW_NONE) , m_damageReplyPending(false) , m_screen(0) , m_skipCloseAnimation(false) { connect(this, SIGNAL(damaged(KWin::Toplevel*,QRect)), SIGNAL(needsRepaint())); connect(screens(), SIGNAL(changed()), SLOT(checkScreen())); connect(screens(), SIGNAL(countChanged(int,int)), SLOT(checkScreen())); setupCheckScreenConnection(); + + // Only for compatibility reasons, drop in the next major release. + connect(this, &Toplevel::frameGeometryChanged, this, &Toplevel::geometryChanged); } Toplevel::~Toplevel() { Q_ASSERT(damage_handle == XCB_NONE); delete info; } QDebug& operator<<(QDebug& stream, const Toplevel* cl) { if (cl == nullptr) return stream << "\'NULL\'"; cl->debug(stream); return stream; } void Toplevel::detectShape(xcb_window_t id) { const bool wasShape = is_shape; is_shape = Xcb::Extensions::self()->hasShape(id); if (wasShape != is_shape) { emit shapedChanged(); } } // used only by Deleted::copy() void Toplevel::copyToDeleted(Toplevel* c) { m_internalId = c->internalId(); m_frameGeometry = c->m_frameGeometry; m_visual = c->m_visual; bit_depth = c->bit_depth; info = c->info; m_client.reset(c->m_client, false); ready_for_painting = c->ready_for_painting; damage_handle = XCB_NONE; damage_region = c->damage_region; repaints_region = c->repaints_region; layer_repaints_region = c->layer_repaints_region; is_shape = c->is_shape; effect_window = c->effect_window; if (effect_window != nullptr) effect_window->setWindow(this); resource_name = c->resourceName(); resource_class = c->resourceClass(); m_clientMachine = c->m_clientMachine; m_clientMachine->setParent(this); m_wmClientLeader = c->wmClientLeader(); opaque_region = c->opaqueRegion(); m_screen = c->m_screen; m_skipCloseAnimation = c->m_skipCloseAnimation; m_internalFBO = c->m_internalFBO; m_internalImage = c->m_internalImage; } // before being deleted, remove references to everything that's now // owner by Deleted void Toplevel::disownDataPassedToDeleted() { info = nullptr; } QRect Toplevel::visibleRect() const { // There's no strict order between frame geometry and buffer geometry. QRect rect = frameGeometry() | bufferGeometry(); if (shadow() && !shadow()->shadowRegion().isEmpty()) { rect |= shadow()->shadowRegion().boundingRect().translated(pos()); } return rect; } Xcb::Property Toplevel::fetchWmClientLeader() const { return Xcb::Property(false, window(), atoms->wm_client_leader, XCB_ATOM_WINDOW, 0, 10000); } void Toplevel::readWmClientLeader(Xcb::Property &prop) { m_wmClientLeader = prop.value(window()); } void Toplevel::getWmClientLeader() { auto prop = fetchWmClientLeader(); readWmClientLeader(prop); } /** * Returns sessionId for this client, * taken either from its window or from the leader window. */ QByteArray Toplevel::sessionId() const { QByteArray result = Xcb::StringProperty(window(), atoms->sm_client_id); if (result.isEmpty() && m_wmClientLeader && m_wmClientLeader != window()) { result = Xcb::StringProperty(m_wmClientLeader, atoms->sm_client_id); } return result; } /** * Returns command property for this client, * taken either from its window or from the leader window. */ QByteArray Toplevel::wmCommand() { QByteArray result = Xcb::StringProperty(window(), XCB_ATOM_WM_COMMAND); if (result.isEmpty() && m_wmClientLeader && m_wmClientLeader != window()) { result = Xcb::StringProperty(m_wmClientLeader, XCB_ATOM_WM_COMMAND); } result.replace(0, ' '); return result; } void Toplevel::getWmClientMachine() { m_clientMachine->resolve(window(), wmClientLeader()); } /** * Returns client machine for this client, * taken either from its window or from the leader window. */ QByteArray Toplevel::wmClientMachine(bool use_localhost) const { if (!m_clientMachine) { // this should never happen return QByteArray(); } if (use_localhost && m_clientMachine->isLocal()) { // special name for the local machine (localhost) return ClientMachine::localhost(); } return m_clientMachine->hostName(); } /** * Returns client leader window for this client. * Returns the client window itself if no leader window is defined. */ xcb_window_t Toplevel::wmClientLeader() const { if (m_wmClientLeader != XCB_WINDOW_NONE) { return m_wmClientLeader; } return window(); } void Toplevel::getResourceClass() { setResourceClass(QByteArray(info->windowClassName()).toLower(), QByteArray(info->windowClassClass()).toLower()); } void Toplevel::setResourceClass(const QByteArray &name, const QByteArray &className) { resource_name = name; resource_class = className; emit windowClassChanged(); } bool Toplevel::resourceMatch(const Toplevel *c1, const Toplevel *c2) { return c1->resourceClass() == c2->resourceClass(); } double Toplevel::opacity() const { if (info->opacity() == 0xffffffff) return 1.0; return info->opacity() * 1.0 / 0xffffffff; } void Toplevel::setOpacity(double new_opacity) { double old_opacity = opacity(); new_opacity = qBound(0.0, new_opacity, 1.0); if (old_opacity == new_opacity) return; info->setOpacity(static_cast< unsigned long >(new_opacity * 0xffffffff)); if (compositing()) { addRepaintFull(); emit opacityChanged(this, old_opacity); } } bool Toplevel::setupCompositing() { if (!compositing()) return false; if (damage_handle != XCB_NONE) return false; if (kwinApp()->operationMode() == Application::OperationModeX11 && !surface()) { damage_handle = xcb_generate_id(connection()); xcb_damage_create(connection(), damage_handle, frameId(), XCB_DAMAGE_REPORT_LEVEL_NON_EMPTY); } damage_region = QRegion(0, 0, width(), height()); effect_window = new EffectWindowImpl(this); Compositor::self()->scene()->addToplevel(this); return true; } void Toplevel::finishCompositing(ReleaseReason releaseReason) { if (kwinApp()->operationMode() == Application::OperationModeX11 && damage_handle == XCB_NONE) return; if (effect_window->window() == this) { // otherwise it's already passed to Deleted, don't free data discardWindowPixmap(); delete effect_window; } if (damage_handle != XCB_NONE && releaseReason != ReleaseReason::Destroyed) { xcb_damage_destroy(connection(), damage_handle); } damage_handle = XCB_NONE; damage_region = QRegion(); repaints_region = QRegion(); effect_window = nullptr; } void Toplevel::discardWindowPixmap() { addDamageFull(); if (effectWindow() != nullptr && effectWindow()->sceneWindow() != nullptr) effectWindow()->sceneWindow()->discardPixmap(); } void Toplevel::damageNotifyEvent() { m_isDamaged = true; // Note: The rect is supposed to specify the damage extents, // but we don't know it at this point. No one who connects // to this signal uses the rect however. emit damaged(this, QRect()); } bool Toplevel::compositing() const { if (!Workspace::self()) { return false; } return Workspace::self()->compositing(); } bool Toplevel::resetAndFetchDamage() { if (!m_isDamaged) return false; if (damage_handle == XCB_NONE) { m_isDamaged = false; return true; } xcb_connection_t *conn = connection(); // Create a new region and copy the damage region to it, // resetting the damaged state. xcb_xfixes_region_t region = xcb_generate_id(conn); xcb_xfixes_create_region(conn, region, 0, nullptr); xcb_damage_subtract(conn, damage_handle, 0, region); // Send a fetch-region request and destroy the region m_regionCookie = xcb_xfixes_fetch_region_unchecked(conn, region); xcb_xfixes_destroy_region(conn, region); m_isDamaged = false; m_damageReplyPending = true; return m_damageReplyPending; } void Toplevel::getDamageRegionReply() { if (!m_damageReplyPending) return; m_damageReplyPending = false; // Get the fetch-region reply xcb_xfixes_fetch_region_reply_t *reply = xcb_xfixes_fetch_region_reply(connection(), m_regionCookie, nullptr); if (!reply) return; // Convert the reply to a QRegion int count = xcb_xfixes_fetch_region_rectangles_length(reply); QRegion region; if (count > 1 && count < 16) { xcb_rectangle_t *rects = xcb_xfixes_fetch_region_rectangles(reply); QVector qrects; qrects.reserve(count); for (int i = 0; i < count; i++) qrects << QRect(rects[i].x, rects[i].y, rects[i].width, rects[i].height); region.setRects(qrects.constData(), count); } else region += QRect(reply->extents.x, reply->extents.y, reply->extents.width, reply->extents.height); const QRect bufferRect = bufferGeometry(); const QRect frameRect = frameGeometry(); damage_region += region; repaints_region += region.translated(bufferRect.topLeft() - frameRect.topLeft()); free(reply); } void Toplevel::addDamageFull() { if (!compositing()) return; const QRect bufferRect = bufferGeometry(); const QRect frameRect = frameGeometry(); const int offsetX = bufferRect.x() - frameRect.x(); const int offsetY = bufferRect.y() - frameRect.y(); const QRect damagedRect = QRect(0, 0, bufferRect.width(), bufferRect.height()); damage_region = damagedRect; repaints_region |= damagedRect.translated(offsetX, offsetY); emit damaged(this, damagedRect); } void Toplevel::resetDamage() { damage_region = QRegion(); } void Toplevel::addRepaint(const QRect& r) { if (!compositing()) { return; } repaints_region += r; emit needsRepaint(); } void Toplevel::addRepaint(int x, int y, int w, int h) { QRect r(x, y, w, h); addRepaint(r); } void Toplevel::addRepaint(const QRegion& r) { if (!compositing()) { return; } repaints_region += r; emit needsRepaint(); } void Toplevel::addLayerRepaint(const QRect& r) { if (!compositing()) { return; } layer_repaints_region += r; emit needsRepaint(); } void Toplevel::addLayerRepaint(int x, int y, int w, int h) { QRect r(x, y, w, h); addLayerRepaint(r); } void Toplevel::addLayerRepaint(const QRegion& r) { if (!compositing()) return; layer_repaints_region += r; emit needsRepaint(); } void Toplevel::addRepaintFull() { repaints_region = visibleRect().translated(-pos()); emit needsRepaint(); } void Toplevel::resetRepaints() { repaints_region = QRegion(); layer_repaints_region = QRegion(); } void Toplevel::addWorkspaceRepaint(int x, int y, int w, int h) { addWorkspaceRepaint(QRect(x, y, w, h)); } void Toplevel::addWorkspaceRepaint(const QRect& r2) { if (!compositing()) return; Compositor::self()->addRepaint(r2); } void Toplevel::setReadyForPainting() { if (!ready_for_painting) { ready_for_painting = true; if (compositing()) { addRepaintFull(); emit windowShown(this); } } } void Toplevel::deleteEffectWindow() { delete effect_window; effect_window = nullptr; } void Toplevel::checkScreen() { if (screens()->count() == 1) { if (m_screen != 0) { m_screen = 0; emit screenChanged(); } } else { const int s = screens()->number(frameGeometry().center()); if (s != m_screen) { m_screen = s; emit screenChanged(); } } qreal newScale = screens()->scale(m_screen); if (newScale != m_screenScale) { m_screenScale = newScale; emit screenScaleChanged(); } } void Toplevel::setupCheckScreenConnection() { - connect(this, SIGNAL(geometryShapeChanged(KWin::Toplevel*,QRect)), SLOT(checkScreen())); - connect(this, SIGNAL(geometryChanged()), SLOT(checkScreen())); + connect(this, &Toplevel::frameGeometryChanged, this, &Toplevel::checkScreen); checkScreen(); } void Toplevel::removeCheckScreenConnection() { - disconnect(this, SIGNAL(geometryShapeChanged(KWin::Toplevel*,QRect)), this, SLOT(checkScreen())); - disconnect(this, SIGNAL(geometryChanged()), this, SLOT(checkScreen())); + disconnect(this, &Toplevel::frameGeometryChanged, this, &Toplevel::checkScreen); } int Toplevel::screen() const { return m_screen; } qreal Toplevel::screenScale() const { return m_screenScale; } qreal Toplevel::bufferScale() const { return surface() ? surface()->scale() : 1; } bool Toplevel::isOnScreen(int screen) const { return screens()->geometry(screen).intersects(frameGeometry()); } bool Toplevel::isOnActiveScreen() const { return isOnScreen(screens()->current()); } void Toplevel::updateShadow() { QRect dirtyRect; // old & new shadow region const QRect oldVisibleRect = visibleRect(); if (shadow()) { dirtyRect = shadow()->shadowRegion().boundingRect(); if (!effectWindow()->sceneWindow()->shadow()->updateShadow()) { effectWindow()->sceneWindow()->updateShadow(nullptr); } emit shadowChanged(); } else { Shadow::createShadow(this); } if (shadow()) dirtyRect |= shadow()->shadowRegion().boundingRect(); if (oldVisibleRect != visibleRect()) emit paddingChanged(this, oldVisibleRect); if (dirtyRect.isValid()) { dirtyRect.translate(pos()); addLayerRepaint(dirtyRect); } } Shadow *Toplevel::shadow() { if (effectWindow() && effectWindow()->sceneWindow()) { return effectWindow()->sceneWindow()->shadow(); } else { return nullptr; } } const Shadow *Toplevel::shadow() const { if (effectWindow() && effectWindow()->sceneWindow()) { return effectWindow()->sceneWindow()->shadow(); } else { return nullptr; } } bool Toplevel::wantsShadowToBeRendered() const { return true; } void Toplevel::getWmOpaqueRegion() { const auto rects = info->opaqueRegion(); QRegion new_opaque_region; for (const auto &r : rects) { new_opaque_region += QRect(r.pos.x, r.pos.y, r.size.width, r.size.height); } opaque_region = new_opaque_region; } bool Toplevel::isClient() const { return false; } bool Toplevel::isDeleted() const { return false; } bool Toplevel::isOnCurrentActivity() const { #ifdef KWIN_BUILD_ACTIVITIES if (!Activities::self()) { return true; } return isOnActivity(Activities::self()->current()); #else return true; #endif } void Toplevel::elevate(bool elevate) { if (!effectWindow()) { return; } effectWindow()->elevate(elevate); addWorkspaceRepaint(visibleRect()); } pid_t Toplevel::pid() const { return info->pid(); } xcb_window_t Toplevel::frameId() const { return m_client; } Xcb::Property Toplevel::fetchSkipCloseAnimation() const { return Xcb::Property(false, window(), atoms->kde_skip_close_animation, XCB_ATOM_CARDINAL, 0, 1); } void Toplevel::readSkipCloseAnimation(Xcb::Property &property) { setSkipCloseAnimation(property.toBool()); } void Toplevel::getSkipCloseAnimation() { Xcb::Property property = fetchSkipCloseAnimation(); readSkipCloseAnimation(property); } bool Toplevel::skipsCloseAnimation() const { return m_skipCloseAnimation; } void Toplevel::setSkipCloseAnimation(bool set) { if (set == m_skipCloseAnimation) { return; } m_skipCloseAnimation = set; emit skipCloseAnimationChanged(); } void Toplevel::setSurface(KWayland::Server::SurfaceInterface *surface) { if (m_surface == surface) { return; } using namespace KWayland::Server; if (m_surface) { disconnect(m_surface, &SurfaceInterface::damaged, this, &Toplevel::addDamage); disconnect(m_surface, &SurfaceInterface::sizeChanged, this, &Toplevel::discardWindowPixmap); } m_surface = surface; connect(m_surface, &SurfaceInterface::damaged, this, &Toplevel::addDamage); connect(m_surface, &SurfaceInterface::sizeChanged, this, &Toplevel::discardWindowPixmap); connect(m_surface, &SurfaceInterface::subSurfaceTreeChanged, this, [this] { // TODO improve to only update actual visual area if (ready_for_painting) { addDamageFull(); m_isDamaged = true; } } ); connect(m_surface, &SurfaceInterface::destroyed, this, [this] { m_surface = nullptr; } ); emit surfaceChanged(); } void Toplevel::addDamage(const QRegion &damage) { m_isDamaged = true; damage_region += damage; for (const QRect &r : damage) { emit damaged(this, r); } } QByteArray Toplevel::windowRole() const { return QByteArray(info->windowRole()); } void Toplevel::setDepth(int depth) { if (bit_depth == depth) { return; } const bool oldAlpha = hasAlpha(); bit_depth = depth; if (oldAlpha != hasAlpha()) { emit hasAlphaChanged(); } } QRegion Toplevel::inputShape() const { if (m_surface) { return m_surface->input(); } else { // TODO: maybe also for X11? return QRegion(); } } QMatrix4x4 Toplevel::inputTransformation() const { QMatrix4x4 m; m.translate(-x(), -y()); return m; } quint32 Toplevel::windowId() const { return window(); } QRect Toplevel::inputGeometry() const { return frameGeometry(); } bool Toplevel::isLocalhost() const { if (!m_clientMachine) { return true; } return m_clientMachine->isLocal(); } QMargins Toplevel::bufferMargins() const { return QMargins(); } QMargins Toplevel::frameMargins() const { return QMargins(); } } // namespace diff --git a/toplevel.h b/toplevel.h index 2cf6bbc0e..b5c43dab6 100644 --- a/toplevel.h +++ b/toplevel.h @@ -1,1043 +1,1052 @@ /******************************************************************** KWin - the KDE window manager This file is part of the KDE project. Copyright (C) 2006 Lubos Lunak This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . *********************************************************************/ #ifndef KWIN_TOPLEVEL_H #define KWIN_TOPLEVEL_H // kwin #include "input.h" #include "utils.h" #include "virtualdesktops.h" #include "xcbutils.h" // KDE #include // Qt #include #include #include // xcb #include #include // c++ #include class QOpenGLFramebufferObject; namespace KWayland { namespace Server { class SurfaceInterface; } } namespace KWin { class ClientMachine; class Deleted; class EffectWindowImpl; class Shadow; /** * Enum to describe the reason why a Toplevel has to be released. */ enum class ReleaseReason { Release, ///< Normal Release after e.g. an Unmap notify event (window still valid) Destroyed, ///< Release after an Destroy notify event (window no longer valid) KWinShutsDown ///< Release on KWin Shutdown (window still valid) }; class KWIN_EXPORT Toplevel : public QObject { Q_OBJECT Q_PROPERTY(bool alpha READ hasAlpha NOTIFY hasAlphaChanged) Q_PROPERTY(qulonglong frameId READ frameId) /** * This property holds the geometry of the Toplevel, excluding invisible * portions, e.g. client-side and server-side drop-shadows, etc. * * @deprecated Use frameGeometry property instead. */ - Q_PROPERTY(QRect geometry READ frameGeometry NOTIFY geometryChanged) + Q_PROPERTY(QRect geometry READ frameGeometry NOTIFY frameGeometryChanged) /** * This property holds rectangle that the pixmap or buffer of this Toplevel * occupies on the screen. This rectangle includes invisible portions of the * client, e.g. client-side drop shadows, etc. */ - Q_PROPERTY(QRect bufferGeometry READ bufferGeometry NOTIFY geometryChanged) + Q_PROPERTY(QRect bufferGeometry READ bufferGeometry) /** * This property holds the geometry of the Toplevel, excluding invisible * portions, e.g. server-side and client-side drop-shadows, etc. */ - Q_PROPERTY(QRect frameGeometry READ frameGeometry NOTIFY geometryChanged) + Q_PROPERTY(QRect frameGeometry READ frameGeometry NOTIFY frameGeometryChanged) /** * This property holds the position of the Toplevel's frame geometry. */ Q_PROPERTY(QPoint pos READ pos) /** * This property holds the size of the Toplevel's frame geometry. */ Q_PROPERTY(QSize size READ size) /** * This property holds the x position of the Toplevel's frame geometry. */ Q_PROPERTY(int x READ x) /** * This property holds the y position of the Toplevel's frame geometry. */ Q_PROPERTY(int y READ y) /** * This property holds the width of the Toplevel's frame geometry. */ Q_PROPERTY(int width READ width) /** * This property holds the height of the Toplevel's frame geometry. */ Q_PROPERTY(int height READ height) Q_PROPERTY(QRect visibleRect READ visibleRect) Q_PROPERTY(qreal opacity READ opacity WRITE setOpacity NOTIFY opacityChanged) Q_PROPERTY(int screen READ screen NOTIFY screenChanged) Q_PROPERTY(qulonglong windowId READ windowId CONSTANT) Q_PROPERTY(int desktop READ desktop) /** * Whether the window is on all desktops. That is desktop is -1. */ Q_PROPERTY(bool onAllDesktops READ isOnAllDesktops) Q_PROPERTY(QRect rect READ rect) Q_PROPERTY(QPoint clientPos READ clientPos) Q_PROPERTY(QSize clientSize READ clientSize) Q_PROPERTY(QByteArray resourceName READ resourceName NOTIFY windowClassChanged) Q_PROPERTY(QByteArray resourceClass READ resourceClass NOTIFY windowClassChanged) Q_PROPERTY(QByteArray windowRole READ windowRole NOTIFY windowRoleChanged) /** * Returns whether the window is a desktop background window (the one with wallpaper). * See _NET_WM_WINDOW_TYPE_DESKTOP at https://standards.freedesktop.org/wm-spec/wm-spec-latest.html . */ Q_PROPERTY(bool desktopWindow READ isDesktop) /** * Returns whether the window is a dock (i.e. a panel). * See _NET_WM_WINDOW_TYPE_DOCK at https://standards.freedesktop.org/wm-spec/wm-spec-latest.html . */ Q_PROPERTY(bool dock READ isDock) /** * Returns whether the window is a standalone (detached) toolbar window. * See _NET_WM_WINDOW_TYPE_TOOLBAR at https://standards.freedesktop.org/wm-spec/wm-spec-latest.html . */ Q_PROPERTY(bool toolbar READ isToolbar) /** * Returns whether the window is a torn-off menu. * See _NET_WM_WINDOW_TYPE_MENU at https://standards.freedesktop.org/wm-spec/wm-spec-latest.html . */ Q_PROPERTY(bool menu READ isMenu) /** * Returns whether the window is a "normal" window, i.e. an application or any other window * for which none of the specialized window types fit. * See _NET_WM_WINDOW_TYPE_NORMAL at https://standards.freedesktop.org/wm-spec/wm-spec-latest.html . */ Q_PROPERTY(bool normalWindow READ isNormalWindow) /** * Returns whether the window is a dialog window. * See _NET_WM_WINDOW_TYPE_DIALOG at https://standards.freedesktop.org/wm-spec/wm-spec-latest.html . */ Q_PROPERTY(bool dialog READ isDialog) /** * Returns whether the window is a splashscreen. Note that many (especially older) applications * do not support marking their splash windows with this type. * See _NET_WM_WINDOW_TYPE_SPLASH at https://standards.freedesktop.org/wm-spec/wm-spec-latest.html . */ Q_PROPERTY(bool splash READ isSplash) /** * Returns whether the window is a utility window, such as a tool window. * See _NET_WM_WINDOW_TYPE_UTILITY at https://standards.freedesktop.org/wm-spec/wm-spec-latest.html . */ Q_PROPERTY(bool utility READ isUtility) /** * Returns whether the window is a dropdown menu (i.e. a popup directly or indirectly open * from the applications menubar). * See _NET_WM_WINDOW_TYPE_DROPDOWN_MENU at https://standards.freedesktop.org/wm-spec/wm-spec-latest.html . */ Q_PROPERTY(bool dropdownMenu READ isDropdownMenu) /** * Returns whether the window is a popup menu (that is not a torn-off or dropdown menu). * See _NET_WM_WINDOW_TYPE_POPUP_MENU at https://standards.freedesktop.org/wm-spec/wm-spec-latest.html . */ Q_PROPERTY(bool popupMenu READ isPopupMenu) /** * Returns whether the window is a tooltip. * See _NET_WM_WINDOW_TYPE_TOOLTIP at https://standards.freedesktop.org/wm-spec/wm-spec-latest.html . */ Q_PROPERTY(bool tooltip READ isTooltip) /** * Returns whether the window is a window with a notification. * See _NET_WM_WINDOW_TYPE_NOTIFICATION at https://standards.freedesktop.org/wm-spec/wm-spec-latest.html . */ Q_PROPERTY(bool notification READ isNotification) /** * Returns whether the window is a window with a critical notification. */ Q_PROPERTY(bool criticalNotification READ isCriticalNotification) /** * Returns whether the window is an On Screen Display. */ Q_PROPERTY(bool onScreenDisplay READ isOnScreenDisplay) /** * Returns whether the window is a combobox popup. * See _NET_WM_WINDOW_TYPE_COMBO at https://standards.freedesktop.org/wm-spec/wm-spec-latest.html . */ Q_PROPERTY(bool comboBox READ isComboBox) /** * Returns whether the window is a Drag&Drop icon. * See _NET_WM_WINDOW_TYPE_DND at https://standards.freedesktop.org/wm-spec/wm-spec-latest.html . */ Q_PROPERTY(bool dndIcon READ isDNDIcon) /** * Returns the NETWM window type * See https://standards.freedesktop.org/wm-spec/wm-spec-latest.html . */ Q_PROPERTY(int windowType READ windowType) Q_PROPERTY(QStringList activities READ activities NOTIFY activitiesChanged) /** * Whether this Toplevel is managed by KWin (it has control over its placement and other * aspects, as opposed to override-redirect windows that are entirely handled by the application). */ Q_PROPERTY(bool managed READ isClient CONSTANT) /** * Whether this Toplevel represents an already deleted window and only kept for the compositor for animations. */ Q_PROPERTY(bool deleted READ isDeleted CONSTANT) /** * Whether the window has an own shape */ Q_PROPERTY(bool shaped READ shape NOTIFY shapedChanged) /** * Whether the window does not want to be animated on window close. * There are legit reasons for this like a screenshot application which does not want it's * window being captured. */ Q_PROPERTY(bool skipsCloseAnimation READ skipsCloseAnimation WRITE setSkipCloseAnimation NOTIFY skipCloseAnimationChanged) /** * The Id of the Wayland Surface associated with this Toplevel. * On X11 only setups the value is @c 0. */ Q_PROPERTY(quint32 surfaceId READ surfaceId NOTIFY surfaceIdChanged) /** * Interface to the Wayland Surface. * Relevant only in Wayland, in X11 it will be nullptr */ Q_PROPERTY(KWayland::Server::SurfaceInterface *surface READ surface) /** * Whether the window is a popup. */ Q_PROPERTY(bool popupWindow READ isPopupWindow) /** * Whether this Toplevel represents the outline. * * @note It's always @c false if compositing is turned off. */ Q_PROPERTY(bool outline READ isOutline) /** * This property holds a UUID to uniquely identify this Toplevel. */ Q_PROPERTY(QUuid internalId READ internalId CONSTANT) public: explicit Toplevel(); virtual xcb_window_t frameId() const; xcb_window_t window() const; /** * @return a unique identifier for the Toplevel. On X11 same as @ref window */ virtual quint32 windowId() const; /** * Returns the geometry of the pixmap or buffer attached to this Toplevel. * * For X11 clients, this method returns server-side geometry of the Toplevel. * * For Wayland clients, this method returns rectangle that the main surface * occupies on the screen, in global screen coordinates. */ virtual QRect bufferGeometry() const = 0; /** * Returns the extents of invisible portions in the pixmap. * * An X11 pixmap may contain invisible space around the actual contents of the * client. That space is reserved for server-side decoration, which we usually * want to skip when building contents window quads. * * Default implementation returns a margins object with all margins set to 0. */ virtual QMargins bufferMargins() const; /** * Returns the geometry of the Toplevel, excluding invisible portions, e.g. * server-side and client-side drop shadows, etc. */ QRect frameGeometry() const; /** * Returns the extents of the server-side decoration. * * Note that the returned margins object will have all margins set to 0 if * the client doesn't have a server-side decoration. * * Default implementation returns a margins object with all margins set to 0. */ virtual QMargins frameMargins() const; /** * The geometry of the Toplevel which accepts input events. This might be larger * than the actual geometry, e.g. to support resizing outside the window. * * Default implementation returns same as geometry. */ virtual QRect inputGeometry() const; QSize size() const; QPoint pos() const; QRect rect() const; int x() const; int y() const; int width() const; int height() const; bool isOnScreen(int screen) const; // true if it's at least partially there bool isOnActiveScreen() const; int screen() const; // the screen where the center is /** * The scale of the screen this window is currently on * @note The buffer scale can be different. * @since 5.12 */ qreal screenScale() const; // /** * Returns the ratio between physical pixels and device-independent pixels for * the attached buffer (or pixmap). * * For X11 clients, this method always returns 1. */ virtual qreal bufferScale() const; virtual QPoint clientPos() const = 0; // inside of geometry() /** * Describes how the client's content maps to the window geometry including the frame. * The default implementation is a 1:1 mapping meaning the frame is part of the content. */ virtual QPoint clientContentPos() const; virtual QSize clientSize() const = 0; /** * Returns a rectangle that the window occupies on the screen, including drop-shadows. */ virtual QRect visibleRect() const; virtual QRect transparentRect() const = 0; virtual bool isClient() const; virtual bool isDeleted() const; // prefer isXXX() instead // 0 for supported types means default for managed/unmanaged types virtual NET::WindowType windowType(bool direct = false, int supported_types = 0) const = 0; bool hasNETSupport() const; bool isDesktop() const; bool isDock() const; bool isToolbar() const; bool isMenu() const; bool isNormalWindow() const; // normal as in 'NET::Normal or NET::Unknown non-transient' bool isDialog() const; bool isSplash() const; bool isUtility() const; bool isDropdownMenu() const; bool isPopupMenu() const; // a context popup, not dropdown, not torn-off bool isTooltip() const; bool isNotification() const; bool isCriticalNotification() const; bool isOnScreenDisplay() const; bool isComboBox() const; bool isDNDIcon() const; virtual bool isLockScreen() const; virtual bool isInputMethod() const; virtual bool isOutline() const; /** * Returns the virtual desktop within the workspace() the client window * is located in, 0 if it isn't located on any special desktop (not mapped yet), * or NET::OnAllDesktops. Do not use desktop() directly, use * isOnDesktop() instead. */ virtual int desktop() const = 0; virtual QVector desktops() const = 0; virtual QStringList activities() const = 0; bool isOnDesktop(int d) const; bool isOnActivity(const QString &activity) const; bool isOnCurrentDesktop() const; bool isOnCurrentActivity() const; bool isOnAllDesktops() const; bool isOnAllActivities() const; virtual QByteArray windowRole() const; QByteArray sessionId() const; QByteArray resourceName() const; QByteArray resourceClass() const; QByteArray wmCommand(); QByteArray wmClientMachine(bool use_localhost) const; const ClientMachine *clientMachine() const; virtual bool isLocalhost() const; xcb_window_t wmClientLeader() const; virtual pid_t pid() const; static bool resourceMatch(const Toplevel* c1, const Toplevel* c2); bool readyForPainting() const; // true if the window has been already painted its contents xcb_visualid_t visual() const; bool shape() const; QRegion inputShape() const; virtual void setOpacity(double opacity); virtual double opacity() const; int depth() const; bool hasAlpha() const; virtual bool setupCompositing(); virtual void finishCompositing(ReleaseReason releaseReason = ReleaseReason::Release); Q_INVOKABLE void addRepaint(const QRect& r); Q_INVOKABLE void addRepaint(const QRegion& r); Q_INVOKABLE void addRepaint(int x, int y, int w, int h); Q_INVOKABLE void addLayerRepaint(const QRect& r); Q_INVOKABLE void addLayerRepaint(const QRegion& r); Q_INVOKABLE void addLayerRepaint(int x, int y, int w, int h); Q_INVOKABLE virtual void addRepaintFull(); // these call workspace->addRepaint(), but first transform the damage if needed void addWorkspaceRepaint(const QRect& r); void addWorkspaceRepaint(int x, int y, int w, int h); QRegion repaints() const; void resetRepaints(); QRegion damage() const; void resetDamage(); EffectWindowImpl* effectWindow(); const EffectWindowImpl* effectWindow() const; /** * Window will be temporarily painted as if being at the top of the stack. * Only available if Compositor is active, if not active, this method is a no-op. */ void elevate(bool elevate); /** * Returns the pointer to the Toplevel's Shadow. A Shadow * is only available if Compositing is enabled and the corresponding X window * has the Shadow property set. * @returns The Shadow belonging to this Toplevel, @c null if there's no Shadow. */ const Shadow *shadow() const; Shadow *shadow(); /** * Updates the Shadow associated with this Toplevel from X11 Property. * Call this method when the Property changes or Compositing is started. */ void updateShadow(); /** * Whether the Toplevel currently wants the shadow to be rendered. Default * implementation always returns @c true. */ virtual bool wantsShadowToBeRendered() const; /** * This method returns the area that the Toplevel window reports to be opaque. * It is supposed to only provide valuable information if hasAlpha is @c true . * @see hasAlpha */ const QRegion& opaqueRegion() const; virtual Layer layer() const = 0; /** * Resets the damage state and sends a request for the damage region. * A call to this function must be followed by a call to getDamageRegionReply(), * or the reply will be leaked. * * Returns true if the window was damaged, and false otherwise. */ bool resetAndFetchDamage(); /** * Gets the reply from a previous call to resetAndFetchDamage(). * Calling this function is a no-op if there is no pending reply. * Call damage() to return the fetched region. */ void getDamageRegionReply(); bool skipsCloseAnimation() const; void setSkipCloseAnimation(bool set); quint32 surfaceId() const; KWayland::Server::SurfaceInterface *surface() const; void setSurface(KWayland::Server::SurfaceInterface *surface); const QSharedPointer &internalFramebufferObject() const; QImage internalImageObject() const; /** * @returns Transformation to map from global to window coordinates. * * Default implementation returns a translation on negative pos(). * @see pos */ virtual QMatrix4x4 inputTransformation() const; /** * The window has a popup grab. This means that when it got mapped the * parent window had an implicit (pointer) grab. * * Normally this is only relevant for transient windows. * * Once the popup grab ends (e.g. pointer press outside of any Toplevel of * the client), the method popupDone should be invoked. * * The default implementation returns @c false. * @see popupDone * @since 5.10 */ virtual bool hasPopupGrab() const { return false; } /** * This method should be invoked for Toplevels with a popup grab when * the grab ends. * * The default implementation does nothing. * @see hasPopupGrab * @since 5.10 */ virtual void popupDone() {}; /** * @brief Finds the Toplevel matching the condition expressed in @p func in @p list. * * The method is templated to operate on either a list of Toplevels or on a list of * a subclass type of Toplevel. * @param list The list to search in * @param func The condition function (compare std::find_if) * @return T* The found Toplevel or @c null if there is no matching Toplevel */ template static T *findInList(const QList &list, std::function func); /** * Whether the window is a popup. * * Popups can be used to implement popup menus, tooltips, combo boxes, etc. * * @since 5.15 */ virtual bool isPopupWindow() const; /** * A UUID to uniquely identify this Toplevel independent of windowing system. */ QUuid internalId() const { return m_internalId; } Q_SIGNALS: void opacityChanged(KWin::Toplevel* toplevel, qreal oldOpacity); void damaged(KWin::Toplevel* toplevel, const QRect& damage); + /** + * This signal is emitted when the Toplevel's frame geometry changes. + * @deprecated since 5.19, use frameGeometryChanged instead + */ void geometryChanged(); void geometryShapeChanged(KWin::Toplevel* toplevel, const QRect& old); void paddingChanged(KWin::Toplevel* toplevel, const QRect& old); void windowClosed(KWin::Toplevel* toplevel, KWin::Deleted* deleted); void windowShown(KWin::Toplevel* toplevel); void windowHidden(KWin::Toplevel* toplevel); /** * Signal emitted when the window's shape state changed. That is if it did not have a shape * and received one or if the shape was withdrawn. Think of Chromium enabling/disabling KWin's * decoration. */ void shapedChanged(); /** * Emitted whenever the state changes in a way, that the Compositor should * schedule a repaint of the scene. */ void needsRepaint(); void activitiesChanged(KWin::Toplevel* toplevel); /** * Emitted whenever the Toplevel's screen changes. This can happen either in consequence to * a screen being removed/added or if the Toplevel's geometry changes. * @since 4.11 */ void screenChanged(); void skipCloseAnimationChanged(); /** * Emitted whenever the window role of the window changes. * @since 5.0 */ void windowRoleChanged(); /** * Emitted whenever the window class name or resource name of the window changes. * @since 5.0 */ void windowClassChanged(); /** * Emitted when a Wayland Surface gets associated with this Toplevel. * @since 5.3 */ void surfaceIdChanged(quint32); /** * @since 5.4 */ void hasAlphaChanged(); /** * Emitted whenever the Surface for this Toplevel changes. */ void surfaceChanged(); /* * Emitted when the client's screen changes onto a screen of a different scale * or the screen we're on changes * @since 5.12 */ void screenScaleChanged(); /** * Emitted whenever the client's shadow changes. * @since 5.15 */ void shadowChanged(); + /** + * This signal is emitted when the Toplevel's frame geometry changes. + */ + void frameGeometryChanged(KWin::Toplevel *toplevel, const QRect &oldGeometry); + protected Q_SLOTS: /** * Checks whether the screen number for this Toplevel changed and updates if needed. * Any method changing the geometry of the Toplevel should call this method. */ void checkScreen(); void setupCheckScreenConnection(); void removeCheckScreenConnection(); void setReadyForPainting(); protected: ~Toplevel() override; void setWindowHandles(xcb_window_t client); void detectShape(xcb_window_t id); virtual void propertyNotifyEvent(xcb_property_notify_event_t *e); virtual void damageNotifyEvent(); virtual void clientMessageEvent(xcb_client_message_event_t *e); void discardWindowPixmap(); void addDamageFull(); virtual void addDamage(const QRegion &damage); Xcb::Property fetchWmClientLeader() const; void readWmClientLeader(Xcb::Property &p); void getWmClientLeader(); void getWmClientMachine(); /** * @returns Whether there is a compositor and it is active. */ bool compositing() const; /** * This function fetches the opaque region from this Toplevel. * Will only be called on corresponding property changes and for initialization. */ void getWmOpaqueRegion(); void getResourceClass(); void setResourceClass(const QByteArray &name, const QByteArray &className = QByteArray()); Xcb::Property fetchSkipCloseAnimation() const; void readSkipCloseAnimation(Xcb::Property &prop); void getSkipCloseAnimation(); virtual void debug(QDebug& stream) const = 0; void copyToDeleted(Toplevel* c); void disownDataPassedToDeleted(); friend QDebug& operator<<(QDebug& stream, const Toplevel*); void deleteEffectWindow(); void setDepth(int depth); QRect m_frameGeometry; xcb_visualid_t m_visual; int bit_depth; NETWinInfo* info; bool ready_for_painting; QRegion repaints_region; // updating, repaint just requires repaint of that area QRegion layer_repaints_region; /** * An FBO object KWin internal windows might render to. */ QSharedPointer m_internalFBO; QImage m_internalImage; protected: bool m_isDamaged; private: // when adding new data members, check also copyToDeleted() QUuid m_internalId; Xcb::Window m_client; xcb_damage_damage_t damage_handle; QRegion damage_region; // damage is really damaged window (XDamage) and texture needs bool is_shape; EffectWindowImpl* effect_window; QByteArray resource_name; QByteArray resource_class; ClientMachine *m_clientMachine; xcb_window_t m_wmClientLeader; bool m_damageReplyPending; QRegion opaque_region; xcb_xfixes_fetch_region_cookie_t m_regionCookie; int m_screen; bool m_skipCloseAnimation; quint32 m_surfaceId = 0; KWayland::Server::SurfaceInterface *m_surface = nullptr; // when adding new data members, check also copyToDeleted() qreal m_screenScale = 1.0; }; inline xcb_window_t Toplevel::window() const { return m_client; } inline void Toplevel::setWindowHandles(xcb_window_t w) { Q_ASSERT(!m_client.isValid() && w != XCB_WINDOW_NONE); m_client.reset(w, false); } inline QRect Toplevel::frameGeometry() const { return m_frameGeometry; } inline QSize Toplevel::size() const { return m_frameGeometry.size(); } inline QPoint Toplevel::pos() const { return m_frameGeometry.topLeft(); } inline int Toplevel::x() const { return m_frameGeometry.x(); } inline int Toplevel::y() const { return m_frameGeometry.y(); } inline int Toplevel::width() const { return m_frameGeometry.width(); } inline int Toplevel::height() const { return m_frameGeometry.height(); } inline QRect Toplevel::rect() const { return QRect(0, 0, width(), height()); } inline bool Toplevel::readyForPainting() const { return ready_for_painting; } inline xcb_visualid_t Toplevel::visual() const { return m_visual; } inline bool Toplevel::isDesktop() const { return windowType() == NET::Desktop; } inline bool Toplevel::isDock() const { return windowType() == NET::Dock; } inline bool Toplevel::isMenu() const { return windowType() == NET::Menu; } inline bool Toplevel::isToolbar() const { return windowType() == NET::Toolbar; } inline bool Toplevel::isSplash() const { return windowType() == NET::Splash; } inline bool Toplevel::isUtility() const { return windowType() == NET::Utility; } inline bool Toplevel::isDialog() const { return windowType() == NET::Dialog; } inline bool Toplevel::isNormalWindow() const { return windowType() == NET::Normal; } inline bool Toplevel::isDropdownMenu() const { return windowType() == NET::DropdownMenu; } inline bool Toplevel::isPopupMenu() const { return windowType() == NET::PopupMenu; } inline bool Toplevel::isTooltip() const { return windowType() == NET::Tooltip; } inline bool Toplevel::isNotification() const { return windowType() == NET::Notification; } inline bool Toplevel::isCriticalNotification() const { return windowType() == NET::CriticalNotification; } inline bool Toplevel::isOnScreenDisplay() const { return windowType() == NET::OnScreenDisplay; } inline bool Toplevel::isComboBox() const { return windowType() == NET::ComboBox; } inline bool Toplevel::isDNDIcon() const { return windowType() == NET::DNDIcon; } inline bool Toplevel::isLockScreen() const { return false; } inline bool Toplevel::isInputMethod() const { return false; } inline bool Toplevel::isOutline() const { return false; } inline QRegion Toplevel::damage() const { return damage_region; } inline QRegion Toplevel::repaints() const { return repaints_region.translated(pos()) | layer_repaints_region; } inline bool Toplevel::shape() const { return is_shape; } inline int Toplevel::depth() const { return bit_depth; } inline bool Toplevel::hasAlpha() const { return depth() == 32; } inline const QRegion& Toplevel::opaqueRegion() const { return opaque_region; } inline EffectWindowImpl* Toplevel::effectWindow() { return effect_window; } inline const EffectWindowImpl* Toplevel::effectWindow() const { return effect_window; } inline bool Toplevel::isOnAllDesktops() const { return kwinApp()->operationMode() == Application::OperationModeWaylandOnly || kwinApp()->operationMode() == Application::OperationModeXwayland //Wayland ? desktops().isEmpty() //X11 : desktop() == NET::OnAllDesktops; } inline bool Toplevel::isOnAllActivities() const { return activities().isEmpty(); } inline bool Toplevel::isOnDesktop(int d) const { return (kwinApp()->operationMode() == Application::OperationModeWaylandOnly || kwinApp()->operationMode() == Application::OperationModeXwayland ? desktops().contains(VirtualDesktopManager::self()->desktopForX11Id(d)) : desktop() == d ) || isOnAllDesktops(); } inline bool Toplevel::isOnActivity(const QString &activity) const { return activities().isEmpty() || activities().contains(activity); } inline bool Toplevel::isOnCurrentDesktop() const { return isOnDesktop(VirtualDesktopManager::self()->current()); } inline QByteArray Toplevel::resourceName() const { return resource_name; // it is always lowercase } inline QByteArray Toplevel::resourceClass() const { return resource_class; // it is always lowercase } inline const ClientMachine *Toplevel::clientMachine() const { return m_clientMachine; } inline quint32 Toplevel::surfaceId() const { return m_surfaceId; } inline KWayland::Server::SurfaceInterface *Toplevel::surface() const { return m_surface; } inline const QSharedPointer &Toplevel::internalFramebufferObject() const { return m_internalFBO; } inline QImage Toplevel::internalImageObject() const { return m_internalImage; } inline QPoint Toplevel::clientContentPos() const { return QPoint(0, 0); } template inline T *Toplevel::findInList(const QList &list, std::function func) { static_assert(std::is_base_of::value, "U must be derived from T"); const auto it = std::find_if(list.begin(), list.end(), func); if (it == list.end()) { return nullptr; } return *it; } inline bool Toplevel::isPopupWindow() const { switch (windowType()) { case NET::ComboBox: case NET::DropdownMenu: case NET::PopupMenu: case NET::Tooltip: return true; default: return false; } } QDebug& operator<<(QDebug& stream, const Toplevel*); } // namespace Q_DECLARE_METATYPE(KWin::Toplevel*) #endif diff --git a/touch_input.cpp b/touch_input.cpp index eec38e772..22ff287e9 100644 --- a/touch_input.cpp +++ b/touch_input.cpp @@ -1,233 +1,233 @@ /******************************************************************** KWin - the KDE window manager This file is part of the KDE project. Copyright (C) 2013, 2016 Martin Gräßlin Copyright (C) 2018 Roman Gilg 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 "touch_input.h" #include "abstract_client.h" #include "input.h" #include "pointer_input.h" #include "input_event_spy.h" #include "toplevel.h" #include "wayland_server.h" #include "workspace.h" #include "decorations/decoratedclient.h" // KDecoration #include // KWayland #include // screenlocker #include // Qt #include #include namespace KWin { TouchInputRedirection::TouchInputRedirection(InputRedirection *parent) : InputDeviceHandler(parent) { } TouchInputRedirection::~TouchInputRedirection() = default; void TouchInputRedirection::init() { Q_ASSERT(!inited()); setInited(true); InputDeviceHandler::init(); if (waylandServer()->hasScreenLockerIntegration()) { connect(ScreenLocker::KSldApp::self(), &ScreenLocker::KSldApp::lockStateChanged, this, [this] { cancel(); // position doesn't matter update(); } ); } connect(workspace(), &QObject::destroyed, this, [this] { setInited(false); }); connect(waylandServer(), &QObject::destroyed, this, [this] { setInited(false); }); } bool TouchInputRedirection::focusUpdatesBlocked() { if (!inited()) { return true; } if (m_windowUpdatedInCycle) { return true; } m_windowUpdatedInCycle = true; if (waylandServer()->seat()->isDragTouch()) { return true; } if (m_touches > 1) { // first touch defines focus return true; } return false; } bool TouchInputRedirection::positionValid() const { Q_ASSERT(m_touches >= 0); // we can only determine a position with at least one touch point return m_touches == 0; } void TouchInputRedirection::focusUpdate(Toplevel *focusOld, Toplevel *focusNow) { // TODO: handle pointer grab aka popups if (AbstractClient *ac = qobject_cast(focusOld)) { ac->leaveEvent(); } disconnect(m_focusGeometryConnection); m_focusGeometryConnection = QMetaObject::Connection(); if (AbstractClient *ac = qobject_cast(focusNow)) { ac->enterEvent(m_lastPosition.toPoint()); workspace()->updateFocusMousePosition(m_lastPosition.toPoint()); } auto seat = waylandServer()->seat(); if (!focusNow || !focusNow->surface() || decoration()) { // no new surface or internal window or on decoration -> cleanup seat->setFocusedTouchSurface(nullptr); return; } // TODO: invalidate pointer focus? // FIXME: add input transformation API to KWayland::Server::SeatInterface for touch input seat->setFocusedTouchSurface(focusNow->surface(), -1 * focusNow->inputTransformation().map(focusNow->pos()) + focusNow->pos()); - m_focusGeometryConnection = connect(focusNow, &Toplevel::geometryChanged, this, + m_focusGeometryConnection = connect(focusNow, &Toplevel::frameGeometryChanged, this, [this] { if (focus().isNull()) { return; } auto seat = waylandServer()->seat(); if (focus().data()->surface() != seat->focusedTouchSurface()) { return; } seat->setFocusedTouchSurfacePosition(-1 * focus()->inputTransformation().map(focus()->pos()) + focus()->pos()); } ); } void TouchInputRedirection::cleanupInternalWindow(QWindow *old, QWindow *now) { Q_UNUSED(old); Q_UNUSED(now); // nothing to do } void TouchInputRedirection::cleanupDecoration(Decoration::DecoratedClientImpl *old, Decoration::DecoratedClientImpl *now) { Q_UNUSED(old); Q_UNUSED(now); // nothing to do } void TouchInputRedirection::insertId(qint32 internalId, qint32 kwaylandId) { m_idMapper.insert(internalId, kwaylandId); } qint32 TouchInputRedirection::mappedId(qint32 internalId) { auto it = m_idMapper.constFind(internalId); if (it != m_idMapper.constEnd()) { return it.value(); } return -1; } void TouchInputRedirection::removeId(qint32 internalId) { m_idMapper.remove(internalId); } void TouchInputRedirection::processDown(qint32 id, const QPointF &pos, quint32 time, LibInput::Device *device) { Q_UNUSED(device) if (!inited()) { return; } m_lastPosition = pos; m_windowUpdatedInCycle = false; m_touches++; if (m_touches == 1) { update(); } input()->processSpies(std::bind(&InputEventSpy::touchDown, std::placeholders::_1, id, pos, time)); input()->processFilters(std::bind(&InputEventFilter::touchDown, std::placeholders::_1, id, pos, time)); m_windowUpdatedInCycle = false; } void TouchInputRedirection::processUp(qint32 id, quint32 time, LibInput::Device *device) { Q_UNUSED(device) if (!inited()) { return; } m_windowUpdatedInCycle = false; input()->processSpies(std::bind(&InputEventSpy::touchUp, std::placeholders::_1, id, time)); input()->processFilters(std::bind(&InputEventFilter::touchUp, std::placeholders::_1, id, time)); m_windowUpdatedInCycle = false; m_touches--; if (m_touches == 0) { update(); } } void TouchInputRedirection::processMotion(qint32 id, const QPointF &pos, quint32 time, LibInput::Device *device) { Q_UNUSED(device) if (!inited()) { return; } m_lastPosition = pos; m_windowUpdatedInCycle = false; input()->processSpies(std::bind(&InputEventSpy::touchMotion, std::placeholders::_1, id, pos, time)); input()->processFilters(std::bind(&InputEventFilter::touchMotion, std::placeholders::_1, id, pos, time)); m_windowUpdatedInCycle = false; } void TouchInputRedirection::cancel() { if (!inited()) { return; } waylandServer()->seat()->cancelTouchSequence(); m_idMapper.clear(); } void TouchInputRedirection::frame() { if (!inited()) { return; } waylandServer()->seat()->touchFrame(); } } diff --git a/unmanaged.cpp b/unmanaged.cpp index bc8987494..529eb5f1b 100644 --- a/unmanaged.cpp +++ b/unmanaged.cpp @@ -1,220 +1,219 @@ /******************************************************************** KWin - the KDE window manager This file is part of the KDE project. Copyright (C) 2006 Lubos Lunak This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . *********************************************************************/ #include "unmanaged.h" #include "workspace.h" #include "effects.h" #include "deleted.h" #include "utils.h" #include "xcbutils.h" #include #include #include #include namespace KWin { // window types that are supported as unmanaged (mainly for compositing) const NET::WindowTypes SUPPORTED_UNMANAGED_WINDOW_TYPES_MASK = NET::NormalMask | NET::DesktopMask | NET::DockMask | NET::ToolbarMask | NET::MenuMask | NET::DialogMask /*| NET::OverrideMask*/ | NET::TopMenuMask | NET::UtilityMask | NET::SplashMask | NET::DropdownMenuMask | NET::PopupMenuMask | NET::TooltipMask | NET::NotificationMask | NET::ComboBoxMask | NET::DNDIconMask | NET::OnScreenDisplayMask | NET::CriticalNotificationMask; Unmanaged::Unmanaged() : Toplevel() { - connect(this, SIGNAL(geometryShapeChanged(KWin::Toplevel*,QRect)), SIGNAL(geometryChanged())); QTimer::singleShot(50, this, SLOT(setReadyForPainting())); } Unmanaged::~Unmanaged() { } bool Unmanaged::track(xcb_window_t w) { XServerGrabber xserverGrabber; Xcb::WindowAttributes attr(w); Xcb::WindowGeometry geo(w); if (attr.isNull() || attr->map_state != XCB_MAP_STATE_VIEWABLE) { return false; } if (attr->_class == XCB_WINDOW_CLASS_INPUT_ONLY) { return false; } if (geo.isNull()) { return false; } setWindowHandles(w); // the window is also the frame Xcb::selectInput(w, attr->your_event_mask | XCB_EVENT_MASK_STRUCTURE_NOTIFY | XCB_EVENT_MASK_PROPERTY_CHANGE); m_frameGeometry = geo.rect(); checkScreen(); m_visual = attr->visual; bit_depth = geo->depth; info = new NETWinInfo(connection(), w, rootWindow(), NET::WMWindowType | NET::WMPid, NET::WM2Opacity | NET::WM2WindowRole | NET::WM2WindowClass | NET::WM2OpaqueRegion); getResourceClass(); getWmClientLeader(); getWmClientMachine(); if (Xcb::Extensions::self()->isShapeAvailable()) xcb_shape_select_input(connection(), w, true); detectShape(w); getWmOpaqueRegion(); getSkipCloseAnimation(); setupCompositing(); if (QWindow *internalWindow = findInternalWindow()) { m_outline = internalWindow->property("__kwin_outline").toBool(); } if (effects) static_cast(effects)->checkInputWindowStacking(); return true; } void Unmanaged::release(ReleaseReason releaseReason) { Deleted* del = nullptr; if (releaseReason != ReleaseReason::KWinShutsDown) { del = Deleted::create(this); } emit windowClosed(this, del); finishCompositing(releaseReason); if (!QWidget::find(window()) && releaseReason != ReleaseReason::Destroyed) { // don't affect our own windows if (Xcb::Extensions::self()->isShapeAvailable()) xcb_shape_select_input(connection(), window(), false); Xcb::selectInput(window(), XCB_EVENT_MASK_NO_EVENT); } if (releaseReason != ReleaseReason::KWinShutsDown) { workspace()->removeUnmanaged(this); addWorkspaceRepaint(del->visibleRect()); disownDataPassedToDeleted(); del->unrefWindow(); } deleteUnmanaged(this); } void Unmanaged::deleteUnmanaged(Unmanaged* c) { delete c; } bool Unmanaged::hasScheduledRelease() const { return m_scheduledRelease; } QRect Unmanaged::bufferGeometry() const { return m_frameGeometry; } int Unmanaged::desktop() const { return NET::OnAllDesktops; // TODO for some window types should be the current desktop? } QStringList Unmanaged::activities() const { return QStringList(); } QVector Unmanaged::desktops() const { return QVector(); } QPoint Unmanaged::clientPos() const { return QPoint(0, 0); // unmanaged windows don't have decorations } QSize Unmanaged::clientSize() const { return size(); } QRect Unmanaged::transparentRect() const { return QRect(clientPos(), clientSize()); } void Unmanaged::debug(QDebug& stream) const { stream << "\'ID:" << window() << "\'"; } NET::WindowType Unmanaged::windowType(bool direct, int supportedTypes) const { // for unmanaged windows the direct does not make any difference // as there are no rules to check and no hacks to apply Q_UNUSED(direct) if (supportedTypes == 0) { supportedTypes = SUPPORTED_UNMANAGED_WINDOW_TYPES_MASK; } return info->windowType(NET::WindowTypes(supportedTypes)); } bool Unmanaged::isOutline() const { return m_outline; } void Unmanaged::addDamage(const QRegion &damage) { repaints_region += damage; Toplevel::addDamage(damage); } QWindow *Unmanaged::findInternalWindow() const { const QWindowList windows = kwinApp()->topLevelWindows(); for (QWindow *w : windows) { if (w->winId() == window()) { return w; } } return nullptr; } bool Unmanaged::setupCompositing() { if (!Toplevel::setupCompositing()) { return false; } // With unmanaged windows there is a race condition between the client painting the window // and us setting up damage tracking. If the client wins we won't get a damage event even // though the window has been painted. To avoid this we mark the whole window as damaged // and schedule a repaint immediately after creating the damage object. addDamageFull(); return true; } } // namespace diff --git a/x11client.cpp b/x11client.cpp index 079d91a69..b83d82046 100644 --- a/x11client.cpp +++ b/x11client.cpp @@ -1,5001 +1,5005 @@ /******************************************************************** KWin - the KDE window manager This file is part of the KDE project. Copyright (C) 1999, 2000 Matthias Ettrich Copyright (C) 2003 Lubos Lunak This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . *********************************************************************/ // own #include "x11client.h" // kwin #ifdef KWIN_BUILD_ACTIVITIES #include "activities.h" #endif #include "atoms.h" #include "client_machine.h" #include "composite.h" #include "cursor.h" #include "deleted.h" #include "effects.h" #include "focuschain.h" #include "geometrytip.h" #include "group.h" #include "netinfo.h" #include "screens.h" #include "shadow.h" #ifdef KWIN_BUILD_TABBOX #include "tabbox.h" #endif #include "workspace.h" #include "screenedge.h" #include "decorations/decorationbridge.h" #include "decorations/decoratedclient.h" #include #include // KDE #include #include #include #include // Qt #include #include #include #include #include #include #include // xcb #include // system #include // c++ #include // Put all externs before the namespace statement to allow the linker // to resolve them properly namespace KWin { const long ClientWinMask = XCB_EVENT_MASK_KEY_PRESS | XCB_EVENT_MASK_KEY_RELEASE | XCB_EVENT_MASK_BUTTON_PRESS | XCB_EVENT_MASK_BUTTON_RELEASE | XCB_EVENT_MASK_KEYMAP_STATE | XCB_EVENT_MASK_BUTTON_MOTION | XCB_EVENT_MASK_POINTER_MOTION | // need this, too! XCB_EVENT_MASK_ENTER_WINDOW | XCB_EVENT_MASK_LEAVE_WINDOW | XCB_EVENT_MASK_FOCUS_CHANGE | XCB_EVENT_MASK_EXPOSURE | XCB_EVENT_MASK_STRUCTURE_NOTIFY | XCB_EVENT_MASK_SUBSTRUCTURE_REDIRECT; // window types that are supported as normal windows (i.e. KWin actually manages them) const NET::WindowTypes SUPPORTED_MANAGED_WINDOW_TYPES_MASK = NET::NormalMask | NET::DesktopMask | NET::DockMask | NET::ToolbarMask | NET::MenuMask | NET::DialogMask /*| NET::OverrideMask*/ | NET::TopMenuMask | NET::UtilityMask | NET::SplashMask | NET::NotificationMask | NET::OnScreenDisplayMask | NET::CriticalNotificationMask; // Creating a client: // - only by calling Workspace::createClient() // - it creates a new client and calls manage() for it // // Destroying a client: // - destroyClient() - only when the window itself has been destroyed // - releaseWindow() - the window is kept, only the client itself is destroyed /** * \class Client x11client.h * \brief The Client class encapsulates a window decoration frame. */ /** * This ctor is "dumb" - it only initializes data. All the real initialization * is done in manage(). */ X11Client::X11Client() : AbstractClient() , m_client() , m_wrapper() , m_frame() , m_activityUpdatesBlocked(false) , m_blockedActivityUpdatesRequireTransients(false) , m_moveResizeGrabWindow() , move_resize_has_keyboard_grab(false) , m_managed(false) , m_transientForId(XCB_WINDOW_NONE) , m_originalTransientForId(XCB_WINDOW_NONE) , shade_below(nullptr) , m_motif(atoms->motif_wm_hints) , blocks_compositing(false) , shadeHoverTimer(nullptr) , m_colormap(XCB_COLORMAP_NONE) , in_group(nullptr) , ping_timer(nullptr) , m_killHelperPID(0) , m_pingTimestamp(XCB_TIME_CURRENT_TIME) , m_userTime(XCB_TIME_CURRENT_TIME) // Not known yet , allowed_actions() , shade_geometry_change(false) , sm_stacking_order(-1) , activitiesDefined(false) , sessionActivityOverride(false) , needsXWindowMove(false) , m_decoInputExtent() , m_focusOutTimer(nullptr) { // TODO: Do all as initialization m_syncRequest.counter = m_syncRequest.alarm = XCB_NONE; m_syncRequest.timeout = m_syncRequest.failsafeTimeout = nullptr; m_syncRequest.lastTimestamp = xTime(); m_syncRequest.isPending = false; // Set the initial mapping state mapping_state = Withdrawn; info = nullptr; shade_mode = ShadeNone; deleting = false; m_fullscreenMode = FullScreenNone; hidden = false; noborder = false; app_noborder = false; ignore_focus_stealing = false; check_active_modal = false; max_mode = MaximizeRestore; //Client to workspace connections require that each //client constructed be connected to the workspace wrapper m_frameGeometry = QRect(0, 0, 100, 100); // So that decorations don't start with size being (0,0) connect(clientMachine(), &ClientMachine::localhostChanged, this, &X11Client::updateCaption); connect(options, &Options::condensedTitleChanged, this, &X11Client::updateCaption); connect(this, &X11Client::moveResizeCursorChanged, this, [this] (CursorShape cursor) { xcb_cursor_t nativeCursor = Cursor::x11Cursor(cursor); m_frame.defineCursor(nativeCursor); if (m_decoInputExtent.isValid()) m_decoInputExtent.defineCursor(nativeCursor); if (isMoveResize()) { // changing window attributes doesn't change cursor if there's pointer grab active xcb_change_active_pointer_grab(connection(), nativeCursor, xTime(), XCB_EVENT_MASK_BUTTON_PRESS | XCB_EVENT_MASK_BUTTON_RELEASE | XCB_EVENT_MASK_POINTER_MOTION | XCB_EVENT_MASK_ENTER_WINDOW | XCB_EVENT_MASK_LEAVE_WINDOW); } }); // SELI TODO: Initialize xsizehints?? } /** * "Dumb" destructor. */ X11Client::~X11Client() { if (m_killHelperPID && !::kill(m_killHelperPID, 0)) { // means the process is alive ::kill(m_killHelperPID, SIGTERM); m_killHelperPID = 0; } if (m_syncRequest.alarm != XCB_NONE) { xcb_sync_destroy_alarm(connection(), m_syncRequest.alarm); } Q_ASSERT(!isMoveResize()); Q_ASSERT(m_client == XCB_WINDOW_NONE); Q_ASSERT(m_wrapper == XCB_WINDOW_NONE); Q_ASSERT(m_frame == XCB_WINDOW_NONE); Q_ASSERT(!check_active_modal); for (auto it = m_connections.constBegin(); it != m_connections.constEnd(); ++it) { disconnect(*it); } } // Use destroyClient() or releaseWindow(), Client instances cannot be deleted directly void X11Client::deleteClient(X11Client *c) { delete c; } /** * Releases the window. The client has done its job and the window is still existing. */ void X11Client::releaseWindow(bool on_shutdown) { Q_ASSERT(!deleting); deleting = true; #ifdef KWIN_BUILD_TABBOX TabBox::TabBox *tabBox = TabBox::TabBox::self(); if (tabBox->isDisplayed() && tabBox->currentClient() == this) { tabBox->nextPrev(true); } #endif destroyWindowManagementInterface(); Deleted* del = nullptr; if (!on_shutdown) { del = Deleted::create(this); } if (isMoveResize()) emit clientFinishUserMovedResized(this); emit windowClosed(this, del); finishCompositing(); RuleBook::self()->discardUsed(this, true); // Remove ForceTemporarily rules StackingUpdatesBlocker blocker(workspace()); if (isMoveResize()) leaveMoveResize(); finishWindowRules(); blockGeometryUpdates(); if (isOnCurrentDesktop() && isShown(true)) addWorkspaceRepaint(visibleRect()); // Grab X during the release to make removing of properties, setting to withdrawn state // and repareting to root an atomic operation (https://lists.kde.org/?l=kde-devel&m=116448102901184&w=2) grabXServer(); exportMappingState(XCB_ICCCM_WM_STATE_WITHDRAWN); setModal(false); // Otherwise its mainwindow wouldn't get focus hidden = true; // So that it's not considered visible anymore (can't use hideClient(), it would set flags) if (!on_shutdown) workspace()->clientHidden(this); m_frame.unmap(); // Destroying decoration would cause ugly visual effect destroyDecoration(); cleanGrouping(); if (!on_shutdown) { workspace()->removeClient(this); // Only when the window is being unmapped, not when closing down KWin (NETWM sections 5.5,5.7) info->setDesktop(0); info->setState(NET::States(), info->state()); // Reset all state flags } xcb_connection_t *c = connection(); m_client.deleteProperty(atoms->kde_net_wm_user_creation_time); m_client.deleteProperty(atoms->net_frame_extents); m_client.deleteProperty(atoms->kde_net_wm_frame_strut); m_client.reparent(rootWindow(), m_bufferGeometry.x(), m_bufferGeometry.y()); xcb_change_save_set(c, XCB_SET_MODE_DELETE, m_client); m_client.selectInput(XCB_EVENT_MASK_NO_EVENT); if (on_shutdown) // Map the window, so it can be found after another WM is started m_client.map(); // TODO: Preserve minimized, shaded etc. state? else // Make sure it's not mapped if the app unmapped it (#65279). The app // may do map+unmap before we initially map the window by calling rawShow() from manage(). m_client.unmap(); m_client.reset(); m_wrapper.reset(); m_frame.reset(); unblockGeometryUpdates(); // Don't use GeometryUpdatesBlocker, it would now set the geometry if (!on_shutdown) { disownDataPassedToDeleted(); del->unrefWindow(); } deleteClient(this); ungrabXServer(); } /** * Like releaseWindow(), but this one is called when the window has been already destroyed * (E.g. The application closed it) */ void X11Client::destroyClient() { Q_ASSERT(!deleting); deleting = true; #ifdef KWIN_BUILD_TABBOX TabBox::TabBox *tabBox = TabBox::TabBox::self(); if (tabBox && tabBox->isDisplayed() && tabBox->currentClient() == this) { tabBox->nextPrev(true); } #endif destroyWindowManagementInterface(); Deleted* del = Deleted::create(this); if (isMoveResize()) emit clientFinishUserMovedResized(this); emit windowClosed(this, del); finishCompositing(ReleaseReason::Destroyed); RuleBook::self()->discardUsed(this, true); // Remove ForceTemporarily rules StackingUpdatesBlocker blocker(workspace()); if (isMoveResize()) leaveMoveResize(); finishWindowRules(); blockGeometryUpdates(); if (isOnCurrentDesktop() && isShown(true)) addWorkspaceRepaint(visibleRect()); setModal(false); hidden = true; // So that it's not considered visible anymore workspace()->clientHidden(this); destroyDecoration(); cleanGrouping(); workspace()->removeClient(this); m_client.reset(); // invalidate m_wrapper.reset(); m_frame.reset(); unblockGeometryUpdates(); // Don't use GeometryUpdatesBlocker, it would now set the geometry disownDataPassedToDeleted(); del->unrefWindow(); deleteClient(this); } /** * Manages the clients. This means handling the very first maprequest: * reparenting, initial geometry, initial state, placement, etc. * Returns false if KWin is not going to manage this window. */ bool X11Client::manage(xcb_window_t w, bool isMapped) { StackingUpdatesBlocker stacking_blocker(workspace()); Xcb::WindowAttributes attr(w); Xcb::WindowGeometry windowGeometry(w); if (attr.isNull() || windowGeometry.isNull()) { return false; } // From this place on, manage() must not return false blockGeometryUpdates(); setPendingGeometryUpdate(PendingGeometryForced); // Force update when finishing with geometry changes embedClient(w, attr->visual, attr->colormap, windowGeometry->depth); m_visual = attr->visual; bit_depth = windowGeometry->depth; // SELI TODO: Order all these things in some sane manner const NET::Properties properties = NET::WMDesktop | NET::WMState | NET::WMWindowType | NET::WMStrut | NET::WMName | NET::WMIconGeometry | NET::WMIcon | NET::WMPid | NET::WMIconName; const NET::Properties2 properties2 = NET::WM2BlockCompositing | NET::WM2WindowClass | NET::WM2WindowRole | NET::WM2UserTime | NET::WM2StartupId | NET::WM2ExtendedStrut | NET::WM2Opacity | NET::WM2FullscreenMonitors | NET::WM2FrameOverlap | NET::WM2GroupLeader | NET::WM2Urgency | NET::WM2Input | NET::WM2Protocols | NET::WM2InitialMappingState | NET::WM2IconPixmap | NET::WM2OpaqueRegion | NET::WM2DesktopFileName | NET::WM2GTKFrameExtents; auto wmClientLeaderCookie = fetchWmClientLeader(); auto skipCloseAnimationCookie = fetchSkipCloseAnimation(); auto showOnScreenEdgeCookie = fetchShowOnScreenEdge(); auto colorSchemeCookie = fetchColorScheme(); auto firstInTabBoxCookie = fetchFirstInTabBox(); auto transientCookie = fetchTransient(); auto activitiesCookie = fetchActivities(); auto applicationMenuServiceNameCookie = fetchApplicationMenuServiceName(); auto applicationMenuObjectPathCookie = fetchApplicationMenuObjectPath(); m_geometryHints.init(window()); m_motif.init(window()); info = new WinInfo(this, m_client, rootWindow(), properties, properties2); if (isDesktop() && bit_depth == 32) { // force desktop windows to be opaque. It's a desktop after all, there is no window below bit_depth = 24; } // If it's already mapped, ignore hint bool init_minimize = !isMapped && (info->initialMappingState() == NET::Iconic); m_colormap = attr->colormap; getResourceClass(); readWmClientLeader(wmClientLeaderCookie); getWmClientMachine(); getSyncCounter(); // First only read the caption text, so that setupWindowRules() can use it for matching, // and only then really set the caption using setCaption(), which checks for duplicates etc. // and also relies on rules already existing cap_normal = readName(); setupWindowRules(false); setCaption(cap_normal, true); connect(this, &X11Client::windowClassChanged, this, &X11Client::evaluateWindowRules); if (Xcb::Extensions::self()->isShapeAvailable()) xcb_shape_select_input(connection(), window(), true); detectShape(window()); detectNoBorder(); fetchIconicName(); setClientFrameExtents(info->gtkFrameExtents()); // Needs to be done before readTransient() because of reading the group checkGroup(); updateUrgency(); updateAllowedActions(); // Group affects isMinimizable() setModal((info->state() & NET::Modal) != 0); // Needs to be valid before handling groups readTransientProperty(transientCookie); setDesktopFileName(rules()->checkDesktopFile(QByteArray(info->desktopFileName()), true).toUtf8()); getIcons(); connect(this, &X11Client::desktopFileNameChanged, this, &X11Client::getIcons); m_geometryHints.read(); getMotifHints(); getWmOpaqueRegion(); readSkipCloseAnimation(skipCloseAnimationCookie); // TODO: Try to obey all state information from info->state() setOriginalSkipTaskbar((info->state() & NET::SkipTaskbar) != 0); setSkipPager((info->state() & NET::SkipPager) != 0); setSkipSwitcher((info->state() & NET::SkipSwitcher) != 0); readFirstInTabBox(firstInTabBoxCookie); setupCompositing(); KStartupInfoId asn_id; KStartupInfoData asn_data; bool asn_valid = workspace()->checkStartupNotification(window(), asn_id, asn_data); // Make sure that the input window is created before we update the stacking order updateInputWindow(); workspace()->updateClientLayer(this); SessionInfo* session = workspace()->takeSessionInfo(this); if (session) { init_minimize = session->minimized; noborder = session->noBorder; } setShortcut(rules()->checkShortcut(session ? session->shortcut : QString(), true)); init_minimize = rules()->checkMinimize(init_minimize, !isMapped); noborder = rules()->checkNoBorder(noborder, !isMapped); readActivities(activitiesCookie); // Initial desktop placement int desk = 0; if (session) { desk = session->desktop; if (session->onAllDesktops) desk = NET::OnAllDesktops; setOnActivities(session->activities); } else { // If this window is transient, ensure that it is opened on the // same window as its parent. this is necessary when an application // starts up on a different desktop than is currently displayed if (isTransient()) { auto mainclients = mainClients(); bool on_current = false; bool on_all = false; AbstractClient* maincl = nullptr; // This is slightly duplicated from Placement::placeOnMainWindow() for (auto it = mainclients.constBegin(); it != mainclients.constEnd(); ++it) { if (mainclients.count() > 1 && // A group-transient (*it)->isSpecialWindow() && // Don't consider toolbars etc when placing !(info->state() & NET::Modal)) // except when it's modal (blocks specials as well) continue; maincl = *it; if ((*it)->isOnCurrentDesktop()) on_current = true; if ((*it)->isOnAllDesktops()) on_all = true; } if (on_all) desk = NET::OnAllDesktops; else if (on_current) desk = VirtualDesktopManager::self()->current(); else if (maincl != nullptr) desk = maincl->desktop(); if (maincl) setOnActivities(maincl->activities()); } else { // a transient shall appear on its leader and not drag that around if (info->desktop()) desk = info->desktop(); // Window had the initial desktop property, force it if (desktop() == 0 && asn_valid && asn_data.desktop() != 0) desk = asn_data.desktop(); } #ifdef KWIN_BUILD_ACTIVITIES if (Activities::self() && !isMapped && !noborder && isNormalWindow() && !activitiesDefined) { //a new, regular window, when we're not recovering from a crash, //and it hasn't got an activity. let's try giving it the current one. //TODO: decide whether to keep this before the 4.6 release //TODO: if we are keeping it (at least as an option), replace noborder checking //with a public API for setting windows to be on all activities. //something like KWindowSystem::setOnAllActivities or //KActivityConsumer::setOnAllActivities setOnActivity(Activities::self()->current(), true); } #endif } if (desk == 0) // Assume window wants to be visible on the current desktop desk = isDesktop() ? static_cast(NET::OnAllDesktops) : VirtualDesktopManager::self()->current(); desk = rules()->checkDesktop(desk, !isMapped); if (desk != NET::OnAllDesktops) // Do range check desk = qBound(1, desk, static_cast(VirtualDesktopManager::self()->count())); setDesktop(desk); info->setDesktop(desk); workspace()->updateOnAllDesktopsOfTransients(this); // SELI TODO //onAllDesktopsChange(); // Decoration doesn't exist here yet QString activitiesList; activitiesList = rules()->checkActivity(activitiesList, !isMapped); if (!activitiesList.isEmpty()) setOnActivities(activitiesList.split(QStringLiteral(","))); QRect geom(windowGeometry.rect()); bool placementDone = false; if (session) geom = session->geometry; QRect area; bool partial_keep_in_area = isMapped || session; if (isMapped || session) { area = workspace()->clientArea(FullArea, geom.center(), desktop()); checkOffscreenPosition(&geom, area); } else { int screen = asn_data.xinerama() == -1 ? screens()->current() : asn_data.xinerama(); screen = rules()->checkScreen(screen, !isMapped); area = workspace()->clientArea(PlacementArea, screens()->geometry(screen).center(), desktop()); } if (isDesktop()) // KWin doesn't manage desktop windows placementDone = true; bool usePosition = false; if (isMapped || session || placementDone) placementDone = true; // Use geometry else if (isTransient() && !isUtility() && !isDialog() && !isSplash()) usePosition = true; else if (isTransient() && !hasNETSupport()) usePosition = true; else if (isDialog() && hasNETSupport()) { // If the dialog is actually non-NETWM transient window, don't try to apply placement to it, // it breaks with too many things (xmms, display) if (mainClients().count() >= 1) { #if 1 // #78082 - Ok, it seems there are after all some cases when an application has a good // reason to specify a position for its dialog. Too bad other WMs have never bothered // with placement for dialogs, so apps always specify positions for their dialogs, // including such silly positions like always centered on the screen or under mouse. // Using ignoring requested position in window-specific settings helps, and now // there's also _NET_WM_FULL_PLACEMENT. usePosition = true; #else ; // Force using placement policy #endif } else usePosition = true; } else if (isSplash()) ; // Force using placement policy else usePosition = true; if (!rules()->checkIgnoreGeometry(!usePosition, true)) { if (m_geometryHints.hasPosition()) { placementDone = true; // Disobey xinerama placement option for now (#70943) area = workspace()->clientArea(PlacementArea, geom.center(), desktop()); } } if (isMovable() && (geom.x() > area.right() || geom.y() > area.bottom())) placementDone = false; // Weird, do not trust. if (placementDone) { QPoint position = geom.topLeft(); // Session contains the position of the frame geometry before gravitating. if (!session) { position = clientPosToFramePos(position); } move(position); } // Create client group if the window will have a decoration bool dontKeepInArea = false; readColorScheme(colorSchemeCookie); readApplicationMenuServiceName(applicationMenuServiceNameCookie); readApplicationMenuObjectPath(applicationMenuObjectPathCookie); updateDecoration(false); // Also gravitates // TODO: Is CentralGravity right here, when resizing is done after gravitating? plainResize(rules()->checkSize(sizeForClientSize(geom.size()), !isMapped)); QPoint forced_pos = rules()->checkPosition(invalidPoint, !isMapped); if (forced_pos != invalidPoint) { move(forced_pos); placementDone = true; // Don't keep inside workarea if the window has specially configured position partial_keep_in_area = true; area = workspace()->clientArea(FullArea, geom.center(), desktop()); } if (!placementDone) { // Placement needs to be after setting size Placement::self()->place(this, area); // The client may have been moved to another screen, update placement area. area = workspace()->clientArea(PlacementArea, this); dontKeepInArea = true; placementDone = true; } // bugs #285967, #286146, #183694 // geometry() now includes the requested size and the decoration and is at the correct screen/position (hopefully) // Maximization for oversized windows must happen NOW. // If we effectively pass keepInArea(), the window will resizeWithChecks() - i.e. constrained // to the combo of all screen MINUS all struts on the edges // If only one screen struts, this will affect screens as a side-effect, the window is artificailly shrinked // below the screen size and as result no more maximized what breaks KMainWindow's stupid width+1, height+1 hack // TODO: get KMainWindow a correct state storage what will allow to store the restore size as well. if (!session) { // has a better handling of this geom_restore = frameGeometry(); // Remember restore geometry if (isMaximizable() && (width() >= area.width() || height() >= area.height())) { // Window is too large for the screen, maximize in the // directions necessary const QSize ss = workspace()->clientArea(ScreenArea, area.center(), desktop()).size(); const QRect fsa = workspace()->clientArea(FullArea, geom.center(), desktop()); const QSize cs = clientSize(); int pseudo_max = ((info->state() & NET::MaxVert) ? MaximizeVertical : 0) | ((info->state() & NET::MaxHoriz) ? MaximizeHorizontal : 0); if (width() >= area.width()) pseudo_max |= MaximizeHorizontal; if (height() >= area.height()) pseudo_max |= MaximizeVertical; // heuristics: // if decorated client is smaller than the entire screen, the user might want to move it around (multiscreen) // in this case, if the decorated client is bigger than the screen (+1), we don't take this as an // attempt for maximization, but just constrain the size (the window simply wants to be bigger) // NOTICE // i intended a second check on cs < area.size() ("the managed client ("minus border") is smaller // than the workspace") but gtk / gimp seems to store it's size including the decoration, // thus a former maximized window wil become non-maximized bool keepInFsArea = false; if (width() < fsa.width() && (cs.width() > ss.width()+1)) { pseudo_max &= ~MaximizeHorizontal; keepInFsArea = true; } if (height() < fsa.height() && (cs.height() > ss.height()+1)) { pseudo_max &= ~MaximizeVertical; keepInFsArea = true; } if (pseudo_max != MaximizeRestore) { maximize((MaximizeMode)pseudo_max); // from now on, care about maxmode, since the maximization call will override mode for fix aspects dontKeepInArea |= (max_mode == MaximizeFull); geom_restore = QRect(); // Use placement when unmaximizing ... if (!(max_mode & MaximizeVertical)) { geom_restore.setY(y()); // ...but only for horizontal direction geom_restore.setHeight(height()); } if (!(max_mode & MaximizeHorizontal)) { geom_restore.setX(x()); // ...but only for vertical direction geom_restore.setWidth(width()); } } if (keepInFsArea) keepInArea(fsa, partial_keep_in_area); } } if ((!isSpecialWindow() || isToolbar()) && isMovable() && !dontKeepInArea) keepInArea(area, partial_keep_in_area); updateShape(); // CT: Extra check for stupid jdk 1.3.1. But should make sense in general // if client has initial state set to Iconic and is transient with a parent // window that is not Iconic, set init_state to Normal if (init_minimize && isTransient()) { auto mainclients = mainClients(); for (auto it = mainclients.constBegin(); it != mainclients.constEnd(); ++it) if ((*it)->isShown(true)) init_minimize = false; // SELI TODO: Even e.g. for NET::Utility? } // If a dialog is shown for minimized window, minimize it too if (!init_minimize && isTransient() && mainClients().count() > 0 && workspace()->sessionManager()->state() != SessionState::Saving) { bool visible_parent = false; // Use allMainClients(), to include also main clients of group transients // that have been optimized out in X11Client::checkGroupTransients() auto mainclients = allMainClients(); for (auto it = mainclients.constBegin(); it != mainclients.constEnd(); ++it) if ((*it)->isShown(true)) visible_parent = true; if (!visible_parent) { init_minimize = true; demandAttention(); } } if (init_minimize) minimize(true); // No animation // Other settings from the previous session if (session) { // Session restored windows are not considered to be new windows WRT rules, // I.e. obey only forcing rules setKeepAbove(session->keepAbove); setKeepBelow(session->keepBelow); setOriginalSkipTaskbar(session->skipTaskbar); setSkipPager(session->skipPager); setSkipSwitcher(session->skipSwitcher); setShade(session->shaded ? ShadeNormal : ShadeNone); setOpacity(session->opacity); geom_restore = session->restore; if (session->maximized != MaximizeRestore) { maximize(MaximizeMode(session->maximized)); } if (session->fullscreen != FullScreenNone) { setFullScreen(true, false); geom_fs_restore = session->fsrestore; } checkOffscreenPosition(&geom_restore, area); checkOffscreenPosition(&geom_fs_restore, area); } else { // Window may want to be maximized // done after checking that the window isn't larger than the workarea, so that // the restore geometry from the checks above takes precedence, and window // isn't restored larger than the workarea MaximizeMode maxmode = static_cast( ((info->state() & NET::MaxVert) ? MaximizeVertical : 0) | ((info->state() & NET::MaxHoriz) ? MaximizeHorizontal : 0)); MaximizeMode forced_maxmode = rules()->checkMaximize(maxmode, !isMapped); // Either hints were set to maximize, or is forced to maximize, // or is forced to non-maximize and hints were set to maximize if (forced_maxmode != MaximizeRestore || maxmode != MaximizeRestore) maximize(forced_maxmode); // Read other initial states setShade(rules()->checkShade(info->state() & NET::Shaded ? ShadeNormal : ShadeNone, !isMapped)); setKeepAbove(rules()->checkKeepAbove(info->state() & NET::KeepAbove, !isMapped)); setKeepBelow(rules()->checkKeepBelow(info->state() & NET::KeepBelow, !isMapped)); setOriginalSkipTaskbar(rules()->checkSkipTaskbar(info->state() & NET::SkipTaskbar, !isMapped)); setSkipPager(rules()->checkSkipPager(info->state() & NET::SkipPager, !isMapped)); setSkipSwitcher(rules()->checkSkipSwitcher(info->state() & NET::SkipSwitcher, !isMapped)); if (info->state() & NET::DemandsAttention) demandAttention(); if (info->state() & NET::Modal) setModal(true); setFullScreen(rules()->checkFullScreen(info->state() & NET::FullScreen, !isMapped), false); } updateAllowedActions(true); // Set initial user time directly m_userTime = readUserTimeMapTimestamp(asn_valid ? &asn_id : nullptr, asn_valid ? &asn_data : nullptr, session); group()->updateUserTime(m_userTime); // And do what X11Client::updateUserTime() does // This should avoid flicker, because real restacking is done // only after manage() finishes because of blocking, but the window is shown sooner m_frame.lower(); if (session && session->stackingOrder != -1) { sm_stacking_order = session->stackingOrder; workspace()->restoreSessionStackingOrder(this); } if (compositing()) // Sending ConfigureNotify is done when setting mapping state below, // Getting the first sync response means window is ready for compositing sendSyncRequest(); else ready_for_painting = true; // set to true in case compositing is turned on later. bug #160393 if (isShown(true)) { bool allow; if (session) allow = session->active && (!workspace()->wasUserInteraction() || workspace()->activeClient() == nullptr || workspace()->activeClient()->isDesktop()); else allow = workspace()->allowClientActivation(this, userTime(), false); const bool isSessionSaving = workspace()->sessionManager()->state() == SessionState::Saving; // If session saving, force showing new windows (i.e. "save file?" dialogs etc.) // also force if activation is allowed if( !isOnCurrentDesktop() && !isMapped && !session && ( allow || isSessionSaving )) VirtualDesktopManager::self()->setCurrent( desktop()); // If the window is on an inactive activity during session saving, temporarily force it to show. if( !isMapped && !session && isSessionSaving && !isOnCurrentActivity()) { setSessionActivityOverride( true ); foreach( AbstractClient* c, mainClients()) { if (X11Client *mc = dynamic_cast(c)) { mc->setSessionActivityOverride(true); } } } if (isOnCurrentDesktop() && !isMapped && !allow && (!session || session->stackingOrder < 0)) workspace()->restackClientUnderActive(this); updateVisibility(); if (!isMapped) { if (allow && isOnCurrentDesktop()) { if (!isSpecialWindow()) if (options->focusPolicyIsReasonable() && wantsTabFocus()) workspace()->requestFocus(this); } else if (!session && !isSpecialWindow()) demandAttention(); } } else updateVisibility(); Q_ASSERT(mapping_state != Withdrawn); m_managed = true; blockGeometryUpdates(false); if (m_userTime == XCB_TIME_CURRENT_TIME || m_userTime == -1U) { // No known user time, set something old m_userTime = xTime() - 1000000; if (m_userTime == XCB_TIME_CURRENT_TIME || m_userTime == -1U) // Let's be paranoid m_userTime = xTime() - 1000000 + 10; } //sendSyntheticConfigureNotify(); // Done when setting mapping state delete session; discardTemporaryRules(); applyWindowRules(); // Just in case RuleBook::self()->discardUsed(this, false); // Remove ApplyNow rules updateWindowRules(Rules::All); // Was blocked while !isManaged() setBlockingCompositing(info->isBlockingCompositing()); readShowOnScreenEdge(showOnScreenEdgeCookie); // Forward all opacity values to the frame in case there'll be other CM running. connect(Compositor::self(), &Compositor::compositingToggled, this, [this](bool active) { if (active) { return; } if (opacity() == 1.0) { return; } NETWinInfo info(connection(), frameId(), rootWindow(), NET::Properties(), NET::Properties2()); info.setOpacity(static_cast(opacity() * 0xffffffff)); } ); // TODO: there's a small problem here - isManaged() depends on the mapping state, // but this client is not yet in Workspace's client list at this point, will // be only done in addClient() emit clientManaging(this); return true; } // Called only from manage() void X11Client::embedClient(xcb_window_t w, xcb_visualid_t visualid, xcb_colormap_t colormap, uint8_t depth) { Q_ASSERT(m_client == XCB_WINDOW_NONE); Q_ASSERT(frameId() == XCB_WINDOW_NONE); Q_ASSERT(m_wrapper == XCB_WINDOW_NONE); m_client.reset(w, false); const uint32_t zero_value = 0; xcb_connection_t *conn = connection(); // We don't want the window to be destroyed when we quit xcb_change_save_set(conn, XCB_SET_MODE_INSERT, m_client); m_client.selectInput(zero_value); m_client.unmap(); m_client.setBorderWidth(zero_value); // Note: These values must match the order in the xcb_cw_t enum const uint32_t cw_values[] = { 0, // back_pixmap 0, // border_pixel colormap, // colormap Cursor::x11Cursor(Qt::ArrowCursor) }; const uint32_t cw_mask = XCB_CW_BACK_PIXMAP | XCB_CW_BORDER_PIXEL | XCB_CW_COLORMAP | XCB_CW_CURSOR; const uint32_t common_event_mask = XCB_EVENT_MASK_KEY_PRESS | XCB_EVENT_MASK_KEY_RELEASE | XCB_EVENT_MASK_ENTER_WINDOW | XCB_EVENT_MASK_LEAVE_WINDOW | XCB_EVENT_MASK_BUTTON_PRESS | XCB_EVENT_MASK_BUTTON_RELEASE | XCB_EVENT_MASK_BUTTON_MOTION | XCB_EVENT_MASK_POINTER_MOTION | XCB_EVENT_MASK_KEYMAP_STATE | XCB_EVENT_MASK_FOCUS_CHANGE | XCB_EVENT_MASK_EXPOSURE | XCB_EVENT_MASK_STRUCTURE_NOTIFY | XCB_EVENT_MASK_SUBSTRUCTURE_REDIRECT; const uint32_t frame_event_mask = common_event_mask | XCB_EVENT_MASK_PROPERTY_CHANGE | XCB_EVENT_MASK_VISIBILITY_CHANGE; const uint32_t wrapper_event_mask = common_event_mask | XCB_EVENT_MASK_SUBSTRUCTURE_NOTIFY; const uint32_t client_event_mask = XCB_EVENT_MASK_FOCUS_CHANGE | XCB_EVENT_MASK_PROPERTY_CHANGE | XCB_EVENT_MASK_COLOR_MAP_CHANGE | XCB_EVENT_MASK_ENTER_WINDOW | XCB_EVENT_MASK_LEAVE_WINDOW | XCB_EVENT_MASK_KEY_PRESS | XCB_EVENT_MASK_KEY_RELEASE; // Create the frame window xcb_window_t frame = xcb_generate_id(conn); xcb_create_window(conn, depth, frame, rootWindow(), 0, 0, 1, 1, 0, XCB_WINDOW_CLASS_INPUT_OUTPUT, visualid, cw_mask, cw_values); m_frame.reset(frame); setWindowHandles(m_client); // Create the wrapper window xcb_window_t wrapperId = xcb_generate_id(conn); xcb_create_window(conn, depth, wrapperId, frame, 0, 0, 1, 1, 0, XCB_WINDOW_CLASS_INPUT_OUTPUT, visualid, cw_mask, cw_values); m_wrapper.reset(wrapperId); m_client.reparent(m_wrapper); // We could specify the event masks when we create the windows, but the original // Xlib code didn't. Let's preserve that behavior here for now so we don't end up // receiving any unexpected events from the wrapper creation or the reparenting. m_frame.selectInput(frame_event_mask); m_wrapper.selectInput(wrapper_event_mask); m_client.selectInput(client_event_mask); updateMouseGrab(); } void X11Client::updateInputWindow() { if (!Xcb::Extensions::self()->isShapeInputAvailable()) return; QRegion region; if (!noBorder() && isDecorated()) { const QMargins &r = decoration()->resizeOnlyBorders(); const int left = r.left(); const int top = r.top(); const int right = r.right(); const int bottom = r.bottom(); if (left != 0 || top != 0 || right != 0 || bottom != 0) { region = QRegion(-left, -top, decoration()->size().width() + left + right, decoration()->size().height() + top + bottom); region = region.subtracted(decoration()->rect()); } } if (region.isEmpty()) { m_decoInputExtent.reset(); return; } QRect bounds = region.boundingRect(); input_offset = bounds.topLeft(); // Move the bounding rect to screen coordinates bounds.translate(frameGeometry().topLeft()); // Move the region to input window coordinates region.translate(-input_offset); if (!m_decoInputExtent.isValid()) { const uint32_t mask = XCB_CW_OVERRIDE_REDIRECT | XCB_CW_EVENT_MASK; const uint32_t values[] = {true, XCB_EVENT_MASK_ENTER_WINDOW | XCB_EVENT_MASK_LEAVE_WINDOW | XCB_EVENT_MASK_BUTTON_PRESS | XCB_EVENT_MASK_BUTTON_RELEASE | XCB_EVENT_MASK_POINTER_MOTION }; m_decoInputExtent.create(bounds, XCB_WINDOW_CLASS_INPUT_ONLY, mask, values); if (mapping_state == Mapped) m_decoInputExtent.map(); } else { m_decoInputExtent.setGeometry(bounds); } const QVector rects = Xcb::regionToRects(region); xcb_shape_rectangles(connection(), XCB_SHAPE_SO_SET, XCB_SHAPE_SK_INPUT, XCB_CLIP_ORDERING_UNSORTED, m_decoInputExtent, 0, 0, rects.count(), rects.constData()); } void X11Client::updateDecoration(bool check_workspace_pos, bool force) { if (!force && ((!isDecorated() && noBorder()) || (isDecorated() && !noBorder()))) return; QRect oldgeom = frameGeometry(); QRect oldClientGeom = oldgeom.adjusted(borderLeft(), borderTop(), -borderRight(), -borderBottom()); blockGeometryUpdates(true); if (force) destroyDecoration(); if (!noBorder()) { createDecoration(oldgeom); } else destroyDecoration(); updateShadow(); if (check_workspace_pos) checkWorkspacePosition(oldgeom, -2, oldClientGeom); updateInputWindow(); blockGeometryUpdates(false); updateFrameExtents(); } void X11Client::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::updateShadow); connect(decoration, &KDecoration2::Decoration::resizeOnlyBordersChanged, this, &X11Client::updateInputWindow); connect(decoration, &KDecoration2::Decoration::bordersChanged, this, [this]() { updateFrameExtents(); GeometryUpdatesBlocker blocker(this); // TODO: this is obviously idempotent // calculateGravitation(true) would have to operate on the old border sizes // move(calculateGravitation(true)); // move(calculateGravitation(false)); QRect oldgeom = frameGeometry(); plainResize(sizeForClientSize(clientSize()), ForceGeometrySet); if (!isShade()) checkWorkspacePosition(oldgeom); emit geometryShapeChanged(this, oldgeom); } ); connect(decoratedClient()->decoratedClient(), &KDecoration2::DecoratedClient::widthChanged, this, &X11Client::updateInputWindow); connect(decoratedClient()->decoratedClient(), &KDecoration2::DecoratedClient::heightChanged, this, &X11Client::updateInputWindow); } setDecoration(decoration); move(calculateGravitation(false)); plainResize(sizeForClientSize(clientSize()), ForceGeometrySet); if (Compositor::compositing()) { discardWindowPixmap(); } emit geometryShapeChanged(this, oldgeom); } void X11Client::destroyDecoration() { QRect oldgeom = frameGeometry(); if (isDecorated()) { QPoint grav = calculateGravitation(true); AbstractClient::destroyDecoration(); plainResize(sizeForClientSize(clientSize()), ForceGeometrySet); move(grav); if (compositing()) discardWindowPixmap(); if (!deleting) { emit geometryShapeChanged(this, oldgeom); } } m_decoInputExtent.reset(); } void X11Client::layoutDecorationRects(QRect &left, QRect &top, QRect &right, QRect &bottom) const { if (!isDecorated()) { return; } QRect r = decoration()->rect(); NETStrut strut = info->frameOverlap(); // Ignore the overlap strut when compositing is disabled if (!compositing()) strut.left = strut.top = strut.right = strut.bottom = 0; else if (strut.left == -1 && strut.top == -1 && strut.right == -1 && strut.bottom == -1) { top = QRect(r.x(), r.y(), r.width(), r.height() / 3); left = QRect(r.x(), r.y() + top.height(), width() / 2, r.height() / 3); right = QRect(r.x() + left.width(), r.y() + top.height(), r.width() - left.width(), left.height()); bottom = QRect(r.x(), r.y() + top.height() + left.height(), r.width(), r.height() - left.height() - top.height()); return; } top = QRect(r.x(), r.y(), r.width(), borderTop() + strut.top); bottom = QRect(r.x(), r.y() + r.height() - borderBottom() - strut.bottom, r.width(), borderBottom() + strut.bottom); left = QRect(r.x(), r.y() + top.height(), borderLeft() + strut.left, r.height() - top.height() - bottom.height()); right = QRect(r.x() + r.width() - borderRight() - strut.right, r.y() + top.height(), borderRight() + strut.right, r.height() - top.height() - bottom.height()); } QRect X11Client::transparentRect() const { if (isShade()) return QRect(); NETStrut strut = info->frameOverlap(); // Ignore the strut when compositing is disabled or the decoration doesn't support it if (!compositing()) strut.left = strut.top = strut.right = strut.bottom = 0; else if (strut.left == -1 && strut.top == -1 && strut.right == -1 && strut.bottom == -1) return QRect(); const QRect r = QRect(clientPos(), clientSize()) .adjusted(strut.left, strut.top, -strut.right, -strut.bottom); if (r.isValid()) return r; return QRect(); } void X11Client::detectNoBorder() { if (shape()) { noborder = true; app_noborder = true; return; } switch(windowType()) { case NET::Desktop : case NET::Dock : case NET::TopMenu : case NET::Splash : case NET::Notification : case NET::OnScreenDisplay : case NET::CriticalNotification : noborder = true; app_noborder = true; break; case NET::Unknown : case NET::Normal : case NET::Toolbar : case NET::Menu : case NET::Dialog : case NET::Utility : noborder = false; break; default: abort(); } // NET::Override is some strange beast without clear definition, usually // just meaning "noborder", so let's treat it only as such flag, and ignore it as // a window type otherwise (SUPPORTED_WINDOW_TYPES_MASK doesn't include it) if (info->windowType(NET::OverrideMask) == NET::Override) { noborder = true; app_noborder = true; } } void X11Client::updateFrameExtents() { NETStrut strut; strut.left = borderLeft(); strut.right = borderRight(); strut.top = borderTop(); strut.bottom = borderBottom(); info->setFrameExtents(strut); } void X11Client::setClientFrameExtents(const NETStrut &strut) { const QMargins clientFrameExtents(strut.left, strut.top, strut.right, strut.bottom); if (m_clientFrameExtents == clientFrameExtents) { return; } const bool wasClientSideDecorated = isClientSideDecorated(); m_clientFrameExtents = clientFrameExtents; // We should resize the client when its custom frame extents are changed so // the logical bounds remain the same. This however means that we will send // several configure requests to the application upon restoring it from the // maximized or fullscreen state. Notice that a client-side decorated client // cannot be shaded, therefore it's okay not to use the adjusted size here. setFrameGeometry(frameGeometry()); if (wasClientSideDecorated != isClientSideDecorated()) { emit clientSideDecoratedChanged(); } // This will invalidate the window quads cache. emit geometryShapeChanged(this, frameGeometry()); } /** * Resizes the decoration, and makes sure the decoration widget gets resize event * even if the size hasn't changed. This is needed to make sure the decoration * re-layouts (e.g. when maximization state changes, * the decoration may alter some borders, but the actual size * of the decoration stays the same). */ void X11Client::resizeDecoration() { triggerDecorationRepaint(); updateInputWindow(); } bool X11Client::userNoBorder() const { return noborder; } bool X11Client::isFullScreenable() const { if (!rules()->checkFullScreen(true)) { return false; } if (rules()->checkStrictGeometry(true)) { // check geometry constraints (rule to obey is set) const QRect fsarea = workspace()->clientArea(FullScreenArea, this); if (sizeForClientSize(fsarea.size(), SizeModeAny, true) != fsarea.size()) { return false; // the app wouldn't fit exactly fullscreen geometry due to its strict geometry requirements } } // don't check size constrains - some apps request fullscreen despite requesting fixed size return !isSpecialWindow(); // also better disallow only weird types to go fullscreen } bool X11Client::noBorder() const { return userNoBorder() || isFullScreen(); } bool X11Client::userCanSetNoBorder() const { // Client-side decorations and server-side decorations are mutually exclusive. if (isClientSideDecorated()) { return false; } return !isFullScreen() && !isShade(); } void X11Client::setNoBorder(bool set) { if (!userCanSetNoBorder()) return; set = rules()->checkNoBorder(set); if (noborder == set) return; noborder = set; updateDecoration(true, false); updateWindowRules(Rules::NoBorder); } void X11Client::checkNoBorder() { setNoBorder(app_noborder); } bool X11Client::wantsShadowToBeRendered() const { return !isFullScreen() && maximizeMode() != MaximizeFull; } void X11Client::updateShape() { if (shape()) { // Workaround for #19644 - Shaped windows shouldn't have decoration if (!app_noborder) { // Only when shape is detected for the first time, still let the user to override app_noborder = true; noborder = rules()->checkNoBorder(true); updateDecoration(true); } if (noBorder()) { xcb_shape_combine(connection(), XCB_SHAPE_SO_SET, XCB_SHAPE_SK_BOUNDING, XCB_SHAPE_SK_BOUNDING, frameId(), clientPos().x(), clientPos().y(), window()); } } else if (app_noborder) { xcb_shape_mask(connection(), XCB_SHAPE_SO_SET, XCB_SHAPE_SK_BOUNDING, frameId(), 0, 0, XCB_PIXMAP_NONE); detectNoBorder(); app_noborder = noborder; noborder = rules()->checkNoBorder(noborder || m_motif.noBorder()); updateDecoration(true); } // Decoration mask (i.e. 'else' here) setting is done in setMask() // when the decoration calls it or when the decoration is created/destroyed updateInputShape(); if (compositing()) { addRepaintFull(); addWorkspaceRepaint(visibleRect()); // In case shape change removes part of this window } emit geometryShapeChanged(this, frameGeometry()); } static Xcb::Window shape_helper_window(XCB_WINDOW_NONE); void X11Client::cleanupX11() { shape_helper_window.reset(); } void X11Client::updateInputShape() { if (hiddenPreview()) // Sets it to none, don't change return; if (Xcb::Extensions::self()->isShapeInputAvailable()) { // There appears to be no way to find out if a window has input // shape set or not, so always propagate the input shape // (it's the same like the bounding shape by default). // Also, build the shape using a helper window, not directly // in the frame window, because the sequence set-shape-to-frame, // remove-shape-of-client, add-input-shape-of-client has the problem // that after the second step there's a hole in the input shape // until the real shape of the client is added and that can make // the window lose focus (which is a problem with mouse focus policies) // TODO: It seems there is, after all - XShapeGetRectangles() - but maybe this is better if (!shape_helper_window.isValid()) shape_helper_window.create(QRect(0, 0, 1, 1)); shape_helper_window.resize(m_bufferGeometry.size()); xcb_connection_t *c = connection(); xcb_shape_combine(c, XCB_SHAPE_SO_SET, XCB_SHAPE_SK_INPUT, XCB_SHAPE_SK_BOUNDING, shape_helper_window, 0, 0, frameId()); xcb_shape_combine(c, XCB_SHAPE_SO_SUBTRACT, XCB_SHAPE_SK_INPUT, XCB_SHAPE_SK_BOUNDING, shape_helper_window, clientPos().x(), clientPos().y(), window()); xcb_shape_combine(c, XCB_SHAPE_SO_UNION, XCB_SHAPE_SK_INPUT, XCB_SHAPE_SK_INPUT, shape_helper_window, clientPos().x(), clientPos().y(), window()); xcb_shape_combine(c, XCB_SHAPE_SO_SET, XCB_SHAPE_SK_INPUT, XCB_SHAPE_SK_INPUT, frameId(), 0, 0, shape_helper_window); } } void X11Client::hideClient(bool hide) { if (hidden == hide) return; hidden = hide; updateVisibility(); } bool X11Client::setupCompositing() { if (!Toplevel::setupCompositing()){ return false; } updateVisibility(); // for internalKeep() return true; } void X11Client::finishCompositing(ReleaseReason releaseReason) { Toplevel::finishCompositing(releaseReason); updateVisibility(); // for safety in case KWin is just resizing the window resetHaveResizeEffect(); } /** * Returns whether the window is minimizable or not */ bool X11Client::isMinimizable() const { if (isSpecialWindow() && !isTransient()) return false; if (!rules()->checkMinimize(true)) return false; if (isTransient()) { // #66868 - Let other xmms windows be minimized when the mainwindow is minimized bool shown_mainwindow = false; auto mainclients = mainClients(); for (auto it = mainclients.constBegin(); it != mainclients.constEnd(); ++it) if ((*it)->isShown(true)) shown_mainwindow = true; if (!shown_mainwindow) return true; } #if 0 // This is here because kicker's taskbar doesn't provide separate entries // for windows with an explicitly given parent // TODO: perhaps this should be redone // Disabled for now, since at least modal dialogs should be minimizable // (resulting in the mainwindow being minimized too). if (transientFor() != NULL) return false; #endif if (!wantsTabFocus()) // SELI, TODO: - NET::Utility? why wantsTabFocus() - skiptaskbar? ? return false; return true; } void X11Client::doMinimize() { updateVisibility(); updateAllowedActions(); workspace()->updateMinimizedOfTransients(this); } QRect X11Client::iconGeometry() const { NETRect r = info->iconGeometry(); QRect geom(r.pos.x, r.pos.y, r.size.width, r.size.height); if (geom.isValid()) return geom; else { // Check all mainwindows of this window (recursively) foreach (AbstractClient * amainwin, mainClients()) { X11Client *mainwin = dynamic_cast(amainwin); if (!mainwin) { continue; } geom = mainwin->iconGeometry(); if (geom.isValid()) return geom; } // No mainwindow (or their parents) with icon geometry was found return AbstractClient::iconGeometry(); } } bool X11Client::isShadeable() const { return !isSpecialWindow() && !noBorder() && (rules()->checkShade(ShadeNormal) != rules()->checkShade(ShadeNone)); } void X11Client::setShade(ShadeMode mode) { if (mode == ShadeHover && isMove()) return; // causes geometry breaks and is probably nasty if (isSpecialWindow() || noBorder()) mode = ShadeNone; mode = rules()->checkShade(mode); if (shade_mode == mode) return; bool was_shade = isShade(); ShadeMode was_shade_mode = shade_mode; shade_mode = mode; // Decorations may turn off some borders when shaded // this has to happen _before_ the tab alignment since it will restrict the minimum geometry #if 0 if (decoration) decoration->borders(border_left, border_right, border_top, border_bottom); #endif if (was_shade == isShade()) { // Decoration may want to update after e.g. hover-shade changes emit shadeChanged(); return; // No real change in shaded state } Q_ASSERT(isDecorated()); // noborder windows can't be shaded GeometryUpdatesBlocker blocker(this); // TODO: All this unmapping, resizing etc. feels too much duplicated from elsewhere if (isShade()) { // shade_mode == ShadeNormal addWorkspaceRepaint(visibleRect()); // Shade shade_geometry_change = true; QSize s(sizeForClientSize(QSize(clientSize()))); s.setHeight(borderTop() + borderBottom()); m_wrapper.selectInput(ClientWinMask); // Avoid getting UnmapNotify m_wrapper.unmap(); m_client.unmap(); m_wrapper.selectInput(ClientWinMask | XCB_EVENT_MASK_SUBSTRUCTURE_NOTIFY); exportMappingState(XCB_ICCCM_WM_STATE_ICONIC); plainResize(s); shade_geometry_change = false; if (was_shade_mode == ShadeHover) { if (shade_below && workspace()->stackingOrder().indexOf(shade_below) > -1) workspace()->restack(this, shade_below, true); if (isActive()) workspace()->activateNextClient(this); } else if (isActive()) { workspace()->focusToNull(); } } else { shade_geometry_change = true; if (decoratedClient()) decoratedClient()->signalShadeChange(); QSize s(sizeForClientSize(clientSize())); shade_geometry_change = false; plainResize(s); setGeometryRestore(frameGeometry()); if ((shade_mode == ShadeHover || shade_mode == ShadeActivated) && rules()->checkAcceptFocus(info->input())) setActive(true); if (shade_mode == ShadeHover) { QList order = workspace()->stackingOrder(); // invalidate, since "this" could be the topmost toplevel and shade_below dangeling shade_below = nullptr; // this is likely related to the index parameter?! for (int idx = order.indexOf(this) + 1; idx < order.count(); ++idx) { shade_below = qobject_cast(order.at(idx)); if (shade_below) { break; } } if (shade_below && shade_below->isNormalWindow()) workspace()->raiseClient(this); else shade_below = nullptr; } m_wrapper.map(); m_client.map(); exportMappingState(XCB_ICCCM_WM_STATE_NORMAL); if (isActive()) workspace()->requestFocus(this); } info->setState(isShade() ? NET::Shaded : NET::States(), NET::Shaded); info->setState(isShown(false) ? NET::States() : NET::Hidden, NET::Hidden); discardWindowPixmap(); updateVisibility(); updateAllowedActions(); updateWindowRules(Rules::Shade); emit shadeChanged(); } void X11Client::shadeHover() { setShade(ShadeHover); cancelShadeHoverTimer(); } void X11Client::shadeUnhover() { setShade(ShadeNormal); cancelShadeHoverTimer(); } void X11Client::cancelShadeHoverTimer() { delete shadeHoverTimer; shadeHoverTimer = nullptr; } void X11Client::toggleShade() { // If the mode is ShadeHover or ShadeActive, cancel shade too setShade(shade_mode == ShadeNone ? ShadeNormal : ShadeNone); } void X11Client::updateVisibility() { if (deleting) return; if (hidden) { info->setState(NET::Hidden, NET::Hidden); setSkipTaskbar(true); // Also hide from taskbar if (compositing() && options->hiddenPreviews() == HiddenPreviewsAlways) internalKeep(); else internalHide(); return; } setSkipTaskbar(originalSkipTaskbar()); // Reset from 'hidden' if (isMinimized()) { info->setState(NET::Hidden, NET::Hidden); if (compositing() && options->hiddenPreviews() == HiddenPreviewsAlways) internalKeep(); else internalHide(); return; } info->setState(NET::States(), NET::Hidden); if (!isOnCurrentDesktop()) { if (compositing() && options->hiddenPreviews() != HiddenPreviewsNever) internalKeep(); else internalHide(); return; } if (!isOnCurrentActivity()) { if (compositing() && options->hiddenPreviews() != HiddenPreviewsNever) internalKeep(); else internalHide(); return; } internalShow(); } /** * Sets the client window's mapping state. Possible values are * WithdrawnState, IconicState, NormalState. */ void X11Client::exportMappingState(int s) { Q_ASSERT(m_client != XCB_WINDOW_NONE); Q_ASSERT(!deleting || s == XCB_ICCCM_WM_STATE_WITHDRAWN); if (s == XCB_ICCCM_WM_STATE_WITHDRAWN) { m_client.deleteProperty(atoms->wm_state); return; } Q_ASSERT(s == XCB_ICCCM_WM_STATE_NORMAL || s == XCB_ICCCM_WM_STATE_ICONIC); int32_t data[2]; data[0] = s; data[1] = XCB_NONE; m_client.changeProperty(atoms->wm_state, atoms->wm_state, 32, 2, data); } void X11Client::internalShow() { if (mapping_state == Mapped) return; MappingState old = mapping_state; mapping_state = Mapped; if (old == Unmapped || old == Withdrawn) map(); if (old == Kept) { m_decoInputExtent.map(); updateHiddenPreview(); } emit windowShown(this); } void X11Client::internalHide() { if (mapping_state == Unmapped) return; MappingState old = mapping_state; mapping_state = Unmapped; if (old == Mapped || old == Kept) unmap(); if (old == Kept) updateHiddenPreview(); addWorkspaceRepaint(visibleRect()); workspace()->clientHidden(this); emit windowHidden(this); } void X11Client::internalKeep() { Q_ASSERT(compositing()); if (mapping_state == Kept) return; MappingState old = mapping_state; mapping_state = Kept; if (old == Unmapped || old == Withdrawn) map(); m_decoInputExtent.unmap(); if (isActive()) workspace()->focusToNull(); // get rid of input focus, bug #317484 updateHiddenPreview(); addWorkspaceRepaint(visibleRect()); workspace()->clientHidden(this); } /** * Maps (shows) the client. Note that it is mapping state of the frame, * not necessarily the client window itself (i.e. a shaded window is here * considered mapped, even though it is in IconicState). */ void X11Client::map() { // XComposite invalidates backing pixmaps on unmap (minimize, different // virtual desktop, etc.). We kept the last known good pixmap around // for use in effects, but now we want to have access to the new pixmap if (compositing()) discardWindowPixmap(); m_frame.map(); if (!isShade()) { m_wrapper.map(); m_client.map(); m_decoInputExtent.map(); exportMappingState(XCB_ICCCM_WM_STATE_NORMAL); } else exportMappingState(XCB_ICCCM_WM_STATE_ICONIC); addLayerRepaint(visibleRect()); } /** * Unmaps the client. Again, this is about the frame. */ void X11Client::unmap() { // Here it may look like a race condition, as some other client might try to unmap // the window between these two XSelectInput() calls. However, they're supposed to // use XWithdrawWindow(), which also sends a synthetic event to the root window, // which won't be missed, so this shouldn't be a problem. The chance the real UnmapNotify // will be missed is also very minimal, so I don't think it's needed to grab the server // here. m_wrapper.selectInput(ClientWinMask); // Avoid getting UnmapNotify m_frame.unmap(); m_wrapper.unmap(); m_client.unmap(); m_decoInputExtent.unmap(); m_wrapper.selectInput(ClientWinMask | XCB_EVENT_MASK_SUBSTRUCTURE_NOTIFY); exportMappingState(XCB_ICCCM_WM_STATE_ICONIC); } /** * XComposite doesn't keep window pixmaps of unmapped windows, which means * there wouldn't be any previews of windows that are minimized or on another * virtual desktop. Therefore rawHide() actually keeps such windows mapped. * However special care needs to be taken so that such windows don't interfere. * Therefore they're put very low in the stacking order and they have input shape * set to none, which hopefully is enough. If there's no input shape available, * then it's hoped that there will be some other desktop above it *shrug*. * Using normal shape would be better, but that'd affect other things, e.g. painting * of the actual preview. */ void X11Client::updateHiddenPreview() { if (hiddenPreview()) { workspace()->forceRestacking(); if (Xcb::Extensions::self()->isShapeInputAvailable()) { xcb_shape_rectangles(connection(), XCB_SHAPE_SO_SET, XCB_SHAPE_SK_INPUT, XCB_CLIP_ORDERING_UNSORTED, frameId(), 0, 0, 0, nullptr); } } else { workspace()->forceRestacking(); updateInputShape(); } } void X11Client::sendClientMessage(xcb_window_t w, xcb_atom_t a, xcb_atom_t protocol, uint32_t data1, uint32_t data2, uint32_t data3, xcb_timestamp_t timestamp) { xcb_client_message_event_t ev; memset(&ev, 0, sizeof(ev)); ev.response_type = XCB_CLIENT_MESSAGE; ev.window = w; ev.type = a; ev.format = 32; ev.data.data32[0] = protocol; ev.data.data32[1] = timestamp; ev.data.data32[2] = data1; ev.data.data32[3] = data2; ev.data.data32[4] = data3; uint32_t eventMask = 0; if (w == rootWindow()) { eventMask = XCB_EVENT_MASK_SUBSTRUCTURE_REDIRECT; // Magic! } xcb_send_event(connection(), false, w, eventMask, reinterpret_cast(&ev)); xcb_flush(connection()); } /** * Returns whether the window may be closed (have a close button) */ bool X11Client::isCloseable() const { return rules()->checkCloseable(m_motif.close() && !isSpecialWindow()); } /** * Closes the window by either sending a delete_window message or using XKill. */ void X11Client::closeWindow() { if (!isCloseable()) return; // Update user time, because the window may create a confirming dialog. updateUserTime(); if (info->supportsProtocol(NET::DeleteWindowProtocol)) { sendClientMessage(window(), atoms->wm_protocols, atoms->wm_delete_window); pingWindow(); } else // Client will not react on wm_delete_window. We have not choice // but destroy his connection to the XServer. killWindow(); } /** * Kills the window via XKill */ void X11Client::killWindow() { qCDebug(KWIN_CORE) << "X11Client::killWindow():" << caption(); killProcess(false); m_client.kill(); // Always kill this client at the server destroyClient(); } /** * Send a ping to the window using _NET_WM_PING if possible if it * doesn't respond within a reasonable time, it will be killed. */ void X11Client::pingWindow() { if (!info->supportsProtocol(NET::PingProtocol)) return; // Can't ping :( if (options->killPingTimeout() == 0) return; // Turned off if (ping_timer != nullptr) return; // Pinging already ping_timer = new QTimer(this); connect(ping_timer, &QTimer::timeout, this, [this]() { if (unresponsive()) { qCDebug(KWIN_CORE) << "Final ping timeout, asking to kill:" << caption(); ping_timer->deleteLater(); ping_timer = nullptr; killProcess(true, m_pingTimestamp); return; } qCDebug(KWIN_CORE) << "First ping timeout:" << caption(); setUnresponsive(true); ping_timer->start(); } ); ping_timer->setSingleShot(true); // we'll run the timer twice, at first we'll desaturate the window // and the second time we'll show the "do you want to kill" prompt ping_timer->start(options->killPingTimeout() / 2); m_pingTimestamp = xTime(); workspace()->sendPingToWindow(window(), m_pingTimestamp); } void X11Client::gotPing(xcb_timestamp_t timestamp) { // Just plain compare is not good enough because of 64bit and truncating and whatnot if (NET::timestampCompare(timestamp, m_pingTimestamp) != 0) return; delete ping_timer; ping_timer = nullptr; setUnresponsive(false); if (m_killHelperPID && !::kill(m_killHelperPID, 0)) { // means the process is alive ::kill(m_killHelperPID, SIGTERM); m_killHelperPID = 0; } } void X11Client::killProcess(bool ask, xcb_timestamp_t timestamp) { if (m_killHelperPID && !::kill(m_killHelperPID, 0)) // means the process is alive return; Q_ASSERT(!ask || timestamp != XCB_TIME_CURRENT_TIME); pid_t pid = info->pid(); if (pid <= 0 || clientMachine()->hostName().isEmpty()) // Needed properties missing return; qCDebug(KWIN_CORE) << "Kill process:" << pid << "(" << clientMachine()->hostName() << ")"; if (!ask) { if (!clientMachine()->isLocal()) { QStringList lst; lst << QString::fromUtf8(clientMachine()->hostName()) << QStringLiteral("kill") << QString::number(pid); QProcess::startDetached(QStringLiteral("xon"), lst); } else ::kill(pid, SIGTERM); } else { QString hostname = clientMachine()->isLocal() ? QStringLiteral("localhost") : QString::fromUtf8(clientMachine()->hostName()); // execute helper from build dir or the system installed one const QFileInfo buildDirBinary{QDir{QCoreApplication::applicationDirPath()}, QStringLiteral("kwin_killer_helper")}; QProcess::startDetached(buildDirBinary.exists() ? buildDirBinary.absoluteFilePath() : QStringLiteral(KWIN_KILLER_BIN), QStringList() << QStringLiteral("--pid") << QString::number(unsigned(pid)) << QStringLiteral("--hostname") << hostname << QStringLiteral("--windowname") << captionNormal() << QStringLiteral("--applicationname") << QString::fromUtf8(resourceClass()) << QStringLiteral("--wid") << QString::number(window()) << QStringLiteral("--timestamp") << QString::number(timestamp), QString(), &m_killHelperPID); } } void X11Client::doSetSkipTaskbar() { info->setState(skipTaskbar() ? NET::SkipTaskbar : NET::States(), NET::SkipTaskbar); } void X11Client::doSetSkipPager() { info->setState(skipPager() ? NET::SkipPager : NET::States(), NET::SkipPager); } void X11Client::doSetSkipSwitcher() { info->setState(skipSwitcher() ? NET::SkipSwitcher : NET::States(), NET::SkipSwitcher); } void X11Client::doSetDesktop(int desktop, int was_desk) { Q_UNUSED(desktop) Q_UNUSED(was_desk) updateVisibility(); } /** * Sets whether the client is on @p activity. * If you remove it from its last activity, then it's on all activities. * * Note: If it was on all activities and you try to remove it from one, nothing will happen; * I don't think that's an important enough use case to handle here. */ void X11Client::setOnActivity(const QString &activity, bool enable) { #ifdef KWIN_BUILD_ACTIVITIES if (! Activities::self()) { return; } QStringList newActivitiesList = activities(); if (newActivitiesList.contains(activity) == enable) //nothing to do return; if (enable) { QStringList allActivities = Activities::self()->all(); if (!allActivities.contains(activity)) //bogus ID return; newActivitiesList.append(activity); } else newActivitiesList.removeOne(activity); setOnActivities(newActivitiesList); #else Q_UNUSED(activity) Q_UNUSED(enable) #endif } /** * set exactly which activities this client is on */ void X11Client::setOnActivities(QStringList newActivitiesList) { #ifdef KWIN_BUILD_ACTIVITIES if (!Activities::self()) { return; } QString joinedActivitiesList = newActivitiesList.join(QStringLiteral(",")); joinedActivitiesList = rules()->checkActivity(joinedActivitiesList, false); newActivitiesList = joinedActivitiesList.split(u',', QString::SkipEmptyParts); QStringList allActivities = Activities::self()->all(); auto it = newActivitiesList.begin(); while (it != newActivitiesList.end()) { if (! allActivities.contains(*it)) { it = newActivitiesList.erase(it); } else { it++; } } if (// If we got the request to be on all activities explicitly newActivitiesList.isEmpty() || joinedActivitiesList == Activities::nullUuid() || // If we got a list of activities that covers all activities (newActivitiesList.count() > 1 && newActivitiesList.count() == allActivities.count())) { activityList.clear(); const QByteArray nullUuid = Activities::nullUuid().toUtf8(); m_client.changeProperty(atoms->activities, XCB_ATOM_STRING, 8, nullUuid.length(), nullUuid.constData()); } else { QByteArray joined = joinedActivitiesList.toLatin1(); activityList = newActivitiesList; m_client.changeProperty(atoms->activities, XCB_ATOM_STRING, 8, joined.length(), joined.constData()); } updateActivities(false); #else Q_UNUSED(newActivitiesList) #endif } void X11Client::blockActivityUpdates(bool b) { if (b) { ++m_activityUpdatesBlocked; } else { Q_ASSERT(m_activityUpdatesBlocked); --m_activityUpdatesBlocked; if (!m_activityUpdatesBlocked) updateActivities(m_blockedActivityUpdatesRequireTransients); } } /** * update after activities changed */ void X11Client::updateActivities(bool includeTransients) { if (m_activityUpdatesBlocked) { m_blockedActivityUpdatesRequireTransients |= includeTransients; return; } emit activitiesChanged(this); m_blockedActivityUpdatesRequireTransients = false; // reset FocusChain::self()->update(this, FocusChain::MakeFirst); updateVisibility(); updateWindowRules(Rules::Activity); } /** * Returns the list of activities the client window is on. * if it's on all activities, the list will be empty. * Don't use this, use isOnActivity() and friends (from class Toplevel) */ QStringList X11Client::activities() const { if (sessionActivityOverride) { return QStringList(); } return activityList; } /** * if @p on is true, sets on all activities. * if it's false, sets it to only be on the current activity */ void X11Client::setOnAllActivities(bool on) { #ifdef KWIN_BUILD_ACTIVITIES if (on == isOnAllActivities()) return; if (on) { setOnActivities(QStringList()); } else { setOnActivity(Activities::self()->current(), true); } #else Q_UNUSED(on) #endif } /** * Performs the actual focusing of the window using XSetInputFocus and WM_TAKE_FOCUS */ void X11Client::takeFocus() { if (rules()->checkAcceptFocus(info->input())) m_client.focus(); else demandAttention(false); // window cannot take input, at least withdraw urgency if (info->supportsProtocol(NET::TakeFocusProtocol)) { sendClientMessage(window(), atoms->wm_protocols, atoms->wm_take_focus, 0, 0, 0, XCB_CURRENT_TIME); } workspace()->setShouldGetFocus(this); bool breakShowingDesktop = !keepAbove(); if (breakShowingDesktop) { foreach (const X11Client *c, group()->members()) { if (c->isDesktop()) { breakShowingDesktop = false; break; } } } if (breakShowingDesktop) workspace()->setShowingDesktop(false); } /** * Returns whether the window provides context help or not. If it does, * you should show a help menu item or a help button like '?' and call * contextHelp() if this is invoked. * * \sa contextHelp() */ bool X11Client::providesContextHelp() const { return info->supportsProtocol(NET::ContextHelpProtocol); } /** * Invokes context help on the window. Only works if the window * actually provides context help. * * \sa providesContextHelp() */ void X11Client::showContextHelp() { if (info->supportsProtocol(NET::ContextHelpProtocol)) { sendClientMessage(window(), atoms->wm_protocols, atoms->net_wm_context_help); } } /** * Fetches the window's caption (WM_NAME property). It will be * stored in the client's caption(). */ void X11Client::fetchName() { setCaption(readName()); } static inline QString readNameProperty(xcb_window_t w, xcb_atom_t atom) { const auto cookie = xcb_icccm_get_text_property_unchecked(connection(), w, atom); xcb_icccm_get_text_property_reply_t reply; if (xcb_icccm_get_wm_name_reply(connection(), cookie, &reply, nullptr)) { QString retVal; if (reply.encoding == atoms->utf8_string) { retVal = QString::fromUtf8(QByteArray(reply.name, reply.name_len)); } else if (reply.encoding == XCB_ATOM_STRING) { retVal = QString::fromLocal8Bit(QByteArray(reply.name, reply.name_len)); } xcb_icccm_get_text_property_reply_wipe(&reply); return retVal.simplified(); } return QString(); } QString X11Client::readName() const { if (info->name() && info->name()[0] != '\0') return QString::fromUtf8(info->name()).simplified(); else { return readNameProperty(window(), XCB_ATOM_WM_NAME); } } // The list is taken from https://www.unicode.org/reports/tr9/ (#154840) static const QChar LRM(0x200E); void X11Client::setCaption(const QString& _s, bool force) { QString s(_s); for (int i = 0; i < s.length(); ) { if (!s[i].isPrint()) { if (QChar(s[i]).isHighSurrogate() && i + 1 < s.length() && QChar(s[i + 1]).isLowSurrogate()) { const uint uc = QChar::surrogateToUcs4(s[i], s[i + 1]); if (!QChar::isPrint(uc)) { s.remove(i, 2); } else { i += 2; } continue; } s.remove(i, 1); continue; } ++i; } const bool changed = (s != cap_normal); if (!force && !changed) { return; } cap_normal = s; if (!force && !changed) { emit captionChanged(); return; } bool reset_name = force; bool was_suffix = (!cap_suffix.isEmpty()); cap_suffix.clear(); QString machine_suffix; if (!options->condensedTitle()) { // machine doesn't qualify for "clean" if (clientMachine()->hostName() != ClientMachine::localhost() && !clientMachine()->isLocal()) machine_suffix = QLatin1String(" <@") + QString::fromUtf8(clientMachine()->hostName()) + QLatin1Char('>') + LRM; } QString shortcut_suffix = shortcutCaptionSuffix(); cap_suffix = machine_suffix + shortcut_suffix; if ((!isSpecialWindow() || isToolbar()) && findClientWithSameCaption()) { int i = 2; do { cap_suffix = machine_suffix + QLatin1String(" <") + QString::number(i) + QLatin1Char('>') + LRM; i++; } while (findClientWithSameCaption()); info->setVisibleName(caption().toUtf8().constData()); reset_name = false; } if ((was_suffix && cap_suffix.isEmpty()) || reset_name) { // If it was new window, it may have old value still set, if the window is reused info->setVisibleName(""); info->setVisibleIconName(""); } else if (!cap_suffix.isEmpty() && !cap_iconic.isEmpty()) // Keep the same suffix in iconic name if it's set info->setVisibleIconName(QString(cap_iconic + cap_suffix).toUtf8().constData()); emit captionChanged(); } void X11Client::updateCaption() { setCaption(cap_normal, true); } void X11Client::fetchIconicName() { QString s; if (info->iconName() && info->iconName()[0] != '\0') s = QString::fromUtf8(info->iconName()); else s = readNameProperty(window(), XCB_ATOM_WM_ICON_NAME); if (s != cap_iconic) { bool was_set = !cap_iconic.isEmpty(); cap_iconic = s; if (!cap_suffix.isEmpty()) { if (!cap_iconic.isEmpty()) // Keep the same suffix in iconic name if it's set info->setVisibleIconName(QString(s + cap_suffix).toUtf8().constData()); else if (was_set) info->setVisibleIconName(""); } } } void X11Client::setClientShown(bool shown) { if (deleting) return; // Don't change shown status if this client is being deleted if (shown != hidden) return; // nothing to change hidden = !shown; if (shown) { map(); takeFocus(); autoRaise(); FocusChain::self()->update(this, FocusChain::MakeFirst); } else { unmap(); // Don't move tabs to the end of the list when another tab get's activated FocusChain::self()->update(this, FocusChain::MakeLast); addWorkspaceRepaint(visibleRect()); } } void X11Client::getMotifHints() { const bool wasClosable = m_motif.close(); const bool wasNoBorder = m_motif.noBorder(); if (m_managed) // only on property change, initial read is prefetched m_motif.fetch(); m_motif.read(); if (m_motif.hasDecoration() && m_motif.noBorder() != wasNoBorder) { // If we just got a hint telling us to hide decorations, we do so. if (m_motif.noBorder()) noborder = rules()->checkNoBorder(true); // If the Motif hint is now telling us to show decorations, we only do so if the app didn't // instruct us to hide decorations in some other way, though. else if (!app_noborder) noborder = rules()->checkNoBorder(false); } // mminimize; - Ignore, bogus - E.g. shading or sending to another desktop is "minimizing" too // mmaximize; - Ignore, bogus - Maximizing is basically just resizing const bool closabilityChanged = wasClosable != m_motif.close(); if (isManaged()) updateDecoration(true); // Check if noborder state has changed if (closabilityChanged) { emit closeableChanged(isCloseable()); } } void X11Client::getIcons() { // First read icons from the window itself const QString themedIconName = iconFromDesktopFile(); if (!themedIconName.isEmpty()) { setIcon(QIcon::fromTheme(themedIconName)); return; } QIcon icon; auto readIcon = [this, &icon](int size, bool scale = true) { const QPixmap pix = KWindowSystem::icon(window(), size, size, scale, KWindowSystem::NETWM | KWindowSystem::WMHints, info); if (!pix.isNull()) { icon.addPixmap(pix); } }; readIcon(16); readIcon(32); readIcon(48, false); readIcon(64, false); readIcon(128, false); if (icon.isNull()) { // Then try window group icon = group()->icon(); } if (icon.isNull() && isTransient()) { // Then mainclients auto mainclients = mainClients(); for (auto it = mainclients.constBegin(); it != mainclients.constEnd() && icon.isNull(); ++it) { if (!(*it)->icon().isNull()) { icon = (*it)->icon(); break; } } } if (icon.isNull()) { // And if nothing else, load icon from classhint or xapp icon icon.addPixmap(KWindowSystem::icon(window(), 32, 32, true, KWindowSystem::ClassHint | KWindowSystem::XApp, info)); icon.addPixmap(KWindowSystem::icon(window(), 16, 16, true, KWindowSystem::ClassHint | KWindowSystem::XApp, info)); icon.addPixmap(KWindowSystem::icon(window(), 64, 64, false, KWindowSystem::ClassHint | KWindowSystem::XApp, info)); icon.addPixmap(KWindowSystem::icon(window(), 128, 128, false, KWindowSystem::ClassHint | KWindowSystem::XApp, info)); } setIcon(icon); } void X11Client::getSyncCounter() { // TODO: make sync working on XWayland static const bool isX11 = kwinApp()->operationMode() == Application::OperationModeX11; if (!Xcb::Extensions::self()->isSyncAvailable() || !isX11) return; Xcb::Property syncProp(false, window(), atoms->net_wm_sync_request_counter, XCB_ATOM_CARDINAL, 0, 1); const xcb_sync_counter_t counter = syncProp.value(XCB_NONE); if (counter != XCB_NONE) { m_syncRequest.counter = counter; m_syncRequest.value.hi = 0; m_syncRequest.value.lo = 0; auto *c = connection(); xcb_sync_set_counter(c, m_syncRequest.counter, m_syncRequest.value); if (m_syncRequest.alarm == XCB_NONE) { const uint32_t mask = XCB_SYNC_CA_COUNTER | XCB_SYNC_CA_VALUE_TYPE | XCB_SYNC_CA_TEST_TYPE | XCB_SYNC_CA_EVENTS; const uint32_t values[] = { m_syncRequest.counter, XCB_SYNC_VALUETYPE_RELATIVE, XCB_SYNC_TESTTYPE_POSITIVE_TRANSITION, 1 }; m_syncRequest.alarm = xcb_generate_id(c); auto cookie = xcb_sync_create_alarm_checked(c, m_syncRequest.alarm, mask, values); ScopedCPointer error(xcb_request_check(c, cookie)); if (!error.isNull()) { m_syncRequest.alarm = XCB_NONE; } else { xcb_sync_change_alarm_value_list_t value; memset(&value, 0, sizeof(value)); value.value.hi = 0; value.value.lo = 1; value.delta.hi = 0; value.delta.lo = 1; xcb_sync_change_alarm_aux(c, m_syncRequest.alarm, XCB_SYNC_CA_DELTA | XCB_SYNC_CA_VALUE, &value); } } } } /** * Send the client a _NET_SYNC_REQUEST */ void X11Client::sendSyncRequest() { if (m_syncRequest.counter == XCB_NONE || m_syncRequest.isPending) { return; // do NOT, NEVER send a sync request when there's one on the stack. the clients will just stop respoding. FOREVER! ... } if (!m_syncRequest.failsafeTimeout) { m_syncRequest.failsafeTimeout = new QTimer(this); connect(m_syncRequest.failsafeTimeout, &QTimer::timeout, this, [this]() { // client does not respond to XSYNC requests in reasonable time, remove support if (!ready_for_painting) { // failed on initial pre-show request setReadyForPainting(); setupWindowManagementInterface(); return; } // failed during resize m_syncRequest.isPending = false; m_syncRequest.counter = XCB_NONE; m_syncRequest.alarm = XCB_NONE; delete m_syncRequest.timeout; delete m_syncRequest.failsafeTimeout; m_syncRequest.timeout = nullptr; m_syncRequest.failsafeTimeout = nullptr; m_syncRequest.lastTimestamp = XCB_CURRENT_TIME; } ); m_syncRequest.failsafeTimeout->setSingleShot(true); } // if there's no response within 10 seconds, sth. went wrong and we remove XSYNC support from this client. // see events.cpp X11Client::syncEvent() m_syncRequest.failsafeTimeout->start(ready_for_painting ? 10000 : 1000); // We increment before the notify so that after the notify // syncCounterSerial will equal the value we are expecting // in the acknowledgement const uint32_t oldLo = m_syncRequest.value.lo; m_syncRequest.value.lo++; if (oldLo > m_syncRequest.value.lo) { m_syncRequest.value.hi++; } if (m_syncRequest.lastTimestamp >= xTime()) { updateXTime(); } // Send the message to client sendClientMessage(window(), atoms->wm_protocols, atoms->net_wm_sync_request, m_syncRequest.value.lo, m_syncRequest.value.hi); m_syncRequest.isPending = true; m_syncRequest.lastTimestamp = xTime(); } bool X11Client::wantsInput() const { return rules()->checkAcceptFocus(acceptsFocus() || info->supportsProtocol(NET::TakeFocusProtocol)); } bool X11Client::acceptsFocus() const { return info->input(); } void X11Client::setBlockingCompositing(bool block) { const bool usedToBlock = blocks_compositing; blocks_compositing = rules()->checkBlockCompositing(block && options->windowsBlockCompositing()); if (usedToBlock != blocks_compositing) { emit blockingCompositingChanged(blocks_compositing ? this : nullptr); } } void X11Client::updateAllowedActions(bool force) { if (!isManaged() && !force) return; NET::Actions old_allowed_actions = NET::Actions(allowed_actions); allowed_actions = NET::Actions(); if (isMovable()) allowed_actions |= NET::ActionMove; if (isResizable()) allowed_actions |= NET::ActionResize; if (isMinimizable()) allowed_actions |= NET::ActionMinimize; if (isShadeable()) allowed_actions |= NET::ActionShade; // Sticky state not supported if (isMaximizable()) allowed_actions |= NET::ActionMax; if (userCanSetFullScreen()) allowed_actions |= NET::ActionFullScreen; allowed_actions |= NET::ActionChangeDesktop; // Always (Pagers shouldn't show Docks etc.) if (isCloseable()) allowed_actions |= NET::ActionClose; if (old_allowed_actions == allowed_actions) return; // TODO: This could be delayed and compressed - It's only for pagers etc. anyway info->setAllowedActions(allowed_actions); // ONLY if relevant features have changed (and the window didn't just get/loose moveresize for maximization state changes) const NET::Actions relevant = ~(NET::ActionMove|NET::ActionResize); if ((allowed_actions & relevant) != (old_allowed_actions & relevant)) { if ((allowed_actions & NET::ActionMinimize) != (old_allowed_actions & NET::ActionMinimize)) { emit minimizeableChanged(allowed_actions & NET::ActionMinimize); } if ((allowed_actions & NET::ActionShade) != (old_allowed_actions & NET::ActionShade)) { emit shadeableChanged(allowed_actions & NET::ActionShade); } if ((allowed_actions & NET::ActionMax) != (old_allowed_actions & NET::ActionMax)) { emit maximizeableChanged(allowed_actions & NET::ActionMax); } } } void X11Client::debug(QDebug& stream) const { stream.nospace(); print(stream); } Xcb::StringProperty X11Client::fetchActivities() const { #ifdef KWIN_BUILD_ACTIVITIES return Xcb::StringProperty(window(), atoms->activities); #else return Xcb::StringProperty(); #endif } void X11Client::readActivities(Xcb::StringProperty &property) { #ifdef KWIN_BUILD_ACTIVITIES QStringList newActivitiesList; QString prop = QString::fromUtf8(property); activitiesDefined = !prop.isEmpty(); if (prop == Activities::nullUuid()) { //copied from setOnAllActivities to avoid a redundant XChangeProperty. if (!activityList.isEmpty()) { activityList.clear(); updateActivities(true); } return; } if (prop.isEmpty()) { //note: this makes it *act* like it's on all activities but doesn't set the property to 'ALL' if (!activityList.isEmpty()) { activityList.clear(); updateActivities(true); } return; } newActivitiesList = prop.split(u','); if (newActivitiesList == activityList) return; //expected change, it's ok. //otherwise, somebody else changed it. we need to validate before reacting. //if the activities are not synced, and there are existing clients with //activities specified, somebody has restarted kwin. we can not validate //activities in this case. we need to trust the old values. if (Activities::self() && Activities::self()->serviceStatus() != KActivities::Consumer::Unknown) { QStringList allActivities = Activities::self()->all(); if (allActivities.isEmpty()) { qCDebug(KWIN_CORE) << "no activities!?!?"; //don't touch anything, there's probably something bad going on and we don't wanna make it worse return; } for (int i = 0; i < newActivitiesList.size(); ++i) { if (! allActivities.contains(newActivitiesList.at(i))) { qCDebug(KWIN_CORE) << "invalid:" << newActivitiesList.at(i); newActivitiesList.removeAt(i--); } } } setOnActivities(newActivitiesList); #else Q_UNUSED(property) #endif } void X11Client::checkActivities() { #ifdef KWIN_BUILD_ACTIVITIES Xcb::StringProperty property = fetchActivities(); readActivities(property); #endif } void X11Client::setSessionActivityOverride(bool needed) { sessionActivityOverride = needed; updateActivities(false); } Xcb::Property X11Client::fetchFirstInTabBox() const { return Xcb::Property(false, m_client, atoms->kde_first_in_window_list, atoms->kde_first_in_window_list, 0, 1); } void X11Client::readFirstInTabBox(Xcb::Property &property) { setFirstInTabBox(property.toBool(32, atoms->kde_first_in_window_list)); } void X11Client::updateFirstInTabBox() { // TODO: move into KWindowInfo Xcb::Property property = fetchFirstInTabBox(); readFirstInTabBox(property); } Xcb::StringProperty X11Client::fetchColorScheme() const { return Xcb::StringProperty(m_client, atoms->kde_color_sheme); } void X11Client::readColorScheme(Xcb::StringProperty &property) { AbstractClient::updateColorScheme(rules()->checkDecoColor(QString::fromUtf8(property))); } void X11Client::updateColorScheme() { Xcb::StringProperty property = fetchColorScheme(); readColorScheme(property); } bool X11Client::isClient() const { return true; } NET::WindowType X11Client::windowType(bool direct, int supportedTypes) const { // TODO: does it make sense to cache the returned window type for SUPPORTED_MANAGED_WINDOW_TYPES_MASK? if (supportedTypes == 0) { supportedTypes = SUPPORTED_MANAGED_WINDOW_TYPES_MASK; } NET::WindowType wt = info->windowType(NET::WindowTypes(supportedTypes)); if (direct) { return wt; } NET::WindowType wt2 = rules()->checkType(wt); if (wt != wt2) { wt = wt2; info->setWindowType(wt); // force hint change } // hacks here if (wt == NET::Unknown) // this is more or less suggested in NETWM spec wt = isTransient() ? NET::Dialog : NET::Normal; return wt; } void X11Client::cancelFocusOutTimer() { if (m_focusOutTimer) { m_focusOutTimer->stop(); } } xcb_window_t X11Client::frameId() const { return m_frame; } QRect X11Client::bufferGeometry() const { return m_bufferGeometry; } QMargins X11Client::bufferMargins() const { return QMargins(borderLeft(), borderTop(), borderRight(), borderBottom()); } QPoint X11Client::framePosToClientPos(const QPoint &point) const { int x = point.x(); int y = point.y(); if (isDecorated()) { x += borderLeft(); y += borderTop(); } else { x -= m_clientFrameExtents.left(); y -= m_clientFrameExtents.top(); } return QPoint(x, y); } QPoint X11Client::clientPosToFramePos(const QPoint &point) const { int x = point.x(); int y = point.y(); if (isDecorated()) { x -= borderLeft(); y -= borderTop(); } else { x += m_clientFrameExtents.left(); y += m_clientFrameExtents.top(); } return QPoint(x, y); } QSize X11Client::frameSizeToClientSize(const QSize &size) const { int width = size.width(); int height = size.height(); if (isDecorated()) { width -= borderLeft() + borderRight(); height -= borderTop() + borderBottom(); } else { width += m_clientFrameExtents.left() + m_clientFrameExtents.right(); height += m_clientFrameExtents.top() + m_clientFrameExtents.bottom(); } return QSize(width, height); } QSize X11Client::clientSizeToFrameSize(const QSize &size) const { int width = size.width(); int height = size.height(); if (isDecorated()) { width += borderLeft() + borderRight(); height += borderTop() + borderBottom(); } else { width -= m_clientFrameExtents.left() + m_clientFrameExtents.right(); height -= m_clientFrameExtents.top() + m_clientFrameExtents.bottom(); } return QSize(width, height); } QRect X11Client::frameRectToBufferRect(const QRect &rect) const { if (isDecorated()) { return rect; } return frameRectToClientRect(rect); } Xcb::Property X11Client::fetchShowOnScreenEdge() const { return Xcb::Property(false, window(), atoms->kde_screen_edge_show, XCB_ATOM_CARDINAL, 0, 1); } void X11Client::readShowOnScreenEdge(Xcb::Property &property) { //value comes in two parts, edge in the lower byte //then the type in the upper byte // 0 = autohide // 1 = raise in front on activate const uint32_t value = property.value(ElectricNone); ElectricBorder border = ElectricNone; switch (value & 0xFF) { case 0: border = ElectricTop; break; case 1: border = ElectricRight; break; case 2: border = ElectricBottom; break; case 3: border = ElectricLeft; break; } if (border != ElectricNone) { disconnect(m_edgeRemoveConnection); disconnect(m_edgeGeometryTrackingConnection); bool successfullyHidden = false; if (((value >> 8) & 0xFF) == 1) { setKeepBelow(true); successfullyHidden = keepBelow(); //request could have failed due to user kwin rules m_edgeRemoveConnection = connect(this, &AbstractClient::keepBelowChanged, this, [this](){ if (!keepBelow()) { ScreenEdges::self()->reserve(this, ElectricNone); } }); } else { hideClient(true); successfullyHidden = isHiddenInternal(); - m_edgeGeometryTrackingConnection = connect(this, &X11Client::geometryChanged, this, [this, border](){ + m_edgeGeometryTrackingConnection = connect(this, &X11Client::frameGeometryChanged, this, [this, border](){ hideClient(true); ScreenEdges::self()->reserve(this, border); }); } if (successfullyHidden) { ScreenEdges::self()->reserve(this, border); } else { ScreenEdges::self()->reserve(this, ElectricNone); } } else if (!property.isNull() && property->type != XCB_ATOM_NONE) { // property value is incorrect, delete the property // so that the client knows that it is not hidden xcb_delete_property(connection(), window(), atoms->kde_screen_edge_show); } else { // restore // TODO: add proper unreserve //this will call showOnScreenEdge to reset the state disconnect(m_edgeGeometryTrackingConnection); ScreenEdges::self()->reserve(this, ElectricNone); } } void X11Client::updateShowOnScreenEdge() { Xcb::Property property = fetchShowOnScreenEdge(); readShowOnScreenEdge(property); } void X11Client::showOnScreenEdge() { disconnect(m_edgeRemoveConnection); hideClient(false); setKeepBelow(false); xcb_delete_property(connection(), window(), atoms->kde_screen_edge_show); } void X11Client::addDamage(const QRegion &damage) { if (!ready_for_painting) { // avoid "setReadyForPainting()" function calling overhead if (m_syncRequest.counter == XCB_NONE) { // cannot detect complete redraw, consider done now setReadyForPainting(); setupWindowManagementInterface(); } } repaints_region += damage.translated(bufferGeometry().topLeft() - frameGeometry().topLeft()); Toplevel::addDamage(damage); } bool X11Client::belongsToSameApplication(const AbstractClient *other, SameApplicationChecks checks) const { const X11Client *c2 = dynamic_cast(other); if (!c2) { return false; } return X11Client::belongToSameApplication(this, c2, checks); } QSize X11Client::resizeIncrements() const { return m_geometryHints.resizeIncrements(); } Xcb::StringProperty X11Client::fetchApplicationMenuServiceName() const { return Xcb::StringProperty(m_client, atoms->kde_net_wm_appmenu_service_name); } void X11Client::readApplicationMenuServiceName(Xcb::StringProperty &property) { updateApplicationMenuServiceName(QString::fromUtf8(property)); } void X11Client::checkApplicationMenuServiceName() { Xcb::StringProperty property = fetchApplicationMenuServiceName(); readApplicationMenuServiceName(property); } Xcb::StringProperty X11Client::fetchApplicationMenuObjectPath() const { return Xcb::StringProperty(m_client, atoms->kde_net_wm_appmenu_object_path); } void X11Client::readApplicationMenuObjectPath(Xcb::StringProperty &property) { updateApplicationMenuObjectPath(QString::fromUtf8(property)); } void X11Client::checkApplicationMenuObjectPath() { Xcb::StringProperty property = fetchApplicationMenuObjectPath(); readApplicationMenuObjectPath(property); } void X11Client::handleSync() { setReadyForPainting(); setupWindowManagementInterface(); m_syncRequest.isPending = false; if (m_syncRequest.failsafeTimeout) { m_syncRequest.failsafeTimeout->stop(); } if (isResize()) { if (m_syncRequest.timeout) { m_syncRequest.timeout->stop(); } performMoveResize(); updateWindowPixmap(); } else // setReadyForPainting does as well, but there's a small chance for resize syncs after the resize ended addRepaintFull(); } void X11Client::move(int x, int y, ForceGeometry_t force) { const QPoint framePosition(x, y); m_clientGeometry.moveTopLeft(framePosToClientPos(framePosition)); const QPoint bufferPosition = isDecorated() ? framePosition : m_clientGeometry.topLeft(); // resuming geometry updates is handled only in setGeometry() Q_ASSERT(pendingGeometryUpdate() == PendingGeometryNone || areGeometryUpdatesBlocked()); if (!areGeometryUpdatesBlocked() && framePosition != rules()->checkPosition(framePosition)) { qCDebug(KWIN_CORE) << "forced position fail:" << framePosition << ":" << rules()->checkPosition(framePosition); } m_frameGeometry.moveTopLeft(framePosition); if (force == NormalGeometrySet && m_bufferGeometry.topLeft() == bufferPosition) { return; } m_bufferGeometry.moveTopLeft(bufferPosition); if (areGeometryUpdatesBlocked()) { if (pendingGeometryUpdate() == PendingGeometryForced) { // Maximum, nothing needed. } else if (force == ForceGeometrySet) { setPendingGeometryUpdate(PendingGeometryForced); } else { setPendingGeometryUpdate(PendingGeometryNormal); } return; } updateServerGeometry(); updateWindowRules(Rules::Position); screens()->setCurrent(this); workspace()->updateStackingOrder(); // client itself is not damaged + if (frameGeometryBeforeUpdateBlocking() != frameGeometry()) { + emit frameGeometryChanged(this, frameGeometryBeforeUpdateBlocking()); + } addRepaintDuringGeometryUpdates(); updateGeometryBeforeUpdateBlocking(); - emit geometryChanged(); } bool X11Client::belongToSameApplication(const X11Client *c1, const X11Client *c2, SameApplicationChecks checks) { bool same_app = false; // tests that definitely mean they belong together if (c1 == c2) same_app = true; else if (c1->isTransient() && c2->hasTransient(c1, true)) same_app = true; // c1 has c2 as mainwindow else if (c2->isTransient() && c1->hasTransient(c2, true)) same_app = true; // c2 has c1 as mainwindow else if (c1->group() == c2->group()) same_app = true; // same group else if (c1->wmClientLeader() == c2->wmClientLeader() && c1->wmClientLeader() != c1->window() // if WM_CLIENT_LEADER is not set, it returns window(), && c2->wmClientLeader() != c2->window()) // don't use in this test then same_app = true; // same client leader // tests that mean they most probably don't belong together else if ((c1->pid() != c2->pid() && !checks.testFlag(SameApplicationCheck::AllowCrossProcesses)) || c1->wmClientMachine(false) != c2->wmClientMachine(false)) ; // different processes else if (c1->wmClientLeader() != c2->wmClientLeader() && c1->wmClientLeader() != c1->window() // if WM_CLIENT_LEADER is not set, it returns window(), && c2->wmClientLeader() != c2->window() // don't use in this test then && !checks.testFlag(SameApplicationCheck::AllowCrossProcesses)) ; // different client leader else if (!resourceMatch(c1, c2)) ; // different apps else if (!sameAppWindowRoleMatch(c1, c2, checks.testFlag(SameApplicationCheck::RelaxedForActive)) && !checks.testFlag(SameApplicationCheck::AllowCrossProcesses)) ; // "different" apps else if (c1->pid() == 0 || c2->pid() == 0) ; // old apps that don't have _NET_WM_PID, consider them different // if they weren't found to match above else same_app = true; // looks like it's the same app return same_app; } // Non-transient windows with window role containing '#' are always // considered belonging to different applications (unless // the window role is exactly the same). KMainWindow sets // window role this way by default, and different KMainWindow // usually "are" different application from user's point of view. // This help with no-focus-stealing for e.g. konqy reusing. // On the other hand, if one of the windows is active, they are // considered belonging to the same application. This is for // the cases when opening new mainwindow directly from the application, // e.g. 'Open New Window' in konqy ( active_hack == true ). bool X11Client::sameAppWindowRoleMatch(const X11Client *c1, const X11Client *c2, bool active_hack) { if (c1->isTransient()) { while (const X11Client *t = dynamic_cast(c1->transientFor())) c1 = t; if (c1->groupTransient()) return c1->group() == c2->group(); #if 0 // if a group transient is in its own group, it didn't possibly have a group, // and therefore should be considered belonging to the same app like // all other windows from the same app || c1->group()->leaderClient() == c1 || c2->group()->leaderClient() == c2; #endif } if (c2->isTransient()) { while (const X11Client *t = dynamic_cast(c2->transientFor())) c2 = t; if (c2->groupTransient()) return c1->group() == c2->group(); #if 0 || c1->group()->leaderClient() == c1 || c2->group()->leaderClient() == c2; #endif } int pos1 = c1->windowRole().indexOf('#'); int pos2 = c2->windowRole().indexOf('#'); if ((pos1 >= 0 && pos2 >= 0)) { if (!active_hack) // without the active hack for focus stealing prevention, return c1 == c2; // different mainwindows are always different apps if (!c1->isActive() && !c2->isActive()) return c1 == c2; else return true; } return true; } /* Transiency stuff: ICCCM 4.1.2.6, NETWM 7.3 WM_TRANSIENT_FOR is basically means "this is my mainwindow". For NET::Unknown windows, transient windows are considered to be NET::Dialog windows, for compatibility with non-NETWM clients. KWin may adjust the value of this property in some cases (window pointing to itself or creating a loop, keeping NET::Splash windows above other windows from the same app, etc.). X11Client::transient_for_id is the value of the WM_TRANSIENT_FOR property, after possibly being adjusted by KWin. X11Client::transient_for points to the Client this Client is transient for, or is NULL. If X11Client::transient_for_id is poiting to the root window, the window is considered to be transient for the whole window group, as suggested in NETWM 7.3. In the case of group transient window, X11Client::transient_for is NULL, and X11Client::groupTransient() returns true. Such window is treated as if it were transient for every window in its window group that has been mapped _before_ it (or, to be exact, was added to the same group before it). Otherwise two group transients can create loops, which can lead very very nasty things (bug #67914 and all its dupes). X11Client::original_transient_for_id is the value of the property, which may be different if X11Client::transient_for_id if e.g. forcing NET::Splash to be kept on top of its window group, or when the mainwindow is not mapped yet, in which case the window is temporarily made group transient, and when the mainwindow is mapped, transiency is re-evaluated. This can get a bit complicated with with e.g. two Konqueror windows created by the same process. They should ideally appear like two independent applications to the user. This should be accomplished by all windows in the same process having the same window group (needs to be changed in Qt at the moment), and using non-group transients poiting to their relevant mainwindow for toolwindows etc. KWin should handle both group and non-group transient dialogs well. In other words: - non-transient windows : isTransient() == false - normal transients : transientFor() != NULL - group transients : groupTransient() == true - list of mainwindows : mainClients() (call once and loop over the result) - list of transients : transients() - every window in the group : group()->members() */ Xcb::TransientFor X11Client::fetchTransient() const { return Xcb::TransientFor(window()); } void X11Client::readTransientProperty(Xcb::TransientFor &transientFor) { xcb_window_t new_transient_for_id = XCB_WINDOW_NONE; if (transientFor.getTransientFor(&new_transient_for_id)) { m_originalTransientForId = new_transient_for_id; new_transient_for_id = verifyTransientFor(new_transient_for_id, true); } else { m_originalTransientForId = XCB_WINDOW_NONE; new_transient_for_id = verifyTransientFor(XCB_WINDOW_NONE, false); } setTransient(new_transient_for_id); } void X11Client::readTransient() { Xcb::TransientFor transientFor = fetchTransient(); readTransientProperty(transientFor); } void X11Client::setTransient(xcb_window_t new_transient_for_id) { if (new_transient_for_id != m_transientForId) { removeFromMainClients(); X11Client *transient_for = nullptr; m_transientForId = new_transient_for_id; if (m_transientForId != XCB_WINDOW_NONE && !groupTransient()) { transient_for = workspace()->findClient(Predicate::WindowMatch, m_transientForId); Q_ASSERT(transient_for != nullptr); // verifyTransient() had to check this transient_for->addTransient(this); } // checkGroup() will check 'check_active_modal' setTransientFor(transient_for); checkGroup(nullptr, true); // force, because transiency has changed workspace()->updateClientLayer(this); workspace()->resetUpdateToolWindowsTimer(); emit transientChanged(); } } void X11Client::removeFromMainClients() { if (transientFor()) transientFor()->removeTransient(this); if (groupTransient()) { for (auto it = group()->members().constBegin(); it != group()->members().constEnd(); ++it) (*it)->removeTransient(this); } } // *sigh* this transiency handling is madness :( // This one is called when destroying/releasing a window. // It makes sure this client is removed from all grouping // related lists. void X11Client::cleanGrouping() { // qDebug() << "CLEANGROUPING:" << this; // for ( auto it = group()->members().begin(); // it != group()->members().end(); // ++it ) // qDebug() << "CL:" << *it; // QList mains; // mains = mainClients(); // for ( auto it = mains.begin(); // it != mains.end(); // ++it ) // qDebug() << "MN:" << *it; removeFromMainClients(); // qDebug() << "CLEANGROUPING2:" << this; // for ( auto it = group()->members().begin(); // it != group()->members().end(); // ++it ) // qDebug() << "CL2:" << *it; // mains = mainClients(); // for ( auto it = mains.begin(); // it != mains.end(); // ++it ) // qDebug() << "MN2:" << *it; 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; } // qDebug() << "CLEANGROUPING3:" << this; // for ( auto it = group()->members().begin(); // it != group()->members().end(); // ++it ) // qDebug() << "CL3:" << *it; // mains = mainClients(); // for ( auto it = mains.begin(); // it != mains.end(); // ++it ) // qDebug() << "MN3:" << *it; // HACK // removeFromMainClients() did remove 'this' from transient // lists of all group members, but then made windows that // were transient for 'this' group transient, which again // added 'this' to those transient lists :( QList group_members = group()->members(); group()->removeMember(this); in_group = nullptr; for (auto it = group_members.constBegin(); it != group_members.constEnd(); ++it) (*it)->removeTransient(this); // qDebug() << "CLEANGROUPING4:" << this; // for ( auto it = group_members.begin(); // it != group_members.end(); // ++it ) // qDebug() << "CL4:" << *it; m_transientForId = XCB_WINDOW_NONE; } // Make sure that no group transient is considered transient // for a window that is (directly or indirectly) transient for it // (including another group transients). // Non-group transients not causing loops are checked in verifyTransientFor(). void X11Client::checkGroupTransients() { for (auto it1 = group()->members().constBegin(); it1 != group()->members().constEnd(); ++it1) { if (!(*it1)->groupTransient()) // check all group transients in the group continue; // TODO optimize to check only the changed ones? for (auto it2 = group()->members().constBegin(); it2 != group()->members().constEnd(); ++it2) { // group transients can be transient only for others in the group, // so don't make them transient for the ones that are transient for it if (*it1 == *it2) continue; for (AbstractClient* cl = (*it2)->transientFor(); cl != nullptr; cl = cl->transientFor()) { if (cl == *it1) { // don't use removeTransient(), that would modify *it2 too (*it2)->removeTransientFromList(*it1); continue; } } // if *it1 and *it2 are both group transients, and are transient for each other, // make only *it2 transient for *it1 (i.e. subwindow), as *it2 came later, // and should be therefore on top of *it1 // TODO This could possibly be optimized, it also requires hasTransient() to check for loops. if ((*it2)->groupTransient() && (*it1)->hasTransient(*it2, true) && (*it2)->hasTransient(*it1, true)) (*it2)->removeTransientFromList(*it1); // if there are already windows W1 and W2, W2 being transient for W1, and group transient W3 // is added, make it transient only for W2, not for W1, because it's already indirectly // transient for it - the indirect transiency actually shouldn't break anything, // but it can lead to exponentially expensive operations (#95231) // TODO this is pretty slow as well for (auto it3 = group()->members().constBegin(); it3 != group()->members().constEnd(); ++it3) { if (*it1 == *it2 || *it2 == *it3 || *it1 == *it3) continue; if ((*it2)->hasTransient(*it1, false) && (*it3)->hasTransient(*it1, false)) { if ((*it2)->hasTransient(*it3, true)) (*it2)->removeTransientFromList(*it1); if ((*it3)->hasTransient(*it2, true)) (*it3)->removeTransientFromList(*it1); } } } } } /** * Check that the window is not transient for itself, and similar nonsense. */ xcb_window_t X11Client::verifyTransientFor(xcb_window_t new_transient_for, bool set) { xcb_window_t new_property_value = new_transient_for; // make sure splashscreens are shown above all their app's windows, even though // they're in Normal layer if (isSplash() && new_transient_for == XCB_WINDOW_NONE) new_transient_for = rootWindow(); if (new_transient_for == XCB_WINDOW_NONE) { if (set) // sometimes WM_TRANSIENT_FOR is set to None, instead of root window new_property_value = new_transient_for = rootWindow(); else return XCB_WINDOW_NONE; } if (new_transient_for == window()) { // pointing to self // also fix the property itself qCWarning(KWIN_CORE) << "Client " << this << " has WM_TRANSIENT_FOR poiting to itself." ; new_property_value = new_transient_for = rootWindow(); } // The transient_for window may be embedded in another application, // so kwin cannot see it. Try to find the managed client for the // window and fix the transient_for property if possible. xcb_window_t before_search = new_transient_for; while (new_transient_for != XCB_WINDOW_NONE && new_transient_for != rootWindow() && !workspace()->findClient(Predicate::WindowMatch, new_transient_for)) { Xcb::Tree tree(new_transient_for); if (tree.isNull()) { break; } new_transient_for = tree->parent; } if (X11Client *new_transient_for_client = workspace()->findClient(Predicate::WindowMatch, new_transient_for)) { if (new_transient_for != before_search) { qCDebug(KWIN_CORE) << "Client " << this << " has WM_TRANSIENT_FOR poiting to non-toplevel window " << before_search << ", child of " << new_transient_for_client << ", adjusting."; new_property_value = new_transient_for; // also fix the property } } else new_transient_for = before_search; // nice try // loop detection // group transients cannot cause loops, because they're considered transient only for non-transient // windows in the group int count = 20; xcb_window_t loop_pos = new_transient_for; while (loop_pos != XCB_WINDOW_NONE && loop_pos != rootWindow()) { X11Client *pos = workspace()->findClient(Predicate::WindowMatch, loop_pos); if (pos == nullptr) break; loop_pos = pos->m_transientForId; if (--count == 0 || pos == this) { qCWarning(KWIN_CORE) << "Client " << this << " caused WM_TRANSIENT_FOR loop." ; new_transient_for = rootWindow(); } } if (new_transient_for != rootWindow() && workspace()->findClient(Predicate::WindowMatch, new_transient_for) == nullptr) { // it's transient for a specific window, but that window is not mapped new_transient_for = rootWindow(); } if (new_property_value != m_originalTransientForId) Xcb::setTransientFor(window(), new_property_value); return new_transient_for; } void X11Client::addTransient(AbstractClient* cl) { AbstractClient::addTransient(cl); if (workspace()->mostRecentlyActivatedClient() == this && cl->isModal()) check_active_modal = true; // qDebug() << "ADDTRANS:" << this << ":" << cl; // qDebug() << kBacktrace(); // for ( auto it = transients_list.begin(); // it != transients_list.end(); // ++it ) // qDebug() << "AT:" << (*it); } void X11Client::removeTransient(AbstractClient* cl) { // qDebug() << "REMOVETRANS:" << this << ":" << cl; // qDebug() << kBacktrace(); // cl is transient for this, but this is going away // make cl group transient AbstractClient::removeTransient(cl); if (cl->transientFor() == this) { if (X11Client *c = dynamic_cast(cl)) { c->m_transientForId = XCB_WINDOW_NONE; c->setTransientFor(nullptr); // SELI // SELI cl->setTransient( rootWindow()); c->setTransient(XCB_WINDOW_NONE); } } } // A new window has been mapped. Check if it's not a mainwindow for this already existing window. void X11Client::checkTransient(xcb_window_t w) { if (m_originalTransientForId != w) return; w = verifyTransientFor(w, true); setTransient(w); } // returns true if cl is the transient_for window for this client, // or recursively the transient_for window bool X11Client::hasTransient(const AbstractClient* cl, bool indirect) const { if (const X11Client *c = dynamic_cast(cl)) { // checkGroupTransients() uses this to break loops, so hasTransient() must detect them QList set; return hasTransientInternal(c, indirect, set); } return false; } bool X11Client::hasTransientInternal(const X11Client *cl, bool indirect, QList &set) const { if (const X11Client *t = dynamic_cast(cl->transientFor())) { if (t == this) return true; if (!indirect) return false; if (set.contains(cl)) return false; set.append(cl); return hasTransientInternal(t, indirect, set); } if (!cl->isTransient()) return false; if (group() != cl->group()) return false; // cl is group transient, search from top if (transients().contains(const_cast< X11Client *>(cl))) return true; if (!indirect) return false; if (set.contains(this)) return false; set.append(this); for (auto it = transients().constBegin(); it != transients().constEnd(); ++it) { const X11Client *c = qobject_cast(*it); if (!c) { continue; } if (c->hasTransientInternal(cl, indirect, set)) return true; } return false; } QList X11Client::mainClients() const { if (!isTransient()) return QList(); if (const AbstractClient *t = transientFor()) return QList{const_cast< AbstractClient* >(t)}; QList result; Q_ASSERT(group()); for (auto it = group()->members().constBegin(); it != group()->members().constEnd(); ++it) if ((*it)->hasTransient(this, false)) result.append(*it); return result; } AbstractClient* X11Client::findModal(bool allow_itself) { for (auto it = transients().constBegin(); it != transients().constEnd(); ++it) if (AbstractClient* ret = (*it)->findModal(true)) return ret; if (isModal() && allow_itself) return this; return nullptr; } // X11Client::window_group only holds the contents of the hint, // but it should be used only to find the group, not for anything else // Argument is only when some specific group needs to be set. void X11Client::checkGroup(Group* set_group, bool force) { Group* old_group = in_group; if (old_group != nullptr) old_group->ref(); // turn off automatic deleting if (set_group != nullptr) { if (set_group != in_group) { if (in_group != nullptr) in_group->removeMember(this); in_group = set_group; in_group->addMember(this); } } else if (info->groupLeader() != XCB_WINDOW_NONE) { Group* new_group = workspace()->findGroup(info->groupLeader()); X11Client *t = qobject_cast(transientFor()); if (t != nullptr && t->group() != new_group) { // move the window to the right group (e.g. a dialog provided // by different app, but transient for this one, so make it part of that group) new_group = t->group(); } if (new_group == nullptr) // doesn't exist yet new_group = new Group(info->groupLeader()); if (new_group != in_group) { if (in_group != nullptr) in_group->removeMember(this); in_group = new_group; in_group->addMember(this); } } else { if (X11Client *t = qobject_cast(transientFor())) { // doesn't have window group set, but is transient for something // so make it part of that group Group* new_group = t->group(); if (new_group != in_group) { if (in_group != nullptr) in_group->removeMember(this); in_group = t->group(); in_group->addMember(this); } } else if (groupTransient()) { // group transient which actually doesn't have a group :( // try creating group with other windows with the same client leader Group* new_group = workspace()->findClientLeaderGroup(this); if (new_group == nullptr) new_group = new Group(XCB_WINDOW_NONE); if (new_group != in_group) { if (in_group != nullptr) in_group->removeMember(this); in_group = new_group; in_group->addMember(this); } } else { // Not transient without a group, put it in its client leader group. // This might be stupid if grouping was used for e.g. taskbar grouping // or minimizing together the whole group, but as long as it is used // only for dialogs it's better to keep windows from one app in one group. Group* new_group = workspace()->findClientLeaderGroup(this); if (in_group != nullptr && in_group != new_group) { in_group->removeMember(this); in_group = nullptr; } if (new_group == nullptr) new_group = new Group(XCB_WINDOW_NONE); if (in_group != new_group) { in_group = new_group; in_group->addMember(this); } } } if (in_group != old_group || force) { for (auto it = transients().constBegin(); it != transients().constEnd(); ) { auto *c = *it; // group transients in the old group are no longer transient for it if (c->groupTransient() && c->group() != group()) { removeTransientFromList(c); it = transients().constBegin(); // restart, just in case something more has changed with the list } else ++it; } if (groupTransient()) { // no longer transient for ones in the old group if (old_group != nullptr) { for (auto it = old_group->members().constBegin(); it != old_group->members().constEnd(); ++it) (*it)->removeTransient(this); } // and make transient for all in the new group for (auto it = group()->members().constBegin(); it != group()->members().constEnd(); ++it) { if (*it == this) break; // this means the window is only transient for windows mapped before it (*it)->addTransient(this); } } // group transient splashscreens should be transient even for windows // in group mapped later for (auto it = group()->members().constBegin(); it != group()->members().constEnd(); ++it) { if (!(*it)->isSplash()) continue; if (!(*it)->groupTransient()) continue; if (*it == this || hasTransient(*it, true)) // TODO indirect? continue; addTransient(*it); } } if (old_group != nullptr) old_group->deref(); // can be now deleted if empty checkGroupTransients(); checkActiveModal(); workspace()->updateClientLayer(this); } // used by Workspace::findClientLeaderGroup() void X11Client::changeClientLeaderGroup(Group* gr) { // transientFor() != NULL are in the group of their mainwindow, so keep them there if (transientFor() != nullptr) return; // also don't change the group for window which have group set if (info->groupLeader()) return; checkGroup(gr); // change group } bool X11Client::check_active_modal = false; void X11Client::checkActiveModal() { // if the active window got new modal transient, activate it. // cannot be done in AddTransient(), because there may temporarily // exist loops, breaking findModal X11Client *check_modal = dynamic_cast(workspace()->mostRecentlyActivatedClient()); if (check_modal != nullptr && check_modal->check_active_modal) { X11Client *new_modal = dynamic_cast(check_modal->findModal()); if (new_modal != nullptr && new_modal != check_modal) { if (!new_modal->isManaged()) return; // postpone check until end of manage() workspace()->activateClient(new_modal); } check_modal->check_active_modal = false; } } /** * Calculate the appropriate frame size for the given client size \a * wsize. * * \a wsize is adapted according to the window's size hints (minimum, * maximum and incremental size changes). */ QSize X11Client::sizeForClientSize(const QSize& wsize, SizeMode mode, bool noframe) const { int w = wsize.width(); int h = wsize.height(); if (w < 1 || h < 1) { qCWarning(KWIN_CORE) << "sizeForClientSize() with empty size!" ; } if (w < 1) w = 1; if (h < 1) h = 1; // basesize, minsize, maxsize, paspect and resizeinc have all values defined, // even if they're not set in flags - see getWmNormalHints() QSize min_size = minSize(); QSize max_size = maxSize(); if (isDecorated()) { QSize decominsize(0, 0); QSize border_size(borderLeft() + borderRight(), borderTop() + borderBottom()); if (border_size.width() > decominsize.width()) // just in case decominsize.setWidth(border_size.width()); if (border_size.height() > decominsize.height()) decominsize.setHeight(border_size.height()); if (decominsize.width() > min_size.width()) min_size.setWidth(decominsize.width()); if (decominsize.height() > min_size.height()) min_size.setHeight(decominsize.height()); } w = qMin(max_size.width(), w); h = qMin(max_size.height(), h); w = qMax(min_size.width(), w); h = qMax(min_size.height(), h); int w1 = w; int h1 = h; int width_inc = m_geometryHints.resizeIncrements().width(); int height_inc = m_geometryHints.resizeIncrements().height(); int basew_inc = m_geometryHints.baseSize().width(); int baseh_inc = m_geometryHints.baseSize().height(); if (!m_geometryHints.hasBaseSize()) { basew_inc = m_geometryHints.minSize().width(); baseh_inc = m_geometryHints.minSize().height(); } w = int((w - basew_inc) / width_inc) * width_inc + basew_inc; h = int((h - baseh_inc) / height_inc) * height_inc + baseh_inc; // code for aspect ratios based on code from FVWM /* * The math looks like this: * * minAspectX dwidth maxAspectX * ---------- <= ------- <= ---------- * minAspectY dheight maxAspectY * * If that is multiplied out, then the width and height are * invalid in the following situations: * * minAspectX * dheight > minAspectY * dwidth * maxAspectX * dheight < maxAspectY * dwidth * */ if (m_geometryHints.hasAspect()) { double min_aspect_w = m_geometryHints.minAspect().width(); // use doubles, because the values can be MAX_INT double min_aspect_h = m_geometryHints.minAspect().height(); // and multiplying would go wrong otherwise double max_aspect_w = m_geometryHints.maxAspect().width(); double max_aspect_h = m_geometryHints.maxAspect().height(); // According to ICCCM 4.1.2.3 PMinSize should be a fallback for PBaseSize for size increments, // but not for aspect ratio. Since this code comes from FVWM, handles both at the same time, // and I have no idea how it works, let's hope nobody relies on that. const QSize baseSize = m_geometryHints.baseSize(); w -= baseSize.width(); h -= baseSize.height(); int max_width = max_size.width() - baseSize.width(); int min_width = min_size.width() - baseSize.width(); int max_height = max_size.height() - baseSize.height(); int min_height = min_size.height() - baseSize.height(); #define ASPECT_CHECK_GROW_W \ if ( min_aspect_w * h > min_aspect_h * w ) \ { \ int delta = int( min_aspect_w * h / min_aspect_h - w ) / width_inc * width_inc; \ if ( w + delta <= max_width ) \ w += delta; \ } #define ASPECT_CHECK_SHRINK_H_GROW_W \ if ( min_aspect_w * h > min_aspect_h * w ) \ { \ int delta = int( h - w * min_aspect_h / min_aspect_w ) / height_inc * height_inc; \ if ( h - delta >= min_height ) \ h -= delta; \ else \ { \ int delta = int( min_aspect_w * h / min_aspect_h - w ) / width_inc * width_inc; \ if ( w + delta <= max_width ) \ w += delta; \ } \ } #define ASPECT_CHECK_GROW_H \ if ( max_aspect_w * h < max_aspect_h * w ) \ { \ int delta = int( w * max_aspect_h / max_aspect_w - h ) / height_inc * height_inc; \ if ( h + delta <= max_height ) \ h += delta; \ } #define ASPECT_CHECK_SHRINK_W_GROW_H \ if ( max_aspect_w * h < max_aspect_h * w ) \ { \ int delta = int( w - max_aspect_w * h / max_aspect_h ) / width_inc * width_inc; \ if ( w - delta >= min_width ) \ w -= delta; \ else \ { \ int delta = int( w * max_aspect_h / max_aspect_w - h ) / height_inc * height_inc; \ if ( h + delta <= max_height ) \ h += delta; \ } \ } switch(mode) { case SizeModeAny: #if 0 // make SizeModeAny equal to SizeModeFixedW - prefer keeping fixed width, // so that changing aspect ratio to a different value and back keeps the same size (#87298) { ASPECT_CHECK_SHRINK_H_GROW_W ASPECT_CHECK_SHRINK_W_GROW_H ASPECT_CHECK_GROW_H ASPECT_CHECK_GROW_W break; } #endif case SizeModeFixedW: { // the checks are order so that attempts to modify height are first ASPECT_CHECK_GROW_H ASPECT_CHECK_SHRINK_H_GROW_W ASPECT_CHECK_SHRINK_W_GROW_H ASPECT_CHECK_GROW_W break; } case SizeModeFixedH: { ASPECT_CHECK_GROW_W ASPECT_CHECK_SHRINK_W_GROW_H ASPECT_CHECK_SHRINK_H_GROW_W ASPECT_CHECK_GROW_H break; } case SizeModeMax: { // first checks that try to shrink ASPECT_CHECK_SHRINK_H_GROW_W ASPECT_CHECK_SHRINK_W_GROW_H ASPECT_CHECK_GROW_W ASPECT_CHECK_GROW_H break; } } #undef ASPECT_CHECK_SHRINK_H_GROW_W #undef ASPECT_CHECK_SHRINK_W_GROW_H #undef ASPECT_CHECK_GROW_W #undef ASPECT_CHECK_GROW_H w += baseSize.width(); h += baseSize.height(); } if (!rules()->checkStrictGeometry(!isFullScreen())) { // disobey increments and aspect by explicit rule w = w1; h = h1; } QSize size(w, h); if (!noframe) { size = clientSizeToFrameSize(size); } return rules()->checkSize(size); } /** * Gets the client's normal WM hints and reconfigures itself respectively. */ void X11Client::getWmNormalHints() { const bool hadFixedAspect = m_geometryHints.hasAspect(); // roundtrip to X server m_geometryHints.fetch(); m_geometryHints.read(); if (!hadFixedAspect && m_geometryHints.hasAspect()) { // align to eventual new constraints maximize(max_mode); } if (isManaged()) { // update to match restrictions QSize new_size = adjustedSize(); if (new_size != size() && !isFullScreen()) { QRect origClientGeometry = m_clientGeometry; resizeWithChecks(new_size); if ((!isSpecialWindow() || isToolbar()) && !isFullScreen()) { // try to keep the window in its xinerama screen if possible, // if that fails at least keep it visible somewhere QRect area = workspace()->clientArea(MovementArea, this); if (area.contains(origClientGeometry)) keepInArea(area); area = workspace()->clientArea(WorkArea, this); if (area.contains(origClientGeometry)) keepInArea(area); } } } updateAllowedActions(); // affects isResizeable() } QSize X11Client::minSize() const { return rules()->checkMinSize(m_geometryHints.minSize()); } QSize X11Client::maxSize() const { return rules()->checkMaxSize(m_geometryHints.maxSize()); } QSize X11Client::basicUnit() const { return m_geometryHints.resizeIncrements(); } /** * Auxiliary function to inform the client about the current window * configuration. */ void X11Client::sendSyntheticConfigureNotify() { xcb_configure_notify_event_t c; memset(&c, 0, sizeof(c)); c.response_type = XCB_CONFIGURE_NOTIFY; c.event = window(); c.window = window(); c.x = m_clientGeometry.x(); c.y = m_clientGeometry.y(); c.width = m_clientGeometry.width(); c.height = m_clientGeometry.height(); c.border_width = 0; c.above_sibling = XCB_WINDOW_NONE; c.override_redirect = 0; xcb_send_event(connection(), true, c.event, XCB_EVENT_MASK_STRUCTURE_NOTIFY, reinterpret_cast(&c)); xcb_flush(connection()); } QPoint X11Client::gravityAdjustment(xcb_gravity_t gravity) const { int dx = 0; int dy = 0; // dx, dy specify how the client window moves to make space for the frame. // In general we have to compute the reference point and from that figure // out how much we need to shift the client, however given that we ignore // the border width attribute and the extents of the server-side decoration // are known in advance, we can simplify the math quite a bit and express // the required window gravity adjustment in terms of border sizes. switch(gravity) { case XCB_GRAVITY_NORTH_WEST: // move down right default: dx = borderLeft(); dy = borderTop(); break; case XCB_GRAVITY_NORTH: // move right dx = 0; dy = borderTop(); break; case XCB_GRAVITY_NORTH_EAST: // move down left dx = -borderRight(); dy = borderTop(); break; case XCB_GRAVITY_WEST: // move right dx = borderLeft(); dy = 0; break; case XCB_GRAVITY_CENTER: dx = (borderLeft() - borderRight()) / 2; dy = (borderTop() - borderBottom()) / 2; break; case XCB_GRAVITY_STATIC: // don't move dx = 0; dy = 0; break; case XCB_GRAVITY_EAST: // move left dx = -borderRight(); dy = 0; break; case XCB_GRAVITY_SOUTH_WEST: // move up right dx = borderLeft() ; dy = -borderBottom(); break; case XCB_GRAVITY_SOUTH: // move up dx = 0; dy = -borderBottom(); break; case XCB_GRAVITY_SOUTH_EAST: // move up left dx = -borderRight(); dy = -borderBottom(); break; } return QPoint(dx, dy); } const QPoint X11Client::calculateGravitation(bool invert) const { const QPoint adjustment = gravityAdjustment(m_geometryHints.windowGravity()); // translate from client movement to frame movement const int dx = adjustment.x() - borderLeft(); const int dy = adjustment.y() - borderTop(); if (!invert) return QPoint(x() + dx, y() + dy); else return QPoint(x() - dx, y() - dy); } void X11Client::configureRequest(int value_mask, int rx, int ry, int rw, int rh, int gravity, bool from_tool) { const int configurePositionMask = XCB_CONFIG_WINDOW_X | XCB_CONFIG_WINDOW_Y; const int configureSizeMask = XCB_CONFIG_WINDOW_WIDTH | XCB_CONFIG_WINDOW_HEIGHT; const int configureGeometryMask = configurePositionMask | configureSizeMask; // "maximized" is a user setting -> we do not allow the client to resize itself // away from this & against the users explicit wish qCDebug(KWIN_CORE) << this << bool(value_mask & configureGeometryMask) << bool(maximizeMode() & MaximizeVertical) << bool(maximizeMode() & MaximizeHorizontal); // we want to (partially) ignore the request when the window is somehow maximized or quicktiled bool ignore = !app_noborder && (quickTileMode() != QuickTileMode(QuickTileFlag::None) || maximizeMode() != MaximizeRestore); // however, the user shall be able to force obedience despite and also disobedience in general ignore = rules()->checkIgnoreGeometry(ignore); if (!ignore) { // either we're not max'd / q'tiled or the user allowed the client to break that - so break it. updateQuickTileMode(QuickTileFlag::None); max_mode = MaximizeRestore; emit quickTileModeChanged(); } else if (!app_noborder && quickTileMode() == QuickTileMode(QuickTileFlag::None) && (maximizeMode() == MaximizeVertical || maximizeMode() == MaximizeHorizontal)) { // ignoring can be, because either we do, or the user does explicitly not want it. // for partially maximized windows we want to allow configures in the other dimension. // so we've to ask the user again - to know whether we just ignored for the partial maximization. // the problem here is, that the user can explicitly permit configure requests - even for maximized windows! // we cannot distinguish that from passing "false" for partially maximized windows. ignore = rules()->checkIgnoreGeometry(false); if (!ignore) { // the user is not interested, so we fix up dimensions if (maximizeMode() == MaximizeVertical) value_mask &= ~(XCB_CONFIG_WINDOW_Y | XCB_CONFIG_WINDOW_HEIGHT); if (maximizeMode() == MaximizeHorizontal) value_mask &= ~(XCB_CONFIG_WINDOW_X | XCB_CONFIG_WINDOW_WIDTH); if (!(value_mask & configureGeometryMask)) { ignore = true; // the modification turned the request void } } } if (ignore) { qCDebug(KWIN_CORE) << "DENIED"; return; // nothing to (left) to do for use - bugs #158974, #252314, #321491 } qCDebug(KWIN_CORE) << "PERMITTED" << this << bool(value_mask & configureGeometryMask); if (gravity == 0) // default (nonsense) value for the argument gravity = m_geometryHints.windowGravity(); if (value_mask & configurePositionMask) { QPoint new_pos = framePosToClientPos(pos()); new_pos -= gravityAdjustment(xcb_gravity_t(gravity)); if (value_mask & XCB_CONFIG_WINDOW_X) { new_pos.setX(rx); } if (value_mask & XCB_CONFIG_WINDOW_Y) { new_pos.setY(ry); } // clever(?) workaround for applications like xv that want to set // the location to the current location but miscalculate the // frame size due to kwin being a double-reparenting window // manager if (new_pos.x() == m_clientGeometry.x() && new_pos.y() == m_clientGeometry.y() && gravity == XCB_GRAVITY_NORTH_WEST && !from_tool) { new_pos.setX(x()); new_pos.setY(y()); } new_pos += gravityAdjustment(xcb_gravity_t(gravity)); new_pos = clientPosToFramePos(new_pos); int nw = clientSize().width(); int nh = clientSize().height(); if (value_mask & XCB_CONFIG_WINDOW_WIDTH) { nw = rw; } if (value_mask & XCB_CONFIG_WINDOW_HEIGHT) { nh = rh; } QSize ns = sizeForClientSize(QSize(nw, nh)); // enforces size if needed new_pos = rules()->checkPosition(new_pos); int newScreen = screens()->number(QRect(new_pos, ns).center()); if (newScreen != rules()->checkScreen(newScreen)) return; // not allowed by rule QRect origClientGeometry = m_clientGeometry; GeometryUpdatesBlocker blocker(this); move(new_pos); plainResize(ns); QRect area = workspace()->clientArea(WorkArea, this); if (!from_tool && (!isSpecialWindow() || isToolbar()) && !isFullScreen() && area.contains(origClientGeometry)) keepInArea(area); // this is part of the kicker-xinerama-hack... it should be // safe to remove when kicker gets proper ExtendedStrut support; // see Workspace::updateClientArea() and // X11Client::adjustedClientArea() if (hasStrut()) workspace() -> updateClientArea(); } if (value_mask & configureSizeMask && !(value_mask & configurePositionMask)) { // pure resize int nw = clientSize().width(); int nh = clientSize().height(); if (value_mask & XCB_CONFIG_WINDOW_WIDTH) { nw = rw; } if (value_mask & XCB_CONFIG_WINDOW_HEIGHT) { nh = rh; } QSize ns = sizeForClientSize(QSize(nw, nh)); if (ns != size()) { // don't restore if some app sets its own size again QRect origClientGeometry = m_clientGeometry; GeometryUpdatesBlocker blocker(this); resizeWithChecks(ns, xcb_gravity_t(gravity)); if (!from_tool && (!isSpecialWindow() || isToolbar()) && !isFullScreen()) { // try to keep the window in its xinerama screen if possible, // if that fails at least keep it visible somewhere QRect area = workspace()->clientArea(MovementArea, this); if (area.contains(origClientGeometry)) keepInArea(area); area = workspace()->clientArea(WorkArea, this); if (area.contains(origClientGeometry)) keepInArea(area); } } } geom_restore = frameGeometry(); // No need to send synthetic configure notify event here, either it's sent together // with geometry change, or there's no need to send it. // Handling of the real ConfigureRequest event forces sending it, as there it's necessary. } void X11Client::resizeWithChecks(int w, int h, xcb_gravity_t gravity, ForceGeometry_t force) { Q_ASSERT(!shade_geometry_change); if (isShade()) { if (h == borderTop() + borderBottom()) { qCWarning(KWIN_CORE) << "Shaded geometry passed for size:" ; } } int newx = x(); int newy = y(); 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(); QSize tmp = adjustedSize(QSize(w, h)); // checks size constraints, including min/max size w = tmp.width(); h = tmp.height(); if (gravity == 0) { gravity = m_geometryHints.windowGravity(); } switch(gravity) { case XCB_GRAVITY_NORTH_WEST: // top left corner doesn't move default: break; case XCB_GRAVITY_NORTH: // middle of top border doesn't move newx = (newx + width() / 2) - (w / 2); break; case XCB_GRAVITY_NORTH_EAST: // top right corner doesn't move newx = newx + width() - w; break; case XCB_GRAVITY_WEST: // middle of left border doesn't move newy = (newy + height() / 2) - (h / 2); break; case XCB_GRAVITY_CENTER: // middle point doesn't move newx = (newx + width() / 2) - (w / 2); newy = (newy + height() / 2) - (h / 2); break; case XCB_GRAVITY_STATIC: // top left corner of _client_ window doesn't move // since decoration doesn't change, equal to NorthWestGravity break; case XCB_GRAVITY_EAST: // // middle of right border doesn't move newx = newx + width() - w; newy = (newy + height() / 2) - (h / 2); break; case XCB_GRAVITY_SOUTH_WEST: // bottom left corner doesn't move newy = newy + height() - h; break; case XCB_GRAVITY_SOUTH: // middle of bottom border doesn't move newx = (newx + width() / 2) - (w / 2); newy = newy + height() - h; break; case XCB_GRAVITY_SOUTH_EAST: // bottom right corner doesn't move newx = newx + width() - w; newy = newy + height() - h; break; } setFrameGeometry(newx, newy, w, h, force); } // _NET_MOVERESIZE_WINDOW void X11Client::NETMoveResizeWindow(int flags, int x, int y, int width, int height) { int gravity = flags & 0xff; int value_mask = 0; if (flags & (1 << 8)) { value_mask |= XCB_CONFIG_WINDOW_X; } if (flags & (1 << 9)) { value_mask |= XCB_CONFIG_WINDOW_Y; } if (flags & (1 << 10)) { value_mask |= XCB_CONFIG_WINDOW_WIDTH; } if (flags & (1 << 11)) { value_mask |= XCB_CONFIG_WINDOW_HEIGHT; } configureRequest(value_mask, x, y, width, height, gravity, true); } bool X11Client::isMovable() const { if (!hasNETSupport() && !m_motif.move()) { return false; } if (isFullScreen()) return false; if (isSpecialWindow() && !isSplash() && !isToolbar()) // allow moving of splashscreens :) return false; if (rules()->checkPosition(invalidPoint) != invalidPoint) // forced position return false; return true; } bool X11Client::isMovableAcrossScreens() const { if (!hasNETSupport() && !m_motif.move()) { return false; } if (isSpecialWindow() && !isSplash() && !isToolbar()) // allow moving of splashscreens :) return false; if (rules()->checkPosition(invalidPoint) != invalidPoint) // forced position return false; return true; } bool X11Client::isResizable() const { if (!hasNETSupport() && !m_motif.resize()) { return false; } if (isFullScreen()) return false; if (isSpecialWindow() || isSplash() || isToolbar()) return false; if (rules()->checkSize(QSize()).isValid()) // forced size return false; const Position mode = moveResizePointerMode(); if ((mode == PositionTop || mode == PositionTopLeft || mode == PositionTopRight || mode == PositionLeft || mode == PositionBottomLeft) && rules()->checkPosition(invalidPoint) != invalidPoint) return false; QSize min = minSize(); QSize max = maxSize(); return min.width() < max.width() || min.height() < max.height(); } bool X11Client::isMaximizable() const { if (!isResizable() || isToolbar()) // SELI isToolbar() ? return false; if (rules()->checkMaximize(MaximizeRestore) == MaximizeRestore && rules()->checkMaximize(MaximizeFull) != MaximizeRestore) return true; return false; } /** * Reimplemented to inform the client about the new window position. */ void X11Client::setFrameGeometry(int x, int y, int w, int h, ForceGeometry_t force) { // this code is also duplicated in X11Client::plainResize() // Ok, the shading geometry stuff. Generally, code doesn't care about shaded geometry, // simply because there are too many places dealing with geometry. Those places // ignore shaded state and use normal geometry, which they usually should get // from adjustedSize(). Such geometry comes here, and if the window is shaded, // the geometry is used only for client_size, since that one is not used when // shading. Then the frame geometry is adjusted for the shaded geometry. // This gets more complicated in the case the code does only something like // setGeometry( geometry()) - geometry() will return the shaded frame geometry. // Such code is wrong and should be changed to handle the case when the window is shaded, // for example using X11Client::clientSize() QRect frameGeometry(x, y, w, h); if (shade_geometry_change) ; // nothing else if (isShade()) { if (frameGeometry.height() == borderTop() + borderBottom()) { qCDebug(KWIN_CORE) << "Shaded geometry passed for size:"; } else { m_clientGeometry = frameRectToClientRect(frameGeometry); frameGeometry.setHeight(borderTop() + borderBottom()); } } else { m_clientGeometry = frameRectToClientRect(frameGeometry); } const QRect bufferGeometry = frameRectToBufferRect(frameGeometry); if (!areGeometryUpdatesBlocked() && frameGeometry != rules()->checkGeometry(frameGeometry)) { qCDebug(KWIN_CORE) << "forced geometry fail:" << frameGeometry << ":" << rules()->checkGeometry(frameGeometry); } m_frameGeometry = frameGeometry; if (force == NormalGeometrySet && m_bufferGeometry == bufferGeometry && pendingGeometryUpdate() == PendingGeometryNone) { return; } m_bufferGeometry = bufferGeometry; if (areGeometryUpdatesBlocked()) { if (pendingGeometryUpdate() == PendingGeometryForced) {} // maximum, nothing needed else if (force == ForceGeometrySet) setPendingGeometryUpdate(PendingGeometryForced); else setPendingGeometryUpdate(PendingGeometryNormal); return; } updateServerGeometry(); updateWindowRules(Rules::Position|Rules::Size); // keep track of old maximize mode // to detect changes screens()->setCurrent(this); workspace()->updateStackingOrder(); // Need to regenerate decoration pixmaps when the buffer size is changed. if (bufferGeometryBeforeUpdateBlocking().size() != m_bufferGeometry.size()) { discardWindowPixmap(); } + if (frameGeometryBeforeUpdateBlocking() != m_frameGeometry) { + emit frameGeometryChanged(this, frameGeometryBeforeUpdateBlocking()); + } emit geometryShapeChanged(this, frameGeometryBeforeUpdateBlocking()); addRepaintDuringGeometryUpdates(); updateGeometryBeforeUpdateBlocking(); - // TODO: this signal is emitted too often - emit geometryChanged(); } void X11Client::plainResize(int w, int h, ForceGeometry_t force) { QSize frameSize(w, h); QSize bufferSize; // this code is also duplicated in X11Client::setGeometry(), and it's also commented there if (shade_geometry_change) ; // nothing else if (isShade()) { if (frameSize.height() == borderTop() + borderBottom()) { qCDebug(KWIN_CORE) << "Shaded geometry passed for size:"; } else { m_clientGeometry.setSize(frameSizeToClientSize(frameSize)); frameSize.setHeight(borderTop() + borderBottom()); } } else { m_clientGeometry.setSize(frameSizeToClientSize(frameSize)); } if (isDecorated()) { bufferSize = frameSize; } else { bufferSize = m_clientGeometry.size(); } if (!areGeometryUpdatesBlocked() && frameSize != rules()->checkSize(frameSize)) { qCDebug(KWIN_CORE) << "forced size fail:" << frameSize << ":" << rules()->checkSize(frameSize); } m_frameGeometry.setSize(frameSize); // resuming geometry updates is handled only in setGeometry() Q_ASSERT(pendingGeometryUpdate() == PendingGeometryNone || areGeometryUpdatesBlocked()); if (force == NormalGeometrySet && m_bufferGeometry.size() == bufferSize) { return; } m_bufferGeometry.setSize(bufferSize); if (areGeometryUpdatesBlocked()) { if (pendingGeometryUpdate() == PendingGeometryForced) {} // maximum, nothing needed else if (force == ForceGeometrySet) setPendingGeometryUpdate(PendingGeometryForced); else setPendingGeometryUpdate(PendingGeometryNormal); return; } updateServerGeometry(); updateWindowRules(Rules::Position|Rules::Size); screens()->setCurrent(this); workspace()->updateStackingOrder(); if (bufferGeometryBeforeUpdateBlocking().size() != m_bufferGeometry.size()) { discardWindowPixmap(); } + if (frameGeometryBeforeUpdateBlocking() != frameGeometry()) { + emit frameGeometryChanged(this, frameGeometryBeforeUpdateBlocking()); + } emit geometryShapeChanged(this, frameGeometryBeforeUpdateBlocking()); addRepaintDuringGeometryUpdates(); updateGeometryBeforeUpdateBlocking(); - // TODO: this signal is emitted too often - emit geometryChanged(); } void X11Client::updateServerGeometry() { const QRect oldBufferGeometry = bufferGeometryBeforeUpdateBlocking(); if (oldBufferGeometry.size() != m_bufferGeometry.size() || pendingGeometryUpdate() == PendingGeometryForced) { resizeDecoration(); // If the client is being interactively resized, then the frame window, the wrapper window, // and the client window have correct geometry at this point, so we don't have to configure // them again. If the client doesn't support frame counters, always update geometry. const bool needsGeometryUpdate = !isResize() || m_syncRequest.counter == XCB_NONE; if (needsGeometryUpdate) { m_frame.setGeometry(m_bufferGeometry); } if (!isShade()) { if (needsGeometryUpdate) { m_wrapper.setGeometry(QRect(clientPos(), clientSize())); m_client.resize(clientSize()); } // SELI - won't this be too expensive? // THOMAS - yes, but gtk+ clients will not resize without ... sendSyntheticConfigureNotify(); } updateShape(); } else { if (isMoveResize()) { if (compositing()) { // Defer the X update until we leave this mode needsXWindowMove = true; } else { m_frame.move(m_bufferGeometry.topLeft()); // sendSyntheticConfigureNotify() on finish shall be sufficient } } else { m_frame.move(m_bufferGeometry.topLeft()); sendSyntheticConfigureNotify(); } // Unconditionally move the input window: it won't affect rendering m_decoInputExtent.move(pos() + inputPos()); } } static bool changeMaximizeRecursion = false; void X11Client::changeMaximize(bool horizontal, bool vertical, bool adjust) { if (changeMaximizeRecursion) return; if (!isResizable() || isToolbar()) // SELI isToolbar() ? return; QRect clientArea; if (isElectricBorderMaximizing()) clientArea = workspace()->clientArea(MaximizeArea, Cursor::pos(), desktop()); else clientArea = workspace()->clientArea(MaximizeArea, this); MaximizeMode old_mode = max_mode; // 'adjust == true' means to update the size only, e.g. after changing workspace size if (!adjust) { if (vertical) max_mode = MaximizeMode(max_mode ^ MaximizeVertical); if (horizontal) max_mode = MaximizeMode(max_mode ^ MaximizeHorizontal); } // if the client insist on a fix aspect ratio, we check whether the maximizing will get us // out of screen bounds and take that as a "full maximization with aspect check" then if (m_geometryHints.hasAspect() && // fixed aspect (max_mode == MaximizeVertical || max_mode == MaximizeHorizontal) && // ondimensional maximization rules()->checkStrictGeometry(true)) { // obey aspect const QSize minAspect = m_geometryHints.minAspect(); const QSize maxAspect = m_geometryHints.maxAspect(); if (max_mode == MaximizeVertical || (old_mode & MaximizeVertical)) { const double fx = minAspect.width(); // use doubles, because the values can be MAX_INT const double fy = maxAspect.height(); // use doubles, because the values can be MAX_INT if (fx*clientArea.height()/fy > clientArea.width()) // too big max_mode = old_mode & MaximizeHorizontal ? MaximizeRestore : MaximizeFull; } else { // max_mode == MaximizeHorizontal const double fx = maxAspect.width(); const double fy = minAspect.height(); if (fy*clientArea.width()/fx > clientArea.height()) // too big max_mode = old_mode & MaximizeVertical ? MaximizeRestore : MaximizeFull; } } max_mode = rules()->checkMaximize(max_mode); if (!adjust && max_mode == old_mode) return; GeometryUpdatesBlocker blocker(this); // maximing one way and unmaximizing the other way shouldn't happen, // so restore first and then maximize the other way if ((old_mode == MaximizeVertical && max_mode == MaximizeHorizontal) || (old_mode == MaximizeHorizontal && max_mode == MaximizeVertical)) { changeMaximize(false, false, false); // restore } // save sizes for restoring, if maximalizing QSize sz; if (isShade()) sz = sizeForClientSize(clientSize()); else sz = size(); if (quickTileMode() == QuickTileMode(QuickTileFlag::None)) { if (!adjust && !(old_mode & MaximizeVertical)) { geom_restore.setTop(y()); geom_restore.setHeight(sz.height()); } if (!adjust && !(old_mode & MaximizeHorizontal)) { geom_restore.setLeft(x()); geom_restore.setWidth(sz.width()); } } // call into decoration update borders if (isDecorated() && decoration()->client() && !(options->borderlessMaximizedWindows() && max_mode == KWin::MaximizeFull)) { changeMaximizeRecursion = true; const auto c = decoration()->client().data(); if ((max_mode & MaximizeVertical) != (old_mode & MaximizeVertical)) { emit c->maximizedVerticallyChanged(max_mode & MaximizeVertical); } if ((max_mode & MaximizeHorizontal) != (old_mode & MaximizeHorizontal)) { emit c->maximizedHorizontallyChanged(max_mode & MaximizeHorizontal); } if ((max_mode == MaximizeFull) != (old_mode == MaximizeFull)) { emit c->maximizedChanged(max_mode & 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(app_noborder || (m_motif.hasDecoration() && m_motif.noBorder()) || max_mode == MaximizeFull)); changeMaximizeRecursion = false; } const ForceGeometry_t geom_mode = isDecorated() ? ForceGeometrySet : NormalGeometrySet; // Conditional quick tiling exit points if (quickTileMode() != QuickTileMode(QuickTileFlag::None)) { if (old_mode == MaximizeFull && !clientArea.contains(geom_restore.center())) { // Not restoring on the same screen // TODO: The following doesn't work for some reason //quick_tile_mode = QuickTileFlag::None; // And exit quick tile mode manually } else if ((old_mode == MaximizeVertical && max_mode == MaximizeRestore) || (old_mode == MaximizeFull && max_mode == MaximizeHorizontal)) { // Modifying geometry of a tiled window updateQuickTileMode(QuickTileFlag::None); // Exit quick tile mode without restoring geometry } } switch(max_mode) { case MaximizeVertical: { if (old_mode & MaximizeHorizontal) { // actually restoring from MaximizeFull if (geom_restore.width() == 0 || !clientArea.contains(geom_restore.center())) { // needs placement plainResize(adjustedSize(QSize(width() * 2 / 3, clientArea.height()), SizeModeFixedH), geom_mode); Placement::self()->placeSmart(this, clientArea); } else { setFrameGeometry(QRect(QPoint(geom_restore.x(), clientArea.top()), adjustedSize(QSize(geom_restore.width(), clientArea.height()), SizeModeFixedH)), geom_mode); } } else { QRect r(x(), clientArea.top(), width(), clientArea.height()); r.setTopLeft(rules()->checkPosition(r.topLeft())); r.setSize(adjustedSize(r.size(), SizeModeFixedH)); setFrameGeometry(r, geom_mode); } info->setState(NET::MaxVert, NET::Max); break; } case MaximizeHorizontal: { if (old_mode & MaximizeVertical) { // actually restoring from MaximizeFull if (geom_restore.height() == 0 || !clientArea.contains(geom_restore.center())) { // needs placement plainResize(adjustedSize(QSize(clientArea.width(), height() * 2 / 3), SizeModeFixedW), geom_mode); Placement::self()->placeSmart(this, clientArea); } else { setFrameGeometry(QRect(QPoint(clientArea.left(), geom_restore.y()), adjustedSize(QSize(clientArea.width(), geom_restore.height()), SizeModeFixedW)), geom_mode); } } else { QRect r(clientArea.left(), y(), clientArea.width(), height()); r.setTopLeft(rules()->checkPosition(r.topLeft())); r.setSize(adjustedSize(r.size(), SizeModeFixedW)); setFrameGeometry(r, geom_mode); } info->setState(NET::MaxHoriz, NET::Max); break; } case MaximizeRestore: { QRect restore = frameGeometry(); // when only partially maximized, geom_restore may not have the other dimension remembered if (old_mode & MaximizeVertical) { restore.setTop(geom_restore.top()); restore.setBottom(geom_restore.bottom()); } if (old_mode & MaximizeHorizontal) { restore.setLeft(geom_restore.left()); restore.setRight(geom_restore.right()); } if (!restore.isValid()) { QSize s = QSize(clientArea.width() * 2 / 3, clientArea.height() * 2 / 3); if (geom_restore.width() > 0) s.setWidth(geom_restore.width()); if (geom_restore.height() > 0) s.setHeight(geom_restore.height()); plainResize(adjustedSize(s)); Placement::self()->placeSmart(this, clientArea); restore = frameGeometry(); if (geom_restore.width() > 0) restore.moveLeft(geom_restore.x()); if (geom_restore.height() > 0) restore.moveTop(geom_restore.y()); geom_restore = restore; // relevant for mouse pos calculation, bug #298646 } if (m_geometryHints.hasAspect()) { restore.setSize(adjustedSize(restore.size(), SizeModeAny)); } setFrameGeometry(restore, geom_mode); if (!clientArea.contains(geom_restore.center())) // Not restoring to the same screen Placement::self()->place(this, clientArea); info->setState(NET::States(), NET::Max); updateQuickTileMode(QuickTileFlag::None); break; } case MaximizeFull: { QRect r(clientArea); r.setTopLeft(rules()->checkPosition(r.topLeft())); r.setSize(adjustedSize(r.size(), SizeModeMax)); if (r.size() != clientArea.size()) { // to avoid off-by-one errors... if (isElectricBorderMaximizing() && r.width() < clientArea.width()) { r.moveLeft(qMax(clientArea.left(), Cursor::pos().x() - r.width()/2)); r.moveRight(qMin(clientArea.right(), r.right())); } else { r.moveCenter(clientArea.center()); const bool closeHeight = r.height() > 97*clientArea.height()/100; const bool closeWidth = r.width() > 97*clientArea.width() /100; const bool overHeight = r.height() > clientArea.height(); const bool overWidth = r.width() > clientArea.width(); if (closeWidth || closeHeight) { Position titlePos = titlebarPosition(); const QRect screenArea = workspace()->clientArea(ScreenArea, clientArea.center(), desktop()); if (closeHeight) { bool tryBottom = titlePos == PositionBottom; if ((overHeight && titlePos == PositionTop) || screenArea.top() == clientArea.top()) r.setTop(clientArea.top()); else tryBottom = true; if (tryBottom && (overHeight || screenArea.bottom() == clientArea.bottom())) r.setBottom(clientArea.bottom()); } if (closeWidth) { bool tryLeft = titlePos == PositionLeft; if ((overWidth && titlePos == PositionRight) || screenArea.right() == clientArea.right()) r.setRight(clientArea.right()); else tryLeft = true; if (tryLeft && (overWidth || screenArea.left() == clientArea.left())) r.setLeft(clientArea.left()); } } } r.moveTopLeft(rules()->checkPosition(r.topLeft())); } setFrameGeometry(r, geom_mode); if (options->electricBorderMaximize() && r.top() == clientArea.top()) updateQuickTileMode(QuickTileFlag::Maximize); else updateQuickTileMode(QuickTileFlag::None); info->setState(NET::Max, NET::Max); break; } default: break; } updateAllowedActions(); updateWindowRules(Rules::MaximizeVert|Rules::MaximizeHoriz|Rules::Position|Rules::Size); emit quickTileModeChanged(); } bool X11Client::userCanSetFullScreen() const { if (!isFullScreenable()) { return false; } return isNormalWindow() || isDialog(); } void X11Client::setFullScreen(bool set, bool user) { set = rules()->checkFullScreen(set); const bool wasFullscreen = isFullScreen(); if (wasFullscreen == set) { return; } if (user && !userCanSetFullScreen()) { return; } setShade(ShadeNone); if (wasFullscreen) { workspace()->updateFocusMousePosition(Cursor::pos()); // may cause leave event } else { geom_fs_restore = frameGeometry(); } if (set) { m_fullscreenMode = FullScreenNormal; workspace()->raiseClient(this); } else { m_fullscreenMode = FullScreenNone; } StackingUpdatesBlocker blocker1(workspace()); GeometryUpdatesBlocker blocker2(this); // active fullscreens get different layer workspace()->updateClientLayer(this); info->setState(isFullScreen() ? NET::FullScreen : NET::States(), NET::FullScreen); updateDecoration(false, false); if (set) { if (info->fullscreenMonitors().isSet()) { setFrameGeometry(fullscreenMonitorsArea(info->fullscreenMonitors())); } else { setFrameGeometry(workspace()->clientArea(FullScreenArea, this)); } } else { Q_ASSERT(!geom_fs_restore.isNull()); const int currentScreen = screen(); setFrameGeometry(QRect(geom_fs_restore.topLeft(), adjustedSize(geom_fs_restore.size()))); if(currentScreen != screen()) { workspace()->sendClientToScreen(this, currentScreen); } } updateWindowRules(Rules::Fullscreen | Rules::Position | Rules::Size); emit clientFullScreenSet(this, set, user); emit fullScreenChanged(); } void X11Client::updateFullscreenMonitors(NETFullscreenMonitors topology) { int nscreens = screens()->count(); // qDebug() << "incoming request with top: " << topology.top << " bottom: " << topology.bottom // << " left: " << topology.left << " right: " << topology.right // << ", we have: " << nscreens << " screens."; if (topology.top >= nscreens || topology.bottom >= nscreens || topology.left >= nscreens || topology.right >= nscreens) { qCWarning(KWIN_CORE) << "fullscreenMonitors update failed. request higher than number of screens."; return; } info->setFullscreenMonitors(topology); if (isFullScreen()) setFrameGeometry(fullscreenMonitorsArea(topology)); } /** * Calculates the bounding rectangle defined by the 4 monitor indices indicating the * top, bottom, left, and right edges of the window when the fullscreen state is enabled. */ QRect X11Client::fullscreenMonitorsArea(NETFullscreenMonitors requestedTopology) const { QRect top, bottom, left, right, total; top = screens()->geometry(requestedTopology.top); bottom = screens()->geometry(requestedTopology.bottom); left = screens()->geometry(requestedTopology.left); right = screens()->geometry(requestedTopology.right); total = top.united(bottom.united(left.united(right))); // qDebug() << "top: " << top << " bottom: " << bottom // << " left: " << left << " right: " << right; // qDebug() << "returning rect: " << total; return total; } static GeometryTip* geometryTip = nullptr; void X11Client::positionGeometryTip() { Q_ASSERT(isMove() || isResize()); // Position and Size display if (effects && static_cast(effects)->provides(Effect::GeometryTip)) return; // some effect paints this for us if (options->showGeometryTip()) { if (!geometryTip) { geometryTip = new GeometryTip(&m_geometryHints); } QRect wgeom(moveResizeGeometry()); // position of the frame, size of the window itself wgeom.setWidth(wgeom.width() - (width() - clientSize().width())); wgeom.setHeight(wgeom.height() - (height() - clientSize().height())); if (isShade()) wgeom.setHeight(0); geometryTip->setGeometry(wgeom); if (!geometryTip->isVisible()) geometryTip->show(); geometryTip->raise(); } } bool X11Client::doStartMoveResize() { bool has_grab = false; // This reportedly improves smoothness of the moveresize operation, // something with Enter/LeaveNotify events, looks like XFree performance problem or something *shrug* // (https://lists.kde.org/?t=107302193400001&r=1&w=2) QRect r = workspace()->clientArea(FullArea, this); m_moveResizeGrabWindow.create(r, XCB_WINDOW_CLASS_INPUT_ONLY, 0, nullptr, rootWindow()); m_moveResizeGrabWindow.map(); m_moveResizeGrabWindow.raise(); updateXTime(); const xcb_grab_pointer_cookie_t cookie = xcb_grab_pointer_unchecked(connection(), false, m_moveResizeGrabWindow, XCB_EVENT_MASK_BUTTON_PRESS | XCB_EVENT_MASK_BUTTON_RELEASE | XCB_EVENT_MASK_POINTER_MOTION | XCB_EVENT_MASK_ENTER_WINDOW | XCB_EVENT_MASK_LEAVE_WINDOW, XCB_GRAB_MODE_ASYNC, XCB_GRAB_MODE_ASYNC, m_moveResizeGrabWindow, Cursor::x11Cursor(cursor()), xTime()); ScopedCPointer pointerGrab(xcb_grab_pointer_reply(connection(), cookie, nullptr)); if (!pointerGrab.isNull() && pointerGrab->status == XCB_GRAB_STATUS_SUCCESS) { has_grab = true; } if (!has_grab && grabXKeyboard(frameId())) has_grab = move_resize_has_keyboard_grab = true; if (!has_grab) { // at least one grab is necessary in order to be able to finish move/resize m_moveResizeGrabWindow.reset(); return false; } return true; } void X11Client::leaveMoveResize() { if (needsXWindowMove) { // Do the deferred move m_frame.move(m_bufferGeometry.topLeft()); needsXWindowMove = false; } if (!isResize()) sendSyntheticConfigureNotify(); // tell the client about it's new final position if (geometryTip) { geometryTip->hide(); delete geometryTip; geometryTip = nullptr; } if (move_resize_has_keyboard_grab) ungrabXKeyboard(); move_resize_has_keyboard_grab = false; xcb_ungrab_pointer(connection(), xTime()); m_moveResizeGrabWindow.reset(); if (m_syncRequest.counter == XCB_NONE) { // don't forget to sanitize since the timeout will no more fire m_syncRequest.isPending = false; } delete m_syncRequest.timeout; m_syncRequest.timeout = nullptr; AbstractClient::leaveMoveResize(); } bool X11Client::isWaitingForMoveResizeSync() const { return m_syncRequest.isPending && isResize(); } void X11Client::doResizeSync() { if (!m_syncRequest.timeout) { m_syncRequest.timeout = new QTimer(this); connect(m_syncRequest.timeout, &QTimer::timeout, this, &X11Client::performMoveResize); m_syncRequest.timeout->setSingleShot(true); } if (m_syncRequest.counter != XCB_NONE) { m_syncRequest.timeout->start(250); sendSyncRequest(); } else { // for clients not supporting the XSYNC protocol, we m_syncRequest.isPending = true; // limit the resizes to 30Hz to take pointless load from X11 m_syncRequest.timeout->start(33); // and the client, the mouse is still moved at full speed } // and no human can control faster resizes anyway const QRect moveResizeClientGeometry = frameRectToClientRect(moveResizeGeometry()); const QRect moveResizeBufferGeometry = frameRectToBufferRect(moveResizeGeometry()); // According to the Composite extension spec, a window will get a new pixmap allocated each time // it is mapped or resized. Given that we redirect frame windows and not client windows, we have // to resize the frame window in order to forcefully reallocate offscreen storage. If we don't do // this, then we might render partially updated client window. I know, it sucks. m_frame.setGeometry(moveResizeBufferGeometry); m_wrapper.setGeometry(QRect(clientPos(), moveResizeClientGeometry.size())); m_client.resize(moveResizeClientGeometry.size()); } void X11Client::doPerformMoveResize() { if (m_syncRequest.counter == XCB_NONE) { // client w/o XSYNC support. allow the next resize event m_syncRequest.isPending = false; // NEVER do this for clients with a valid counter } // (leads to sync request races in some clients) } /** * Returns \a area with the client's strut taken into account. * * Used from Workspace in updateClientArea. */ // TODO move to Workspace? QRect X11Client::adjustedClientArea(const QRect &desktopArea, const QRect& area) const { QRect r = area; NETExtendedStrut str = strut(); QRect stareaL = QRect( 0, str . left_start, str . left_width, str . left_end - str . left_start + 1); QRect stareaR = QRect( desktopArea . right() - str . right_width + 1, str . right_start, str . right_width, str . right_end - str . right_start + 1); QRect stareaT = QRect( str . top_start, 0, str . top_end - str . top_start + 1, str . top_width); QRect stareaB = QRect( str . bottom_start, desktopArea . bottom() - str . bottom_width + 1, str . bottom_end - str . bottom_start + 1, str . bottom_width); QRect screenarea = workspace()->clientArea(ScreenArea, this); // HACK: workarea handling is not xinerama aware, so if this strut // reserves place at a xinerama edge that's inside the virtual screen, // ignore the strut for workspace setting. if (area == QRect(QPoint(0, 0), screens()->displaySize())) { if (stareaL.left() < screenarea.left()) stareaL = QRect(); if (stareaR.right() > screenarea.right()) stareaR = QRect(); if (stareaT.top() < screenarea.top()) stareaT = QRect(); if (stareaB.bottom() < screenarea.bottom()) stareaB = QRect(); } // Handle struts at xinerama edges that are inside the virtual screen. // They're given in virtual screen coordinates, make them affect only // their xinerama screen. stareaL.setLeft(qMax(stareaL.left(), screenarea.left())); stareaR.setRight(qMin(stareaR.right(), screenarea.right())); stareaT.setTop(qMax(stareaT.top(), screenarea.top())); stareaB.setBottom(qMin(stareaB.bottom(), screenarea.bottom())); if (stareaL . intersects(area)) { // qDebug() << "Moving left of: " << r << " to " << stareaL.right() + 1; r . setLeft(stareaL . right() + 1); } if (stareaR . intersects(area)) { // qDebug() << "Moving right of: " << r << " to " << stareaR.left() - 1; r . setRight(stareaR . left() - 1); } if (stareaT . intersects(area)) { // qDebug() << "Moving top of: " << r << " to " << stareaT.bottom() + 1; r . setTop(stareaT . bottom() + 1); } if (stareaB . intersects(area)) { // qDebug() << "Moving bottom of: " << r << " to " << stareaB.top() - 1; r . setBottom(stareaB . top() - 1); } return r; } NETExtendedStrut X11Client::strut() const { NETExtendedStrut ext = info->extendedStrut(); NETStrut str = info->strut(); const QSize displaySize = screens()->displaySize(); if (ext.left_width == 0 && ext.right_width == 0 && ext.top_width == 0 && ext.bottom_width == 0 && (str.left != 0 || str.right != 0 || str.top != 0 || str.bottom != 0)) { // build extended from simple if (str.left != 0) { ext.left_width = str.left; ext.left_start = 0; ext.left_end = displaySize.height(); } if (str.right != 0) { ext.right_width = str.right; ext.right_start = 0; ext.right_end = displaySize.height(); } if (str.top != 0) { ext.top_width = str.top; ext.top_start = 0; ext.top_end = displaySize.width(); } if (str.bottom != 0) { ext.bottom_width = str.bottom; ext.bottom_start = 0; ext.bottom_end = displaySize.width(); } } return ext; } StrutRect X11Client::strutRect(StrutArea area) const { Q_ASSERT(area != StrutAreaAll); // Not valid const QSize displaySize = screens()->displaySize(); NETExtendedStrut strutArea = strut(); switch(area) { case StrutAreaTop: if (strutArea.top_width != 0) return StrutRect(QRect( strutArea.top_start, 0, strutArea.top_end - strutArea.top_start, strutArea.top_width ), StrutAreaTop); break; case StrutAreaRight: if (strutArea.right_width != 0) return StrutRect(QRect( displaySize.width() - strutArea.right_width, strutArea.right_start, strutArea.right_width, strutArea.right_end - strutArea.right_start ), StrutAreaRight); break; case StrutAreaBottom: if (strutArea.bottom_width != 0) return StrutRect(QRect( strutArea.bottom_start, displaySize.height() - strutArea.bottom_width, strutArea.bottom_end - strutArea.bottom_start, strutArea.bottom_width ), StrutAreaBottom); break; case StrutAreaLeft: if (strutArea.left_width != 0) return StrutRect(QRect( 0, strutArea.left_start, strutArea.left_width, strutArea.left_end - strutArea.left_start ), StrutAreaLeft); break; default: abort(); // Not valid } return StrutRect(); // Null rect } StrutRects X11Client::strutRects() const { StrutRects region; region += strutRect(StrutAreaTop); region += strutRect(StrutAreaRight); region += strutRect(StrutAreaBottom); region += strutRect(StrutAreaLeft); return region; } bool X11Client::hasStrut() const { NETExtendedStrut ext = strut(); if (ext.left_width == 0 && ext.right_width == 0 && ext.top_width == 0 && ext.bottom_width == 0) return false; return true; } bool X11Client::hasOffscreenXineramaStrut() const { // Get strut as a QRegion QRegion region; region += strutRect(StrutAreaTop); region += strutRect(StrutAreaRight); region += strutRect(StrutAreaBottom); region += strutRect(StrutAreaLeft); // Remove all visible areas so that only the invisible remain for (int i = 0; i < screens()->count(); i ++) region -= screens()->geometry(i); // If there's anything left then we have an offscreen strut return !region.isEmpty(); } void X11Client::applyWindowRules() { AbstractClient::applyWindowRules(); setBlockingCompositing(info->isBlockingCompositing()); } void X11Client::damageNotifyEvent() { if (m_syncRequest.isPending && isResize()) { emit damaged(this, QRect()); m_isDamaged = true; return; } if (!readyForPainting()) { // avoid "setReadyForPainting()" function calling overhead if (m_syncRequest.counter == XCB_NONE) { // cannot detect complete redraw, consider done now setReadyForPainting(); setupWindowManagementInterface(); } } AbstractClient::damageNotifyEvent(); } void X11Client::updateWindowPixmap() { if (effectWindow() && effectWindow()->sceneWindow()) { effectWindow()->sceneWindow()->updatePixmap(); } } } // namespace diff --git a/xdgshellclient.cpp b/xdgshellclient.cpp index f7a406813..07992de3d 100644 --- a/xdgshellclient.cpp +++ b/xdgshellclient.cpp @@ -1,2068 +1,2069 @@ /******************************************************************** KWin - the KDE window manager This file is part of the KDE project. Copyright (C) 2015 Martin Gräßlin Copyright (C) 2018 David Edmundson Copyright (C) 2019 Vlad Zahorodnii This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . *********************************************************************/ #include "xdgshellclient.h" #include "cursor.h" #include "decorations/decoratedclient.h" #include "decorations/decorationbridge.h" #include "deleted.h" #include "placement.h" #include "screenedge.h" #include "screens.h" #ifdef KWIN_BUILD_TABBOX #include "tabbox.h" #endif #include "virtualdesktops.h" #include "wayland_server.h" #include "workspace.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include Q_DECLARE_METATYPE(NET::WindowType) using namespace KWayland::Server; namespace KWin { XdgShellClient::XdgShellClient(XdgShellSurfaceInterface *surface) : AbstractClient() , m_xdgShellToplevel(surface) , m_xdgShellPopup(nullptr) { setSurface(surface->surface()); init(); } XdgShellClient::XdgShellClient(XdgShellPopupInterface *surface) : AbstractClient() , m_xdgShellToplevel(nullptr) , m_xdgShellPopup(surface) { setSurface(surface->surface()); init(); } XdgShellClient::~XdgShellClient() = default; void XdgShellClient::init() { m_requestGeometryBlockCounter++; connect(this, &XdgShellClient::desktopFileNameChanged, this, &XdgShellClient::updateIcon); createWindowId(); setupCompositing(); updateIcon(); // TODO: Initialize with null rect. m_frameGeometry = QRect(0, 0, -1, -1); m_windowGeometry = QRect(0, 0, -1, -1); if (waylandServer()->inputMethodConnection() == surface()->client()) { m_windowType = NET::OnScreenDisplay; } connect(surface(), &SurfaceInterface::unmapped, this, &XdgShellClient::unmap); connect(surface(), &SurfaceInterface::unbound, this, &XdgShellClient::destroyClient); connect(surface(), &SurfaceInterface::destroyed, this, &XdgShellClient::destroyClient); if (m_xdgShellToplevel) { connect(m_xdgShellToplevel, &XdgShellSurfaceInterface::destroyed, this, &XdgShellClient::destroyClient); connect(m_xdgShellToplevel, &XdgShellSurfaceInterface::configureAcknowledged, this, &XdgShellClient::handleConfigureAcknowledged); m_caption = m_xdgShellToplevel->title().simplified(); connect(m_xdgShellToplevel, &XdgShellSurfaceInterface::titleChanged, this, &XdgShellClient::handleWindowTitleChanged); QTimer::singleShot(0, this, &XdgShellClient::updateCaption); connect(m_xdgShellToplevel, &XdgShellSurfaceInterface::moveRequested, this, &XdgShellClient::handleMoveRequested); connect(m_xdgShellToplevel, &XdgShellSurfaceInterface::resizeRequested, this, &XdgShellClient::handleResizeRequested); // Determine the resource name, this is inspired from ICCCM 4.1.2.5 // the binary name of the invoked client. QFileInfo info{m_xdgShellToplevel->client()->executablePath()}; QByteArray resourceName; if (info.exists()) { resourceName = info.fileName().toUtf8(); } setResourceClass(resourceName, m_xdgShellToplevel->windowClass()); setDesktopFileName(m_xdgShellToplevel->windowClass()); connect(m_xdgShellToplevel, &XdgShellSurfaceInterface::windowClassChanged, this, &XdgShellClient::handleWindowClassChanged); connect(m_xdgShellToplevel, &XdgShellSurfaceInterface::minimizeRequested, this, &XdgShellClient::handleMinimizeRequested); connect(m_xdgShellToplevel, &XdgShellSurfaceInterface::maximizedChanged, this, &XdgShellClient::handleMaximizeRequested); connect(m_xdgShellToplevel, &XdgShellSurfaceInterface::fullscreenChanged, this, &XdgShellClient::handleFullScreenRequested); connect(m_xdgShellToplevel, &XdgShellSurfaceInterface::windowMenuRequested, this, &XdgShellClient::handleWindowMenuRequested); connect(m_xdgShellToplevel, &XdgShellSurfaceInterface::transientForChanged, this, &XdgShellClient::handleTransientForChanged); connect(m_xdgShellToplevel, &XdgShellSurfaceInterface::windowGeometryChanged, this, &XdgShellClient::handleWindowGeometryChanged); auto global = static_cast(m_xdgShellToplevel->global()); connect(global, &XdgShellInterface::pingDelayed, this, &XdgShellClient::handlePingDelayed); connect(global, &XdgShellInterface::pingTimeout, this, &XdgShellClient::handlePingTimeout); connect(global, &XdgShellInterface::pongReceived, this, &XdgShellClient::handlePongReceived); auto configure = [this] { if (m_closing) { return; } if (m_requestGeometryBlockCounter != 0 || areGeometryUpdatesBlocked()) { return; } m_xdgShellToplevel->configure(xdgSurfaceStates(), m_requestedClientSize); }; connect(this, &AbstractClient::activeChanged, this, configure); connect(this, &AbstractClient::clientStartUserMovedResized, this, configure); connect(this, &AbstractClient::clientFinishUserMovedResized, this, configure); - connect(this, &XdgShellClient::geometryChanged, this, &XdgShellClient::updateClientOutputs); + connect(this, &XdgShellClient::frameGeometryChanged, this, &XdgShellClient::updateClientOutputs); connect(screens(), &Screens::changed, this, &XdgShellClient::updateClientOutputs); } else if (m_xdgShellPopup) { connect(m_xdgShellPopup, &XdgShellPopupInterface::configureAcknowledged, this, &XdgShellClient::handleConfigureAcknowledged); connect(m_xdgShellPopup, &XdgShellPopupInterface::grabRequested, this, &XdgShellClient::handleGrabRequested); connect(m_xdgShellPopup, &XdgShellPopupInterface::destroyed, this, &XdgShellClient::destroyClient); connect(m_xdgShellPopup, &XdgShellPopupInterface::windowGeometryChanged, this, &XdgShellClient::handleWindowGeometryChanged); } // set initial desktop setDesktop(VirtualDesktopManager::self()->current()); // setup shadow integration updateShadow(); connect(surface(), &SurfaceInterface::shadowChanged, this, &Toplevel::updateShadow); connect(waylandServer(), &WaylandServer::foreignTransientChanged, this, [this](KWayland::Server::SurfaceInterface *child) { if (child == surface()) { handleTransientForChanged(); } }); handleTransientForChanged(); AbstractClient::updateColorScheme(QString()); connect(surface(), &SurfaceInterface::committed, this, &XdgShellClient::finishInit); } void XdgShellClient::finishInit() { disconnect(surface(), &SurfaceInterface::committed, this, &XdgShellClient::finishInit); connect(surface(), &SurfaceInterface::committed, this, &XdgShellClient::handleCommitted); bool needsPlacement = !isInitialPositionSet(); if (supportsWindowRules()) { setupWindowRules(false); const QRect originalGeometry = QRect(pos(), sizeForClientSize(clientSize())); const QRect ruledGeometry = rules()->checkGeometry(originalGeometry, true); if (originalGeometry != ruledGeometry) { setFrameGeometry(ruledGeometry); } maximize(rules()->checkMaximize(maximizeMode(), true)); setDesktop(rules()->checkDesktop(desktop(), true)); setDesktopFileName(rules()->checkDesktopFile(desktopFileName(), true).toUtf8()); if (rules()->checkMinimize(isMinimized(), true)) { minimize(true); // No animation. } setSkipTaskbar(rules()->checkSkipTaskbar(skipTaskbar(), true)); setSkipPager(rules()->checkSkipPager(skipPager(), true)); setSkipSwitcher(rules()->checkSkipSwitcher(skipSwitcher(), true)); setKeepAbove(rules()->checkKeepAbove(keepAbove(), true)); setKeepBelow(rules()->checkKeepBelow(keepBelow(), true)); setShortcut(rules()->checkShortcut(shortcut().toString(), true)); updateColorScheme(); // Don't place the client if its position is set by a rule. if (rules()->checkPosition(invalidPoint, true) != invalidPoint) { needsPlacement = false; } // Don't place the client if the maximize state is set by a rule. if (requestedMaximizeMode() != MaximizeRestore) { needsPlacement = false; } discardTemporaryRules(); RuleBook::self()->discardUsed(this, false); // Remove Apply Now rules. updateWindowRules(Rules::All); } if (isFullScreen()) { needsPlacement = false; } if (needsPlacement) { const QRect area = workspace()->clientArea(PlacementArea, Screens::self()->current(), desktop()); placeIn(area); } m_requestGeometryBlockCounter--; if (m_requestGeometryBlockCounter == 0) { requestGeometry(m_blockedRequestGeometry); } m_isInitialized = true; } void XdgShellClient::destroyClient() { m_closing = true; #ifdef KWIN_BUILD_TABBOX TabBox::TabBox *tabBox = TabBox::TabBox::self(); if (tabBox->isDisplayed() && tabBox->currentClient() == this) { tabBox->nextPrev(true); } #endif if (isMoveResize()) { leaveMoveResize(); } // Replace ShellClient with an instance of Deleted in the stacking order. Deleted *deleted = Deleted::create(this); emit windowClosed(this, deleted); // Remove Force Temporarily rules. RuleBook::self()->discardUsed(this, true); destroyWindowManagementInterface(); destroyDecoration(); 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); deleted->unrefWindow(); m_xdgShellToplevel = nullptr; m_xdgShellPopup = nullptr; deleteClient(this); } void XdgShellClient::deleteClient(XdgShellClient *c) { delete c; } QRect XdgShellClient::inputGeometry() const { if (isDecorated()) { return AbstractClient::inputGeometry(); } // TODO: What about sub-surfaces sticking outside the main surface? return m_bufferGeometry; } QRect XdgShellClient::bufferGeometry() const { return m_bufferGeometry; } QStringList XdgShellClient::activities() const { // TODO: implement return QStringList(); } QPoint XdgShellClient::clientContentPos() const { return -1 * clientPos(); } static QRect subSurfaceTreeRect(const SurfaceInterface *surface, const QPoint &position = QPoint()) { QRect rect(position, surface->size()); const QList> subSurfaces = surface->childSubSurfaces(); for (const QPointer &subSurface : subSurfaces) { if (Q_UNLIKELY(!subSurface)) { continue; } const SurfaceInterface *child = subSurface->surface(); if (Q_UNLIKELY(!child)) { continue; } rect |= subSurfaceTreeRect(child, position + subSurface->position()); } return rect; } QSize XdgShellClient::clientSize() const { const QRect boundingRect = subSurfaceTreeRect(surface()); return m_windowGeometry.size().boundedTo(boundingRect.size()); } void XdgShellClient::debug(QDebug &stream) const { stream.nospace(); stream << "\'XdgShellClient:" << surface() << ";WMCLASS:" << resourceClass() << ":" << resourceName() << ";Caption:" << caption() << "\'"; } bool XdgShellClient::belongsToDesktop() const { const auto clients = waylandServer()->clients(); return std::any_of(clients.constBegin(), clients.constEnd(), [this](const XdgShellClient *client) { if (belongsToSameApplication(client, SameApplicationChecks())) { return client->isDesktop(); } return false; } ); } Layer XdgShellClient::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 XdgShellClient::transparentRect() const { // TODO: implement return QRect(); } NET::WindowType XdgShellClient::windowType(bool direct, int supported_types) const { // TODO: implement Q_UNUSED(direct) Q_UNUSED(supported_types) return m_windowType; } double XdgShellClient::opacity() const { return m_opacity; } void XdgShellClient::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 XdgShellClient::addDamage(const QRegion &damage) { const int offsetX = m_bufferGeometry.x() - frameGeometry().x(); const int offsetY = m_bufferGeometry.y() - frameGeometry().y(); repaints_region += damage.translated(offsetX, offsetY); Toplevel::addDamage(damage); } void XdgShellClient::markAsMapped() { if (!m_unmapped) { return; } m_unmapped = false; if (!ready_for_painting) { setReadyForPainting(); } else { addRepaintFull(); emit windowShown(this); } if (shouldExposeToWindowManagement()) { setupWindowManagementInterface(); } updateShowOnScreenEdge(); } void XdgShellClient::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::updateShadow); connect(decoration, &KDecoration2::Decoration::bordersChanged, this, [this]() { GeometryUpdatesBlocker blocker(this); RequestGeometryBlocker requestBlocker(this); const QRect oldGeometry = frameGeometry(); if (!isShade()) { checkWorkspacePosition(oldGeometry); } emit geometryShapeChanged(this, oldGeometry); } ); } setDecoration(decoration); // TODO: ensure the new geometry still fits into the client area (e.g. maximized windows) doSetGeometry(QRect(oldGeom.topLeft(), m_windowGeometry.size() + QSize(borderLeft() + borderRight(), borderBottom() + borderTop()))); emit geometryShapeChanged(this, oldGeom); } void XdgShellClient::updateDecoration(bool check_workspace_pos, bool force) { if (!force && ((!isDecorated() && noBorder()) || (isDecorated() && !noBorder()))) return; QRect oldgeom = frameGeometry(); QRect oldClientGeom = oldgeom.adjusted(borderLeft(), borderTop(), -borderRight(), -borderBottom()); blockGeometryUpdates(true); if (force) destroyDecoration(); if (!noBorder()) { createDecoration(oldgeom); } else destroyDecoration(); if (m_serverDecoration && isDecorated()) { m_serverDecoration->setMode(KWayland::Server::ServerSideDecorationManagerInterface::Mode::Server); } if (m_xdgDecoration) { auto mode = isDecorated() || m_userNoBorder ? XdgDecorationInterface::Mode::ServerSide: XdgDecorationInterface::Mode::ClientSide; m_xdgDecoration->configure(mode); if (m_requestGeometryBlockCounter == 0) { m_xdgShellToplevel->configure(xdgSurfaceStates(), m_requestedClientSize); } } updateShadow(); if (check_workspace_pos) checkWorkspacePosition(oldgeom, -2, oldClientGeom); blockGeometryUpdates(false); } void XdgShellClient::setFrameGeometry(int x, int y, int w, int h, ForceGeometry_t force) { const QRect newGeometry = rules()->checkGeometry(QRect(x, y, w, h)); if (areGeometryUpdatesBlocked()) { // when the GeometryUpdateBlocker exits the current geom is passed to setGeometry // thus we need to set it here. m_frameGeometry = newGeometry; if (pendingGeometryUpdate() == PendingGeometryForced) { // maximum, nothing needed } else if (force == ForceGeometrySet) { setPendingGeometryUpdate(PendingGeometryForced); } else { setPendingGeometryUpdate(PendingGeometryNormal); } return; } if (pendingGeometryUpdate() != PendingGeometryNone) { // reset geometry to the one before blocking, so that we can compare properly m_frameGeometry = frameGeometryBeforeUpdateBlocking(); } const QSize requestedClientSize = newGeometry.size() - QSize(borderLeft() + borderRight(), borderTop() + borderBottom()); if (requestedClientSize == m_windowGeometry.size() && (m_requestedClientSize.isEmpty() || requestedClientSize == m_requestedClientSize)) { // size didn't change, and we don't need to explicitly request a new size doSetGeometry(newGeometry); updateMaximizeMode(m_requestedMaximizeMode); } else { // size did change, Client needs to provide a new buffer requestGeometry(newGeometry); } } QRect XdgShellClient::determineBufferGeometry() const { // Offset of the main surface relative to the frame rect. const int offsetX = borderLeft() - m_windowGeometry.left(); const int offsetY = borderTop() - m_windowGeometry.top(); QRect bufferGeometry; bufferGeometry.setX(x() + offsetX); bufferGeometry.setY(y() + offsetY); bufferGeometry.setSize(surface()->size()); return bufferGeometry; } void XdgShellClient::doSetGeometry(const QRect &rect) { bool frameGeometryIsChanged = false; bool bufferGeometryIsChanged = false; if (m_frameGeometry != rect) { m_frameGeometry = rect; frameGeometryIsChanged = true; } const QRect bufferGeometry = determineBufferGeometry(); if (m_bufferGeometry != bufferGeometry) { m_bufferGeometry = bufferGeometry; bufferGeometryIsChanged = true; } if (!frameGeometryIsChanged && !bufferGeometryIsChanged) { return; } if (m_unmapped && m_geomMaximizeRestore.isEmpty() && !m_frameGeometry.isEmpty()) { // use first valid geometry as restore geometry m_geomMaximizeRestore = m_frameGeometry; } if (frameGeometryIsChanged) { if (hasStrut()) { workspace()->updateClientArea(); } updateWindowRules(Rules::Position | Rules::Size); + emit frameGeometryChanged(this, frameGeometryBeforeUpdateBlocking()); } - const auto old = frameGeometryBeforeUpdateBlocking(); + emit geometryShapeChanged(this, frameGeometryBeforeUpdateBlocking()); + addRepaintDuringGeometryUpdates(); updateGeometryBeforeUpdateBlocking(); - emit geometryShapeChanged(this, old); if (isResize()) { performMoveResize(); } } void XdgShellClient::doMove(int x, int y) { Q_UNUSED(x) Q_UNUSED(y) m_bufferGeometry = determineBufferGeometry(); } QByteArray XdgShellClient::windowRole() const { return QByteArray(); } bool XdgShellClient::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 XdgShellClient::blockActivityUpdates(bool b) { Q_UNUSED(b) } QString XdgShellClient::captionNormal() const { return m_caption; } QString XdgShellClient::captionSuffix() const { return m_captionSuffix; } void XdgShellClient::updateCaption() { const QString oldSuffix = m_captionSuffix; const auto shortcut = shortcutCaptionSuffix(); m_captionSuffix = shortcut; if ((!isSpecialWindow() || isToolbar()) && findClientWithSameCaption()) { int i = 2; do { m_captionSuffix = shortcut + QLatin1String(" <") + QString::number(i) + QLatin1Char('>'); i++; } while (findClientWithSameCaption()); } if (m_captionSuffix != oldSuffix) { emit captionChanged(); } } void XdgShellClient::closeWindow() { if (m_xdgShellToplevel && isCloseable()) { m_xdgShellToplevel->close(); ping(PingReason::CloseWindow); } } AbstractClient *XdgShellClient::findModal(bool allow_itself) { Q_UNUSED(allow_itself) return nullptr; } bool XdgShellClient::isCloseable() const { if (m_windowType == NET::Desktop || m_windowType == NET::Dock) { return false; } if (m_xdgShellToplevel) { return true; } return false; } bool XdgShellClient::isFullScreen() const { return m_fullScreen; } bool XdgShellClient::isMaximizable() const { if (!isResizable()) { return false; } if (rules()->checkMaximize(MaximizeRestore) != MaximizeRestore || rules()->checkMaximize(MaximizeFull) != MaximizeFull) { return false; } return true; } bool XdgShellClient::isMinimizable() const { if (!rules()->checkMinimize(true)) { return false; } return (!m_plasmaShellSurface || m_plasmaShellSurface->role() == PlasmaShellSurfaceInterface::Role::Normal); } bool XdgShellClient::isMovable() const { if (isFullScreen()) { return false; } if (rules()->checkPosition(invalidPoint) != invalidPoint) { return false; } if (m_plasmaShellSurface) { return m_plasmaShellSurface->role() == PlasmaShellSurfaceInterface::Role::Normal; } if (m_xdgShellPopup) { return false; } return true; } bool XdgShellClient::isMovableAcrossScreens() const { if (rules()->checkPosition(invalidPoint) != invalidPoint) { return false; } if (m_plasmaShellSurface) { return m_plasmaShellSurface->role() == PlasmaShellSurfaceInterface::Role::Normal; } if (m_xdgShellPopup) { return false; } return true; } bool XdgShellClient::isResizable() const { if (isFullScreen()) { return false; } if (rules()->checkSize(QSize()).isValid()) { return false; } if (m_plasmaShellSurface) { return m_plasmaShellSurface->role() == PlasmaShellSurfaceInterface::Role::Normal; } if (m_xdgShellPopup) { return false; } return true; } bool XdgShellClient::isShown(bool shaded_is_shown) const { Q_UNUSED(shaded_is_shown) return !m_closing && !m_unmapped && !isMinimized() && !m_hidden; } bool XdgShellClient::isHiddenInternal() const { return m_unmapped || m_hidden; } void XdgShellClient::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 XdgShellClient::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 = frameGeometry(); // 'adjust == true' means to update the size only, e.g. after changing workspace size if (!adjust) { if (vertical) m_requestedMaximizeMode = MaximizeMode(m_requestedMaximizeMode ^ MaximizeVertical); if (horizontal) m_requestedMaximizeMode = MaximizeMode(m_requestedMaximizeMode ^ MaximizeHorizontal); } m_requestedMaximizeMode = rules()->checkMaximize(m_requestedMaximizeMode); if (!adjust && m_requestedMaximizeMode == oldMode) { return; } StackingUpdatesBlocker blocker(workspace()); RequestGeometryBlocker geometryBlocker(this); // call into decoration update borders if (isDecorated() && decoration()->client() && !(options->borderlessMaximizedWindows() && m_requestedMaximizeMode == KWin::MaximizeFull)) { changeMaximizeRecursion = true; const auto c = decoration()->client().data(); if ((m_requestedMaximizeMode & MaximizeVertical) != (oldMode & MaximizeVertical)) { emit c->maximizedVerticallyChanged(m_requestedMaximizeMode & MaximizeVertical); } if ((m_requestedMaximizeMode & MaximizeHorizontal) != (oldMode & MaximizeHorizontal)) { emit c->maximizedHorizontallyChanged(m_requestedMaximizeMode & MaximizeHorizontal); } if ((m_requestedMaximizeMode == MaximizeFull) != (oldMode == MaximizeFull)) { emit c->maximizedChanged(m_requestedMaximizeMode & MaximizeFull); } changeMaximizeRecursion = false; } if (options->borderlessMaximizedWindows()) { // triggers a maximize change. // The next setNoBorder interation will exit since there's no change but the first recursion pullutes the restore geometry changeMaximizeRecursion = true; setNoBorder(rules()->checkNoBorder(m_requestedMaximizeMode == MaximizeFull)); changeMaximizeRecursion = false; } // Conditional quick tiling exit points const auto oldQuickTileMode = quickTileMode(); if (quickTileMode() != QuickTileMode(QuickTileFlag::None)) { if (oldMode == MaximizeFull && !clientArea.contains(m_geomMaximizeRestore.center())) { // Not restoring on the same screen // TODO: The following doesn't work for some reason //quick_tile_mode = QuickTileNone; // And exit quick tile mode manually } else if ((oldMode == MaximizeVertical && m_requestedMaximizeMode == MaximizeRestore) || (oldMode == MaximizeFull && m_requestedMaximizeMode == MaximizeHorizontal)) { // Modifying geometry of a tiled window updateQuickTileMode(QuickTileFlag::None); // Exit quick tile mode without restoring geometry } } 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(); } setFrameGeometry(workspace()->clientArea(MaximizeArea, this)); workspace()->raiseClient(this); } else { if (m_requestedMaximizeMode == MaximizeRestore) { updateQuickTileMode(QuickTileFlag::None); } if (quickTileMode() != oldQuickTileMode) { emit quickTileModeChanged(); } if (m_geomMaximizeRestore.isValid()) { setFrameGeometry(m_geomMaximizeRestore); } else { setFrameGeometry(workspace()->clientArea(PlacementArea, this)); } } } void XdgShellClient::setGeometryRestore(const QRect &geo) { m_geomMaximizeRestore = geo; } MaximizeMode XdgShellClient::maximizeMode() const { return m_maximizeMode; } MaximizeMode XdgShellClient::requestedMaximizeMode() const { return m_requestedMaximizeMode; } QRect XdgShellClient::geometryRestore() const { return m_geomMaximizeRestore; } bool XdgShellClient::noBorder() const { if (m_serverDecoration) { if (m_serverDecoration->mode() == ServerSideDecorationManagerInterface::Mode::Server) { return m_userNoBorder || isFullScreen(); } } if (m_xdgDecoration && m_xdgDecoration->requestedMode() != XdgDecorationInterface::Mode::ClientSide) { return m_userNoBorder || isFullScreen(); } return true; } bool XdgShellClient::isFullScreenable() const { if (!rules()->checkFullScreen(true)) { return false; } return !isSpecialWindow(); } void XdgShellClient::setFullScreen(bool set, bool user) { set = rules()->checkFullScreen(set); const bool wasFullscreen = isFullScreen(); if (wasFullscreen == set) { return; } if (isSpecialWindow()) { return; } if (user && !userCanSetFullScreen()) { return; } if (wasFullscreen) { workspace()->updateFocusMousePosition(Cursor::pos()); // may cause leave event } else { m_geomFsRestore = frameGeometry(); } m_fullScreen = set; if (set) { workspace()->raiseClient(this); } RequestGeometryBlocker requestBlocker(this); StackingUpdatesBlocker blocker1(workspace()); GeometryUpdatesBlocker blocker2(this); workspace()->updateClientLayer(this); // active fullscreens get different layer updateDecoration(false, false); if (set) { setFrameGeometry(workspace()->clientArea(FullScreenArea, this)); } else { if (m_geomFsRestore.isValid()) { int currentScreen = screen(); setFrameGeometry(QRect(m_geomFsRestore.topLeft(), adjustedSize(m_geomFsRestore.size()))); if( currentScreen != screen()) workspace()->sendClientToScreen( this, currentScreen ); } else { // this can happen when the window was first shown already fullscreen, // so let the client set the size by itself setFrameGeometry(QRect(workspace()->clientArea(PlacementArea, this).topLeft(), QSize(0, 0))); } } updateWindowRules(Rules::Fullscreen|Rules::Position|Rules::Size); emit fullScreenChanged(); } void XdgShellClient::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 XdgShellClient::setOnAllActivities(bool set) { Q_UNUSED(set) } void XdgShellClient::takeFocus() { if (rules()->checkAcceptFocus(wantsInput())) { if (m_xdgShellToplevel) { ping(PingReason::FocusWindow); } setActive(true); } if (!keepAbove() && !isOnScreenDisplay() && !belongsToDesktop()) { workspace()->setShowingDesktop(false); } } void XdgShellClient::doSetActive() { if (!isActive()) { return; } StackingUpdatesBlocker blocker(workspace()); workspace()->focusToNull(); } bool XdgShellClient::userCanSetFullScreen() const { if (m_xdgShellToplevel) { return true; } return false; } bool XdgShellClient::userCanSetNoBorder() const { if (m_serverDecoration && m_serverDecoration->mode() == ServerSideDecorationManagerInterface::Mode::Server) { return !isFullScreen() && !isShade(); } if (m_xdgDecoration && m_xdgDecoration->requestedMode() != XdgDecorationInterface::Mode::ClientSide) { return !isFullScreen() && !isShade(); } return false; } bool XdgShellClient::wantsInput() const { return rules()->checkAcceptFocus(acceptsFocus()); } bool XdgShellClient::acceptsFocus() const { if (waylandServer()->inputMethodConnection() == surface()->client()) { return false; } if (m_plasmaShellSurface) { if (m_plasmaShellSurface->role() == PlasmaShellSurfaceInterface::Role::OnScreenDisplay || m_plasmaShellSurface->role() == PlasmaShellSurfaceInterface::Role::ToolTip) { return false; } if (m_plasmaShellSurface->role() == PlasmaShellSurfaceInterface::Role::Notification || m_plasmaShellSurface->role() == PlasmaShellSurfaceInterface::Role::CriticalNotification) { return m_plasmaShellSurface->panelTakesFocus(); } } if (m_closing) { // a closing window does not accept focus return false; } if (m_unmapped) { // an unmapped window does not accept focus return false; } if (m_xdgShellToplevel) { // TODO: proper return true; } return false; } void XdgShellClient::createWindowId() { m_windowId = waylandServer()->createWindowId(surface()); } pid_t XdgShellClient::pid() const { return surface()->client()->processId(); } bool XdgShellClient::isLockScreen() const { return surface()->client() == waylandServer()->screenLockerClientConnection(); } bool XdgShellClient::isInputMethod() const { return surface()->client() == waylandServer()->inputMethodConnection(); } void XdgShellClient::requestGeometry(const QRect &rect) { if (m_requestGeometryBlockCounter != 0) { m_blockedRequestGeometry = rect; return; } QSize size; if (rect.isValid()) { size = rect.size() - QSize(borderLeft() + borderRight(), borderTop() + borderBottom()); } else { size = QSize(0, 0); } m_requestedClientSize = size; quint64 serialId = 0; if (m_xdgShellToplevel) { serialId = m_xdgShellToplevel->configure(xdgSurfaceStates(), size); } if (m_xdgShellPopup) { auto parent = transientFor(); if (parent) { const QPoint globalClientContentPos = parent->frameGeometry().topLeft() + parent->clientPos(); const QPoint relativeOffset = rect.topLeft() - globalClientContentPos; serialId = m_xdgShellPopup->configure(QRect(relativeOffset, size)); } } if (rect.isValid()) { //if there's no requested size, then there's implicity no positional information worth using PendingConfigureRequest configureRequest; configureRequest.serialId = serialId; configureRequest.positionAfterResize = rect.topLeft(); configureRequest.maximizeMode = m_requestedMaximizeMode; m_pendingConfigureRequests.append(configureRequest); } m_blockedRequestGeometry = QRect(); } void XdgShellClient::updatePendingGeometry() { QPoint position = pos(); MaximizeMode maximizeMode = m_maximizeMode; for (auto it = m_pendingConfigureRequests.begin(); it != m_pendingConfigureRequests.end(); it++) { if (it->serialId > m_lastAckedConfigureRequest) { //this serial is not acked yet, therefore we know all future serials are not break; } if (it->serialId == m_lastAckedConfigureRequest) { if (position != it->positionAfterResize) { addLayerRepaint(frameGeometry()); } position = it->positionAfterResize; maximizeMode = it->maximizeMode; m_pendingConfigureRequests.erase(m_pendingConfigureRequests.begin(), ++it); break; } //else serialId < m_lastAckedConfigureRequest and the state is now irrelevant and can be ignored } QRect geometry = QRect(position, adjustedSize()); if (isMove()) { geometry = adjustMoveGeometry(geometry); } if (isResize()) { geometry = adjustResizeGeometry(geometry); } doSetGeometry(geometry); updateMaximizeMode(maximizeMode); } void XdgShellClient::handleConfigureAcknowledged(quint32 serial) { m_lastAckedConfigureRequest = serial; } void XdgShellClient::handleTransientForChanged() { SurfaceInterface *transientSurface = nullptr; if (m_xdgShellToplevel) { if (auto transient = m_xdgShellToplevel->transientFor().data()) { transientSurface = transient->surface(); } } if (m_xdgShellPopup) { transientSurface = m_xdgShellPopup->transientFor().data(); } if (!transientSurface) { transientSurface = waylandServer()->findForeignTransientForSurface(surface()); } XdgShellClient *transientClient = waylandServer()->findClient(transientSurface); if (transientClient != transientFor()) { // Remove from main client. if (transientFor()) { transientFor()->removeTransient(this); } setTransientFor(transientClient); if (transientClient) { transientClient->addTransient(this); } } m_transient = (transientSurface != nullptr); } void XdgShellClient::handleWindowClassChanged(const QByteArray &windowClass) { setResourceClass(resourceName(), windowClass); if (m_isInitialized && supportsWindowRules()) { setupWindowRules(true); applyWindowRules(); } setDesktopFileName(windowClass); } void XdgShellClient::handleWindowGeometryChanged(const QRect &windowGeometry) { m_windowGeometry = windowGeometry; m_hasWindowGeometry = true; } void XdgShellClient::handleWindowTitleChanged(const QString &title) { const QString oldSuffix = m_captionSuffix; m_caption = title.simplified(); updateCaption(); if (m_captionSuffix == oldSuffix) { // Don't emit caption change twice it already got emitted by the changing suffix. emit captionChanged(); } } void XdgShellClient::handleMoveRequested(SeatInterface *seat, quint32 serial) { // FIXME: Check the seat and serial. Q_UNUSED(seat) Q_UNUSED(serial) performMouseCommand(Options::MouseMove, Cursor::pos()); } void XdgShellClient::handleResizeRequested(SeatInterface *seat, quint32 serial, Qt::Edges edges) { // FIXME: 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 position = PositionCenter; if (edges.testFlag(Qt::TopEdge)) { position = PositionTop; } else if (edges.testFlag(Qt::BottomEdge)) { position = PositionBottom; } if (edges.testFlag(Qt::LeftEdge)) { position = Position(position | PositionLeft); } else if (edges.testFlag(Qt::RightEdge)) { position = Position(position | PositionRight); } return position; }; setMoveResizePointerMode(toPosition()); if (!startMoveResize()) { setMoveResizePointerButtonDown(false); } updateCursor(); } void XdgShellClient::handleMinimizeRequested() { performMouseCommand(Options::MouseMinimize, Cursor::pos()); } void XdgShellClient::handleMaximizeRequested(bool maximized) { // If the maximized state of the client hasn't been changed due to a window // rule or because the requested state is the same as the current, then the // compositor still has to send a configure event. RequestGeometryBlocker blocker(this); maximize(maximized ? MaximizeFull : MaximizeRestore); } void XdgShellClient::handleFullScreenRequested(bool fullScreen, OutputInterface *output) { // FIXME: Consider output as well. Q_UNUSED(output); setFullScreen(fullScreen, false); } void XdgShellClient::handleWindowMenuRequested(SeatInterface *seat, quint32 serial, const QPoint &surfacePos) { // FIXME: Check the seat and serial. Q_UNUSED(seat) Q_UNUSED(serial) performMouseCommand(Options::MouseOperationsMenu, pos() + surfacePos); } void XdgShellClient::handleGrabRequested(SeatInterface *seat, quint32 serial) { // FIXME: Check the seat and serial as well whether the parent had focus. Q_UNUSED(seat) Q_UNUSED(serial) m_hasPopupGrab = true; } void XdgShellClient::handlePingDelayed(quint32 serial) { auto it = m_pingSerials.find(serial); if (it != m_pingSerials.end()) { qCDebug(KWIN_CORE) << "First ping timeout:" << caption(); setUnresponsive(true); } } void XdgShellClient::handlePingTimeout(quint32 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); } } void XdgShellClient::handlePongReceived(quint32 serial) { auto it = m_pingSerials.find(serial); if (it != m_pingSerials.end()) { setUnresponsive(false); m_pingSerials.erase(it); } } void XdgShellClient::handleCommitted() { if (!surface()->buffer()) { return; } if (!m_hasWindowGeometry) { m_windowGeometry = subSurfaceTreeRect(surface()); } updatePendingGeometry(); setDepth((surface()->buffer()->hasAlphaChannel() && !isDesktop()) ? 32 : 24); markAsMapped(); } void XdgShellClient::resizeWithChecks(int w, int h, ForceGeometry_t force) { const 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(); } setFrameGeometry(x(), y(), w, h, force); } void XdgShellClient::unmap() { m_unmapped = true; if (isMoveResize()) { leaveMoveResize(); } m_requestedClientSize = QSize(0, 0); destroyWindowManagementInterface(); if (Workspace::self()) { addWorkspaceRepaint(visibleRect()); workspace()->clientHidden(this); } emit windowHidden(this); } void XdgShellClient::installPlasmaShellSurface(PlasmaShellSurfaceInterface *surface) { m_plasmaShellSurface = surface; auto updatePosition = [this, surface] { // That's a mis-use of doSetGeometry method. One should instead use move method. QRect rect = QRect(surface->position(), size()); doSetGeometry(rect); }; auto updateRole = [this, surface] { NET::WindowType type = NET::Unknown; switch (surface->role()) { case PlasmaShellSurfaceInterface::Role::Desktop: type = NET::Desktop; break; case PlasmaShellSurfaceInterface::Role::Panel: type = NET::Dock; break; case PlasmaShellSurfaceInterface::Role::OnScreenDisplay: type = NET::OnScreenDisplay; break; case PlasmaShellSurfaceInterface::Role::Notification: type = NET::Notification; break; case PlasmaShellSurfaceInterface::Role::ToolTip: type = NET::Tooltip; break; case PlasmaShellSurfaceInterface::Role::CriticalNotification: type = NET::CriticalNotification; break; case PlasmaShellSurfaceInterface::Role::Normal: default: type = NET::Normal; break; } if (type != m_windowType) { m_windowType = type; if (m_windowType == NET::Desktop || type == NET::Dock || type == NET::OnScreenDisplay || type == NET::Notification || type == NET::Tooltip || type == NET::CriticalNotification) { setOnAllDesktops(true); } workspace()->updateClientArea(); } }; connect(surface, &PlasmaShellSurfaceInterface::positionChanged, this, updatePosition); connect(surface, &PlasmaShellSurfaceInterface::roleChanged, this, updateRole); connect(surface, &PlasmaShellSurfaceInterface::panelBehaviorChanged, this, [this] { updateShowOnScreenEdge(); workspace()->updateClientArea(); } ); connect(surface, &PlasmaShellSurfaceInterface::panelAutoHideHideRequested, this, [this] { hideClient(true); m_plasmaShellSurface->hideAutoHidingPanel(); updateShowOnScreenEdge(); } ); connect(surface, &PlasmaShellSurfaceInterface::panelAutoHideShowRequested, this, [this] { hideClient(false); ScreenEdges::self()->reserve(this, ElectricNone); m_plasmaShellSurface->showAutoHidingPanel(); } ); updatePosition(); updateRole(); updateShowOnScreenEdge(); - connect(this, &XdgShellClient::geometryChanged, this, &XdgShellClient::updateShowOnScreenEdge); + connect(this, &XdgShellClient::frameGeometryChanged, this, &XdgShellClient::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 XdgShellClient::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 const QRect clientGeometry = frameGeometry(); Qt::Edges edges; for (int i = 0; i < screens()->count(); i++) { const QRect screenGeometry = screens()->geometry(i); if (screenGeometry.left() == clientGeometry.left()) { edges |= Qt::LeftEdge; } if (screenGeometry.right() == clientGeometry.right()) { edges |= Qt::RightEdge; } if (screenGeometry.top() == clientGeometry.top()) { edges |= Qt::TopEdge; } if (screenGeometry.bottom() == clientGeometry.bottom()) { 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 = [clientGeometry](Qt::Edges edges, Qt::Edge horiz, Qt::Edge vert) { if (edges.testFlag(horiz) && edges.testFlag(vert)) { if (clientGeometry.width() >= clientGeometry.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 XdgShellClient::isInitialPositionSet() const { if (m_plasmaShellSurface) { return m_plasmaShellSurface->isPositionSet(); } return false; } void XdgShellClient::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 XdgShellClient::installPalette(ServerSideDecorationPaletteInterface *palette) { m_paletteInterface = palette; auto updatePalette = [this](const QString &palette) { AbstractClient::updateColorScheme(rules()->checkDecoColor(palette)); }; connect(m_paletteInterface, &ServerSideDecorationPaletteInterface::paletteChanged, this, [=](const QString &palette) { updatePalette(palette); }); connect(m_paletteInterface, &QObject::destroyed, this, [=]() { updatePalette(QString()); }); updatePalette(palette->palette()); } void XdgShellClient::updateColorScheme() { if (m_paletteInterface) { AbstractClient::updateColorScheme(rules()->checkDecoColor(m_paletteInterface->palette())); } else { AbstractClient::updateColorScheme(rules()->checkDecoColor(QString())); } } void XdgShellClient::updateMaximizeMode(MaximizeMode maximizeMode) { if (maximizeMode == m_maximizeMode) { return; } m_maximizeMode = maximizeMode; updateWindowRules(Rules::MaximizeHoriz | Rules::MaximizeVert | Rules::Position | Rules::Size); emit clientMaximizedStateChanged(this, m_maximizeMode); emit clientMaximizedStateChanged(this, m_maximizeMode & MaximizeHorizontal, m_maximizeMode & MaximizeVertical); } bool XdgShellClient::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; } quint32 XdgShellClient::windowId() const { return m_windowId; } void XdgShellClient::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 XdgShellClient::isTransient() const { return m_transient; } bool XdgShellClient::hasTransientPlacementHint() const { return isTransient() && transientFor() && m_xdgShellPopup; } QRect XdgShellClient::transientPlacement(const QRect &bounds) const { Q_ASSERT(m_xdgShellPopup); QRect anchorRect; Qt::Edges anchorEdge; Qt::Edges gravity; QPoint offset; PositionerConstraints constraintAdjustments; QSize size = frameGeometry().size(); const QPoint parentClientPos = transientFor()->pos() + transientFor()->clientPos(); // 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; }; anchorRect = m_xdgShellPopup->anchorRect(); anchorEdge = m_xdgShellPopup->anchorEdge(); gravity = m_xdgShellPopup->gravity(); offset = m_xdgShellPopup->anchorOffset(); constraintAdjustments = m_xdgShellPopup->constraintAdjustments(); if (!size.isValid()) { size = m_xdgShellPopup->initialSize(); } QRect popupRect(popupOffset(anchorRect, anchorEdge, gravity, size) + offset + parentClientPos, size); //if that fits, we don't need to do anything if (inBounds(popupRect)) { return popupRect; } //otherwise apply constraint adjustment per axis in order XDG Shell Popup states if (constraintAdjustments & PositionerConstraint::FlipX) { if (!inBounds(popupRect, 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 flippedPopupRect = QRect(popupOffset(anchorRect, flippedAnchorEdge, flippedGravity, size) + offset + parentClientPos, size); //if it still doesn't fit we should continue with the unflipped version if (inBounds(flippedPopupRect, Qt::LeftEdge | Qt::RightEdge)) { popupRect.moveLeft(flippedPopupRect.left()); } } } if (constraintAdjustments & PositionerConstraint::SlideX) { if (!inBounds(popupRect, Qt::LeftEdge)) { popupRect.moveLeft(bounds.left()); } if (!inBounds(popupRect, Qt::RightEdge)) { popupRect.moveRight(bounds.right()); } } if (constraintAdjustments & PositionerConstraint::ResizeX) { QRect unconstrainedRect = popupRect; if (!inBounds(unconstrainedRect, Qt::LeftEdge)) { unconstrainedRect.setLeft(bounds.left()); } if (!inBounds(unconstrainedRect, Qt::RightEdge)) { unconstrainedRect.setRight(bounds.right()); } if (unconstrainedRect.isValid()) { popupRect = unconstrainedRect; } } if (constraintAdjustments & PositionerConstraint::FlipY) { if (!inBounds(popupRect, 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 flippedPopupRect = QRect(popupOffset(anchorRect, flippedAnchorEdge, flippedGravity, size) + offset + parentClientPos, size); //if it still doesn't fit we should continue with the unflipped version if (inBounds(flippedPopupRect, Qt::TopEdge | Qt::BottomEdge)) { popupRect.moveTop(flippedPopupRect.top()); } } } if (constraintAdjustments & PositionerConstraint::SlideY) { if (!inBounds(popupRect, Qt::TopEdge)) { popupRect.moveTop(bounds.top()); } if (!inBounds(popupRect, Qt::BottomEdge)) { popupRect.moveBottom(bounds.bottom()); } } if (constraintAdjustments & PositionerConstraint::ResizeY) { QRect unconstrainedRect = popupRect; if (!inBounds(unconstrainedRect, Qt::TopEdge)) { unconstrainedRect.setTop(bounds.top()); } if (!inBounds(unconstrainedRect, Qt::BottomEdge)) { unconstrainedRect.setBottom(bounds.bottom()); } if (unconstrainedRect.isValid()) { popupRect = unconstrainedRect; } } return popupRect; } QPoint XdgShellClient::popupOffset(const QRect &anchorRect, const Qt::Edges anchorEdge, const Qt::Edges gravity, const QSize popupSize) const { QPoint anchorPoint; switch (anchorEdge & (Qt::LeftEdge | Qt::RightEdge)) { case Qt::LeftEdge: anchorPoint.setX(anchorRect.x()); break; case Qt::RightEdge: anchorPoint.setX(anchorRect.x() + anchorRect.width()); break; default: anchorPoint.setX(qRound(anchorRect.x() + anchorRect.width() / 2.0)); } switch (anchorEdge & (Qt::TopEdge | Qt::BottomEdge)) { case Qt::TopEdge: anchorPoint.setY(anchorRect.y()); break; case Qt::BottomEdge: anchorPoint.setY(anchorRect.y() + anchorRect.height()); break; default: anchorPoint.setY(qRound(anchorRect.y() + anchorRect.height() / 2.0)); } // calculate where the top left point of the popup will end up with the applied gravity // gravity indicates direction. i.e if gravitating towards the top the popup's bottom edge // will next to the anchor point QPoint popupPosAdjust; switch (gravity & (Qt::LeftEdge | Qt::RightEdge)) { case Qt::LeftEdge: popupPosAdjust.setX(-popupSize.width()); break; case Qt::RightEdge: popupPosAdjust.setX(0); break; default: popupPosAdjust.setX(qRound(-popupSize.width() / 2.0)); } switch (gravity & (Qt::TopEdge | Qt::BottomEdge)) { case Qt::TopEdge: popupPosAdjust.setY(-popupSize.height()); break; case Qt::BottomEdge: popupPosAdjust.setY(0); break; default: popupPosAdjust.setY(qRound(-popupSize.height() / 2.0)); } return anchorPoint + popupPosAdjust; } void XdgShellClient::doResizeSync() { requestGeometry(moveResizeGeometry()); } QMatrix4x4 XdgShellClient::inputTransformation() const { QMatrix4x4 matrix; matrix.translate(-m_bufferGeometry.x(), -m_bufferGeometry.y()); return matrix; } void XdgShellClient::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 XdgShellClient is getting destroyed, too updateDecoration(true); } } ); if (!m_unmapped) { updateDecoration(true); } connect(m_serverDecoration, &ServerSideDecorationInterface::modeRequested, this, [this] (ServerSideDecorationManagerInterface::Mode mode) { const bool changed = mode != m_serverDecoration->mode(); if (changed && !m_unmapped) { updateDecoration(false); } } ); } void XdgShellClient::installXdgDecoration(XdgDecorationInterface *deco) { Q_ASSERT(m_xdgShellToplevel); m_xdgDecoration = deco; connect(m_xdgDecoration, &QObject::destroyed, this, [this] { m_xdgDecoration = nullptr; if (m_closing || !Workspace::self()) { return; } updateDecoration(true); } ); connect(m_xdgDecoration, &XdgDecorationInterface::modeRequested, this, [this] () { //force is true as we must send a new configure response updateDecoration(false, true); }); } bool XdgShellClient::shouldExposeToWindowManagement() { if (isLockScreen()) { return false; } if (m_xdgShellPopup) { return false; } return true; } KWayland::Server::XdgShellSurfaceInterface::States XdgShellClient::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 XdgShellClient::doMinimize() { if (isMinimized()) { workspace()->clientHidden(this); } else { emit windowShown(this); } workspace()->updateMinimizedOfTransients(this); } void XdgShellClient::placeIn(const QRect &area) { Placement::self()->place(this, area); setGeometryRestore(frameGeometry()); } void XdgShellClient::showOnScreenEdge() { if (!m_plasmaShellSurface || m_unmapped) { return; } hideClient(false); workspace()->raiseClient(this); if (m_plasmaShellSurface->panelBehavior() == PlasmaShellSurfaceInterface::PanelBehavior::AutoHide) { m_plasmaShellSurface->showAutoHidingPanel(); } } bool XdgShellClient::dockWantsInput() const { if (m_plasmaShellSurface) { if (m_plasmaShellSurface->role() == PlasmaShellSurfaceInterface::Role::Panel) { return m_plasmaShellSurface->panelTakesFocus(); } } return false; } void XdgShellClient::killWindow() { if (!surface()) { return; } auto c = surface()->client(); if (c->processId() == getpid() || c->processId() == 0) { c->destroy(); return; } ::kill(c->processId(), SIGTERM); // give it time to terminate and only if terminate fails, try destroy Wayland connection QTimer::singleShot(5000, c, &ClientConnection::destroy); } bool XdgShellClient::isLocalhost() const { return true; } bool XdgShellClient::hasPopupGrab() const { return m_hasPopupGrab; } void XdgShellClient::popupDone() { if (m_xdgShellPopup) { m_xdgShellPopup->popupDone(); } } void XdgShellClient::updateClientOutputs() { QVector clientOutputs; const auto outputs = waylandServer()->display()->outputs(); for (OutputInterface *output : outputs) { const QRect outputGeometry(output->globalPosition(), output->pixelSize() / output->scale()); if (frameGeometry().intersects(outputGeometry)) { clientOutputs << output; } } surface()->setOutputs(clientOutputs); } bool XdgShellClient::isPopupWindow() const { if (Toplevel::isPopupWindow()) { return true; } if (m_xdgShellPopup != nullptr) { return true; } return false; } bool XdgShellClient::supportsWindowRules() const { if (m_plasmaShellSurface) { return false; } return m_xdgShellToplevel; } QRect XdgShellClient::adjustMoveGeometry(const QRect &rect) const { QRect geometry = rect; geometry.moveTopLeft(moveResizeGeometry().topLeft()); return geometry; } QRect XdgShellClient::adjustResizeGeometry(const QRect &rect) const { QRect geometry = rect; // We need to adjust frame geometry because configure events carry the maximum window geometry // size. A client that has aspect ratio can attach a buffer with smaller size than the one in // a configure event. switch (moveResizePointerMode()) { case PositionTopLeft: geometry.moveRight(moveResizeGeometry().right()); geometry.moveBottom(moveResizeGeometry().bottom()); break; case PositionTop: case PositionTopRight: geometry.moveLeft(moveResizeGeometry().left()); geometry.moveBottom(moveResizeGeometry().bottom()); break; case PositionRight: case PositionBottomRight: case PositionBottom: geometry.moveLeft(moveResizeGeometry().left()); geometry.moveTop(moveResizeGeometry().top()); break; case PositionBottomLeft: case PositionLeft: geometry.moveRight(moveResizeGeometry().right()); geometry.moveTop(moveResizeGeometry().top()); break; case PositionCenter: Q_UNREACHABLE(); } return geometry; } void XdgShellClient::ping(PingReason reason) { Q_ASSERT(m_xdgShellToplevel); XdgShellInterface *shell = static_cast(m_xdgShellToplevel->global()); const quint32 serial = shell->ping(m_xdgShellToplevel); m_pingSerials.insert(serial, reason); } }