kwin/shell_client.cpp
Vlad Zagorodniy 45c93c6cae Destroy ShellClient before Workspace is gone
Summary:
Managing lifetime of objects during tear down is a bit clunky in KWin
mostly because the wayland server outlives the workspace.

3f4e733468 tried to tackle one aspect of this problem, but the proposed
solution is good only in short term. If a ShellClient wants to discard
force temporarily rules, it needs to access RuleBook, whose lifetime is
bounded to the workspace, no matter what happens. Otherwise, the force
temporarily rule will be applied again on the next startup.

It's worth to mention that there was another attempt to address this
problem, see commit 826b9742e9. It was reverted because some internal
clients may need to destroy Wayland resources during tear down.

This change takes another approach. In order to ensure that ShellClient
can access RuleBook during tear down, we manually destroy Wayland clients
in destructor of the Workspace class. Something is done already for X11
clients.

Reviewers: #kwin

Subscribers: romangg, kwin

Tags: #kwin

Differential Revision: https://phabricator.kde.org/D22986
2019-09-10 11:07:31 +03:00

2014 lines
65 KiB
C++

/********************************************************************
KWin - the KDE window manager
This file is part of the KDE project.
Copyright (C) 2015 Martin Gräßlin <mgraesslin@kde.org>
Copyright (C) 2018 David Edmundson <davidedmundson@kde.org>
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 <http://www.gnu.org/licenses/>.
*********************************************************************/
#include "shell_client.h"
#include "composite.h"
#include "cursor.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 "decorations/decorationbridge.h"
#include "decorations/decoratedclient.h"
#include <KDecoration2/Decoration>
#include <KDecoration2/DecoratedClient>
#include <KWayland/Client/surface.h>
#include <KWayland/Server/display.h>
#include <KWayland/Server/clientconnection.h>
#include <KWayland/Server/seat_interface.h>
#include <KWayland/Server/shell_interface.h>
#include <KWayland/Server/surface_interface.h>
#include <KWayland/Server/buffer_interface.h>
#include <KWayland/Server/plasmashell_interface.h>
#include <KWayland/Server/shadow_interface.h>
#include <KWayland/Server/server_decoration_interface.h>
#include <KWayland/Server/qtsurfaceextension_interface.h>
#include <KWayland/Server/plasmawindowmanagement_interface.h>
#include <KWayland/Server/appmenu_interface.h>
#include <KWayland/Server/server_decoration_palette_interface.h>
#include <KWayland/Server/xdgdecoration_interface.h>
#include <QFileInfo>
#include <sys/types.h>
#include <unistd.h>
#include <csignal>
Q_DECLARE_METATYPE(NET::WindowType)
using namespace KWayland::Server;
namespace KWin
{
ShellClient::ShellClient(ShellSurfaceInterface *surface)
: AbstractClient()
, m_shellSurface(surface)
, m_xdgShellSurface(nullptr)
, m_xdgShellPopup(nullptr)
, m_internal(surface->client() == waylandServer()->internalConnection())
{
setSurface(surface->surface());
init();
m_isInitialized = true;
}
ShellClient::ShellClient(XdgShellSurfaceInterface *surface)
: AbstractClient()
, m_shellSurface(nullptr)
, m_xdgShellSurface(surface)
, m_xdgShellPopup(nullptr)
, m_internal(surface->client() == waylandServer()->internalConnection())
{
setSurface(surface->surface());
m_requestGeometryBlockCounter++;
init();
connect(surface->surface(), &SurfaceInterface::committed, this, &ShellClient::finishInit);
}
ShellClient::ShellClient(XdgShellPopupInterface *surface)
: AbstractClient()
, m_shellSurface(nullptr)
, m_xdgShellSurface(nullptr)
, m_xdgShellPopup(surface)
, m_internal(surface->client() == waylandServer()->internalConnection())
{
setSurface(surface->surface());
m_requestGeometryBlockCounter++;
init();
connect(surface->surface(), &SurfaceInterface::committed, this, &ShellClient::finishInit);
}
ShellClient::~ShellClient() = default;
template <class T>
void ShellClient::initSurface(T *shellSurface)
{
m_caption = shellSurface->title().simplified();
// delay till end of init
QTimer::singleShot(0, this, &ShellClient::updateCaption);
connect(shellSurface, &T::destroyed, this, &ShellClient::destroyClient);
connect(shellSurface, &T::titleChanged, this,
[this] (const QString &s) {
const auto oldSuffix = m_captionSuffix;
m_caption = s.simplified();
updateCaption();
if (m_captionSuffix == oldSuffix) {
// don't emit caption change twice
// it already got emitted by the changing suffix
emit captionChanged();
}
}
);
connect(shellSurface, &T::moveRequested, this,
[this] {
// TODO: check the seat and serial
performMouseCommand(Options::MouseMove, Cursor::pos());
}
);
// determine the resource name, this is inspired from ICCCM 4.1.2.5
// the binary name of the invoked client
QFileInfo info{shellSurface->client()->executablePath()};
QByteArray resourceName;
if (info.exists()) {
resourceName = info.fileName().toUtf8();
}
setResourceClass(resourceName, shellSurface->windowClass());
setDesktopFileName(shellSurface->windowClass());
connect(shellSurface, &T::windowClassChanged, this,
[this, resourceName] (const QByteArray &windowClass) {
setResourceClass(resourceName, windowClass);
if (m_isInitialized && supportsWindowRules()) {
setupWindowRules(true);
applyWindowRules();
}
setDesktopFileName(windowClass);
}
);
connect(shellSurface, &T::resizeRequested, this,
[this] (SeatInterface *seat, quint32 serial, Qt::Edges edges) {
// TODO: check the seat and serial
Q_UNUSED(seat)
Q_UNUSED(serial)
if (!isResizable() || isShade()) {
return;
}
if (isMoveResize()) {
finishMoveResize(false);
}
setMoveResizePointerButtonDown(true);
setMoveOffset(Cursor::pos() - pos()); // map from global
setInvertedMoveOffset(rect().bottomRight() - moveOffset());
setUnrestrictedMoveResize(false);
auto toPosition = [edges] {
Position pos = PositionCenter;
if (edges.testFlag(Qt::TopEdge)) {
pos = PositionTop;
} else if (edges.testFlag(Qt::BottomEdge)) {
pos = PositionBottom;
}
if (edges.testFlag(Qt::LeftEdge)) {
pos = Position(pos | PositionLeft);
} else if (edges.testFlag(Qt::RightEdge)) {
pos = Position(pos | PositionRight);
}
return pos;
};
setMoveResizePointerMode(toPosition());
if (!startMoveResize())
setMoveResizePointerButtonDown(false);
updateCursor();
}
);
connect(shellSurface, &T::maximizedChanged, this,
[this] (bool maximized) {
if (m_shellSurface && isFullScreen()) {
// ignore for wl_shell - there it is mutual exclusive and messes with the geometry
return;
}
// If the maximized state of the client hasn't been changed due to a window
// rule or because the requested state is the same as the current, then the
// compositor still has to send a configure event.
RequestGeometryBlocker blocker(this);
maximize(maximized ? MaximizeFull : MaximizeRestore);
}
);
// TODO: consider output!
connect(shellSurface, &T::fullscreenChanged, this, &ShellClient::clientFullScreenChanged);
connect(shellSurface, &T::transientForChanged, this, &ShellClient::setTransient);
connect(this, &ShellClient::geometryChanged, this, &ShellClient::updateClientOutputs);
connect(screens(), &Screens::changed, this, &ShellClient::updateClientOutputs);
}
void ShellClient::init()
{
connect(this, &ShellClient::desktopFileNameChanged, this, &ShellClient::updateIcon);
createWindowId();
setupCompositing();
updateIcon();
SurfaceInterface *s = surface();
Q_ASSERT(s);
if (s->buffer()) {
setReadyForPainting();
if (shouldExposeToWindowManagement()) {
setupWindowManagementInterface();
}
m_unmapped = false;
m_clientSize = s->size();
} else {
ready_for_painting = false;
}
if (!m_internal) {
doSetGeometry(QRect(QPoint(0, 0), m_clientSize));
}
if (waylandServer()->inputMethodConnection() == s->client()) {
m_windowType = NET::OnScreenDisplay;
}
connect(s, &SurfaceInterface::sizeChanged, this,
[this] {
m_clientSize = surface()->size();
doSetGeometry(QRect(pos(), m_clientSize + QSize(borderLeft() + borderRight(), borderTop() + borderBottom())));
}
);
connect(s, &SurfaceInterface::unmapped, this, &ShellClient::unmap);
connect(s, &SurfaceInterface::unbound, this, &ShellClient::destroyClient);
connect(s, &SurfaceInterface::destroyed, this, &ShellClient::destroyClient);
if (m_shellSurface) {
initSurface(m_shellSurface);
auto setPopup = [this] {
// TODO: verify grab serial
m_hasPopupGrab = m_shellSurface->isPopup();
};
connect(m_shellSurface, &ShellSurfaceInterface::popupChanged, this, setPopup);
setPopup();
} else if (m_xdgShellSurface) {
initSurface(m_xdgShellSurface);
auto global = static_cast<XdgShellInterface *>(m_xdgShellSurface->global());
connect(global, &XdgShellInterface::pingDelayed,
this, [this](qint32 serial) {
auto it = m_pingSerials.find(serial);
if (it != m_pingSerials.end()) {
qCDebug(KWIN_CORE) << "First ping timeout:" << caption();
setUnresponsive(true);
}
});
connect(m_xdgShellSurface, &XdgShellSurfaceInterface::configureAcknowledged, this, [this](int serial) {
m_lastAckedConfigureRequest = serial;
});
connect(global, &XdgShellInterface::pingTimeout,
this, [this](qint32 serial) {
auto it = m_pingSerials.find(serial);
if (it != m_pingSerials.end()) {
if (it.value() == PingReason::CloseWindow) {
qCDebug(KWIN_CORE) << "Final ping timeout on a close attempt, asking to kill:" << caption();
//for internal windows, killing the window will delete this
QPointer<QObject> guard(this);
killWindow();
if (!guard) {
return;
}
}
m_pingSerials.erase(it);
}
});
connect(global, &XdgShellInterface::pongReceived,
this, [this](qint32 serial){
auto it = m_pingSerials.find(serial);
if (it != m_pingSerials.end()) {
setUnresponsive(false);
m_pingSerials.erase(it);
}
});
connect(m_xdgShellSurface, &XdgShellSurfaceInterface::windowMenuRequested, this,
[this] (SeatInterface *seat, quint32 serial, const QPoint &surfacePos) {
// TODO: check serial on seat
Q_UNUSED(seat)
Q_UNUSED(serial)
performMouseCommand(Options::MouseOperationsMenu, pos() + surfacePos);
}
);
connect(m_xdgShellSurface, &XdgShellSurfaceInterface::minimizeRequested, this,
[this] {
performMouseCommand(Options::MouseMinimize, Cursor::pos());
}
);
auto configure = [this] {
if (m_closing) {
return;
}
if (m_requestGeometryBlockCounter != 0 || areGeometryUpdatesBlocked()) {
return;
}
m_xdgShellSurface->configure(xdgSurfaceStates(), m_requestedClientSize);
};
connect(this, &AbstractClient::activeChanged, this, configure);
connect(this, &AbstractClient::clientStartUserMovedResized, this, configure);
connect(this, &AbstractClient::clientFinishUserMovedResized, this, configure);
} else if (m_xdgShellPopup) {
connect(m_xdgShellPopup, &XdgShellPopupInterface::grabRequested, this, [this](SeatInterface *seat, quint32 serial) {
Q_UNUSED(seat)
Q_UNUSED(serial)
//TODO - should check the parent had focus
m_hasPopupGrab = true;
});
connect(m_xdgShellPopup, &XdgShellPopupInterface::configureAcknowledged, this, [this](int serial) {
m_lastAckedConfigureRequest = serial;
});
connect(m_xdgShellPopup, &XdgShellPopupInterface::destroyed, this, &ShellClient::destroyClient);
}
// set initial desktop
setDesktop(m_internal ? int(NET::OnAllDesktops) : VirtualDesktopManager::self()->current());
// setup shadow integration
getShadow();
connect(s, &SurfaceInterface::shadowChanged, this, &Toplevel::getShadow);
connect(waylandServer(), &WaylandServer::foreignTransientChanged, this, [this](KWayland::Server::SurfaceInterface *child) {
if (child == surface()) {
setTransient();
}
});
setTransient();
AbstractClient::updateColorScheme(QString());
}
void ShellClient::finishInit() {
SurfaceInterface *s = surface();
disconnect(s, &SurfaceInterface::committed, this, &ShellClient::finishInit);
bool needsPlacement = !isInitialPositionSet();
if (supportsWindowRules()) {
setupWindowRules(false);
const QRect originalGeometry = QRect(pos(), sizeForClientSize(clientSize()));
const QRect ruledGeometry = rules()->checkGeometry(originalGeometry, true);
if (originalGeometry != ruledGeometry) {
setGeometry(ruledGeometry);
}
maximize(rules()->checkMaximize(maximizeMode(), true));
setDesktop(rules()->checkDesktop(desktop(), true));
setDesktopFileName(rules()->checkDesktopFile(desktopFileName(), true).toUtf8());
if (rules()->checkMinimize(isMinimized(), true)) {
minimize(true); // No animation.
}
setSkipTaskbar(rules()->checkSkipTaskbar(skipTaskbar(), true));
setSkipPager(rules()->checkSkipPager(skipPager(), true));
setSkipSwitcher(rules()->checkSkipSwitcher(skipSwitcher(), true));
setKeepAbove(rules()->checkKeepAbove(keepAbove(), true));
setKeepBelow(rules()->checkKeepBelow(keepBelow(), true));
setShortcut(rules()->checkShortcut(shortcut().toString(), true));
updateColorScheme();
// Don't place the client if its position is set by a rule.
if (rules()->checkPosition(invalidPoint, true) != invalidPoint) {
needsPlacement = false;
}
// Don't place the client if the maximize state is set by a rule.
if (requestedMaximizeMode() != MaximizeRestore) {
needsPlacement = false;
}
discardTemporaryRules();
RuleBook::self()->discardUsed(this, false); // Remove Apply Now rules.
updateWindowRules(Rules::All);
}
if (needsPlacement) {
QRect area = workspace()->clientArea(PlacementArea, Screens::self()->current(), desktop());
placeIn(area);
}
m_requestGeometryBlockCounter--;
if (m_requestGeometryBlockCounter == 0) {
requestGeometry(m_blockedRequestGeometry);
}
m_isInitialized = true;
}
void ShellClient::destroyClient()
{
m_closing = true;
#ifdef KWIN_BUILD_TABBOX
TabBox::TabBox *tabBox = TabBox::TabBox::self();
if (tabBox && 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_shellSurface = nullptr;
m_xdgShellSurface = nullptr;
m_xdgShellPopup = nullptr;
deleteClient(this);
}
void ShellClient::deleteClient(ShellClient *c)
{
delete c;
}
QSize ShellClient::toWindowGeometry(const QSize &size) const
{
QSize adjustedSize = size - QSize(borderLeft() + borderRight(), borderTop() + borderBottom());
// a client going fullscreen should have the window the contents size of the screen
if (!isFullScreen() && requestedMaximizeMode() != MaximizeFull) {
adjustedSize -= QSize(m_windowMargins.left() + m_windowMargins.right(), m_windowMargins.top() + m_windowMargins.bottom());
}
return adjustedSize;
}
QStringList ShellClient::activities() const
{
// TODO: implement
return QStringList();
}
QPoint ShellClient::clientContentPos() const
{
return -1 * clientPos();
}
QSize ShellClient::clientSize() const
{
return m_clientSize;
}
void ShellClient::debug(QDebug &stream) const
{
stream.nospace();
stream << "\'ShellClient:" << surface() << ";WMCLASS:" << resourceClass() << ":"
<< resourceName() << ";Caption:" << caption() << "\'";
}
bool ShellClient::belongsToDesktop() const
{
const auto clients = waylandServer()->clients();
return std::any_of(clients.constBegin(), clients.constEnd(),
[this](const ShellClient *client) {
if (belongsToSameApplication(client, SameApplicationChecks())) {
return client->isDesktop();
}
return false;
}
);
}
Layer ShellClient::layerForDock() const
{
if (m_plasmaShellSurface) {
switch (m_plasmaShellSurface->panelBehavior()) {
case PlasmaShellSurfaceInterface::PanelBehavior::WindowsCanCover:
return NormalLayer;
case PlasmaShellSurfaceInterface::PanelBehavior::AutoHide:
return AboveLayer;
case PlasmaShellSurfaceInterface::PanelBehavior::WindowsGoBelow:
case PlasmaShellSurfaceInterface::PanelBehavior::AlwaysVisible:
return DockLayer;
default:
Q_UNREACHABLE();
break;
}
}
return AbstractClient::layerForDock();
}
QRect ShellClient::transparentRect() const
{
// TODO: implement
return QRect();
}
NET::WindowType ShellClient::windowType(bool direct, int supported_types) const
{
// TODO: implement
Q_UNUSED(direct)
Q_UNUSED(supported_types)
return m_windowType;
}
double ShellClient::opacity() const
{
return m_opacity;
}
void ShellClient::setOpacity(double opacity)
{
const qreal newOpacity = qBound(0.0, opacity, 1.0);
if (newOpacity == m_opacity) {
return;
}
const qreal oldOpacity = m_opacity;
m_opacity = newOpacity;
addRepaintFull();
emit opacityChanged(this, oldOpacity);
}
void ShellClient::addDamage(const QRegion &damage)
{
auto s = surface();
if (s->size().isValid()) {
m_clientSize = s->size();
updateWindowMargins();
updatePendingGeometry();
}
markAsMapped();
setDepth((s->buffer()->hasAlphaChannel() && !isDesktop()) ? 32 : 24);
repaints_region += damage.translated(clientPos());
Toplevel::addDamage(damage);
}
void ShellClient::markAsMapped()
{
if (!m_unmapped) {
return;
}
m_unmapped = false;
if (!ready_for_painting) {
setReadyForPainting();
} else {
addRepaintFull();
emit windowShown(this);
}
if (shouldExposeToWindowManagement()) {
setupWindowManagementInterface();
}
updateShowOnScreenEdge();
}
void ShellClient::createDecoration(const QRect &oldGeom)
{
KDecoration2::Decoration *decoration = Decoration::DecorationBridge::self()->createDecoration(this);
if (decoration) {
QMetaObject::invokeMethod(decoration, "update", Qt::QueuedConnection);
connect(decoration, &KDecoration2::Decoration::shadowChanged, this, &Toplevel::getShadow);
connect(decoration, &KDecoration2::Decoration::bordersChanged, this,
[this]() {
GeometryUpdatesBlocker blocker(this);
RequestGeometryBlocker requestBlocker(this);
QRect oldgeom = geometry();
if (!isShade())
checkWorkspacePosition(oldgeom);
emit geometryShapeChanged(this, oldgeom);
}
);
}
setDecoration(decoration);
// TODO: ensure the new geometry still fits into the client area (e.g. maximized windows)
doSetGeometry(QRect(oldGeom.topLeft(), m_clientSize + (decoration ? QSize(decoration->borderLeft() + decoration->borderRight(),
decoration->borderBottom() + decoration->borderTop()) : QSize())));
emit geometryShapeChanged(this, oldGeom);
}
void ShellClient::updateDecoration(bool check_workspace_pos, bool force)
{
if (!force &&
((!isDecorated() && noBorder()) || (isDecorated() && !noBorder())))
return;
QRect oldgeom = geometry();
QRect oldClientGeom = oldgeom.adjusted(borderLeft(), borderTop(), -borderRight(), -borderBottom());
blockGeometryUpdates(true);
if (force)
destroyDecoration();
if (!noBorder()) {
createDecoration(oldgeom);
} else
destroyDecoration();
if (m_serverDecoration && isDecorated()) {
m_serverDecoration->setMode(KWayland::Server::ServerSideDecorationManagerInterface::Mode::Server);
}
if (m_xdgDecoration) {
auto mode = isDecorated() || m_userNoBorder ? XdgDecorationInterface::Mode::ServerSide: XdgDecorationInterface::Mode::ClientSide;
m_xdgDecoration->configure(mode);
if (m_requestGeometryBlockCounter == 0) {
m_xdgShellSurface->configure(xdgSurfaceStates(), m_requestedClientSize);
}
}
getShadow();
if (check_workspace_pos)
checkWorkspacePosition(oldgeom, -2, oldClientGeom);
blockGeometryUpdates(false);
}
void ShellClient::setGeometry(int x, int y, int w, int h, ForceGeometry_t force)
{
const QRect newGeometry = rules()->checkGeometry(QRect(x, y, w, h));
if (areGeometryUpdatesBlocked()) {
// when the GeometryUpdateBlocker exits the current geom is passed to setGeometry
// thus we need to set it here.
geom = newGeometry;
if (pendingGeometryUpdate() == PendingGeometryForced)
{} // maximum, nothing needed
else if (force == ForceGeometrySet)
setPendingGeometryUpdate(PendingGeometryForced);
else
setPendingGeometryUpdate(PendingGeometryNormal);
return;
}
if (pendingGeometryUpdate() != PendingGeometryNone) {
// reset geometry to the one before blocking, so that we can compare properly
geom = geometryBeforeUpdateBlocking();
}
const QSize requestedClientSize = newGeometry.size() - QSize(borderLeft() + borderRight(), borderTop() + borderBottom());
const QSize requestedWindowGeometrySize = toWindowGeometry(newGeometry.size());
if (requestedClientSize == m_clientSize && !isWaitingForMoveResizeSync() &&
(m_requestedClientSize.isEmpty() || requestedWindowGeometrySize == m_requestedClientSize)) {
// size didn't change, and we don't need to explicitly request a new size
doSetGeometry(newGeometry);
updateMaximizeMode(m_requestedMaximizeMode);
} else {
// size did change, Client needs to provide a new buffer
requestGeometry(newGeometry);
}
}
void ShellClient::doSetGeometry(const QRect &rect)
{
if (geom == rect && pendingGeometryUpdate() == PendingGeometryNone) {
return;
}
if (!m_unmapped) {
addWorkspaceRepaint(visibleRect());
}
geom = rect;
updateWindowRules(Rules::Position | Rules::Size);
if (m_unmapped && m_geomMaximizeRestore.isEmpty() && !geom.isEmpty()) {
// use first valid geometry as restore geometry
m_geomMaximizeRestore = geom;
}
if (!m_unmapped) {
addWorkspaceRepaint(visibleRect());
}
if (hasStrut()) {
workspace()->updateClientArea();
}
const auto old = geometryBeforeUpdateBlocking();
updateGeometryBeforeUpdateBlocking();
emit geometryShapeChanged(this, old);
if (isResize()) {
performMoveResize();
}
}
QByteArray ShellClient::windowRole() const
{
return QByteArray();
}
bool ShellClient::belongsToSameApplication(const AbstractClient *other, SameApplicationChecks checks) const
{
if (checks.testFlag(SameApplicationCheck::AllowCrossProcesses)) {
if (other->desktopFileName() == desktopFileName()) {
return true;
}
}
if (auto s = other->surface()) {
return s->client() == surface()->client();
}
return false;
}
void ShellClient::blockActivityUpdates(bool b)
{
Q_UNUSED(b)
}
void ShellClient::updateCaption()
{
const QString oldSuffix = m_captionSuffix;
const auto shortcut = shortcutCaptionSuffix();
m_captionSuffix = shortcut;
if ((!isSpecialWindow() || isToolbar()) && findClientWithSameCaption()) {
int i = 2;
do {
m_captionSuffix = shortcut + QLatin1String(" <") + QString::number(i) + QLatin1Char('>');
i++;
} while (findClientWithSameCaption());
}
if (m_captionSuffix != oldSuffix) {
emit captionChanged();
}
}
void ShellClient::closeWindow()
{
if (m_xdgShellSurface && isCloseable()) {
m_xdgShellSurface->close();
const qint32 pingSerial = static_cast<XdgShellInterface *>(m_xdgShellSurface->global())->ping(m_xdgShellSurface);
m_pingSerials.insert(pingSerial, PingReason::CloseWindow);
}
}
AbstractClient *ShellClient::findModal(bool allow_itself)
{
Q_UNUSED(allow_itself)
return nullptr;
}
bool ShellClient::isCloseable() const
{
if (m_windowType == NET::Desktop || m_windowType == NET::Dock) {
return false;
}
if (m_xdgShellSurface) {
return true;
}
return false;
}
bool ShellClient::isFullScreen() const
{
return m_fullScreen;
}
bool ShellClient::isMaximizable() const
{
if (!isResizable()) {
return false;
}
if (rules()->checkMaximize(MaximizeRestore) != MaximizeRestore || rules()->checkMaximize(MaximizeFull) != MaximizeFull) {
return false;
}
return true;
}
bool ShellClient::isMinimizable() const
{
if (!rules()->checkMinimize(true)) {
return false;
}
return (!m_plasmaShellSurface || m_plasmaShellSurface->role() == PlasmaShellSurfaceInterface::Role::Normal);
}
bool ShellClient::isMovable() const
{
if (rules()->checkPosition(invalidPoint) != invalidPoint) {
return false;
}
if (m_plasmaShellSurface) {
return m_plasmaShellSurface->role() == PlasmaShellSurfaceInterface::Role::Normal;
}
if (m_xdgShellPopup) {
return false;
}
return true;
}
bool ShellClient::isMovableAcrossScreens() const
{
if (rules()->checkPosition(invalidPoint) != invalidPoint) {
return false;
}
if (m_plasmaShellSurface) {
return m_plasmaShellSurface->role() == PlasmaShellSurfaceInterface::Role::Normal;
}
if (m_xdgShellPopup) {
return false;
}
return true;
}
bool ShellClient::isResizable() const
{
if (rules()->checkSize(QSize()).isValid()) {
return false;
}
if (m_plasmaShellSurface) {
return m_plasmaShellSurface->role() == PlasmaShellSurfaceInterface::Role::Normal;
}
if (m_xdgShellPopup) {
return false;
}
return true;
}
bool ShellClient::isShown(bool shaded_is_shown) const
{
Q_UNUSED(shaded_is_shown)
return !m_closing && !m_unmapped && !isMinimized() && !m_hidden;
}
void ShellClient::hideClient(bool hide)
{
if (m_hidden == hide) {
return;
}
m_hidden = hide;
if (hide) {
addWorkspaceRepaint(visibleRect());
workspace()->clientHidden(this);
emit windowHidden(this);
} else {
emit windowShown(this);
}
}
static bool changeMaximizeRecursion = false;
void ShellClient::changeMaximize(bool horizontal, bool vertical, bool adjust)
{
if (changeMaximizeRecursion) {
return;
}
if (!isResizable()) {
return;
}
const QRect clientArea = isElectricBorderMaximizing() ?
workspace()->clientArea(MaximizeArea, Cursor::pos(), desktop()) :
workspace()->clientArea(MaximizeArea, this);
const MaximizeMode oldMode = m_requestedMaximizeMode;
const QRect oldGeometry = geometry();
// 'adjust == true' means to update the size only, e.g. after changing workspace size
if (!adjust) {
if (vertical)
m_requestedMaximizeMode = MaximizeMode(m_requestedMaximizeMode ^ MaximizeVertical);
if (horizontal)
m_requestedMaximizeMode = MaximizeMode(m_requestedMaximizeMode ^ MaximizeHorizontal);
}
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();
}
setGeometry(workspace()->clientArea(MaximizeArea, this));
workspace()->raiseClient(this);
} else {
if (m_requestedMaximizeMode == MaximizeRestore) {
updateQuickTileMode(QuickTileFlag::None);
}
if (quickTileMode() != oldQuickTileMode) {
emit quickTileModeChanged();
}
if (m_geomMaximizeRestore.isValid()) {
setGeometry(m_geomMaximizeRestore);
} else {
setGeometry(workspace()->clientArea(PlacementArea, this));
}
}
}
MaximizeMode ShellClient::maximizeMode() const
{
return m_maximizeMode;
}
MaximizeMode ShellClient::requestedMaximizeMode() const
{
return m_requestedMaximizeMode;
}
bool ShellClient::noBorder() const
{
if (m_serverDecoration) {
if (m_serverDecoration->mode() == ServerSideDecorationManagerInterface::Mode::Server) {
return m_userNoBorder || isFullScreen();
}
}
if (m_xdgDecoration && m_xdgDecoration->requestedMode() != XdgDecorationInterface::Mode::ClientSide) {
return m_userNoBorder || isFullScreen();
}
return true;
}
bool ShellClient::isFullScreenable() const
{
if (!rules()->checkFullScreen(true)) {
return false;
}
return !isSpecialWindow();
}
void ShellClient::setFullScreen(bool set, bool user)
{
set = rules()->checkFullScreen(set);
const bool wasFullscreen = isFullScreen();
if (wasFullscreen == set) {
return;
}
if (isSpecialWindow()) {
return;
}
if (user && !userCanSetFullScreen()) {
return;
}
if (wasFullscreen) {
workspace()->updateFocusMousePosition(Cursor::pos()); // may cause leave event
} else {
// in shell surface, maximise mode and fullscreen are exclusive
// fullscreen->toplevel should restore the state we had before maximising
if (m_shellSurface && m_maximizeMode == MaximizeMode::MaximizeFull) {
m_geomFsRestore = m_geomMaximizeRestore;
} else {
m_geomFsRestore = geometry();
}
}
m_fullScreen = set;
if (set) {
untab();
workspace()->raiseClient(this);
}
RequestGeometryBlocker requestBlocker(this);
StackingUpdatesBlocker blocker1(workspace());
GeometryUpdatesBlocker blocker2(this);
workspace()->updateClientLayer(this); // active fullscreens get different layer
updateDecoration(false, false);
if (set) {
setGeometry(workspace()->clientArea(FullScreenArea, this));
} else {
if (m_geomFsRestore.isValid()) {
int currentScreen = screen();
setGeometry(QRect(m_geomFsRestore.topLeft(), adjustedSize(m_geomFsRestore.size())));
if( currentScreen != screen())
workspace()->sendClientToScreen( this, currentScreen );
} else {
// this can happen when the window was first shown already fullscreen,
// so let the client set the size by itself
setGeometry(QRect(workspace()->clientArea(PlacementArea, this).topLeft(), QSize(0, 0)));
}
}
updateWindowRules(Rules::Fullscreen|Rules::Position|Rules::Size);
emit fullScreenChanged();
}
void ShellClient::setNoBorder(bool set)
{
if (!userCanSetNoBorder()) {
return;
}
set = rules()->checkNoBorder(set);
if (m_userNoBorder == set) {
return;
}
m_userNoBorder = set;
updateDecoration(true, false);
updateWindowRules(Rules::NoBorder);
}
void ShellClient::setOnAllActivities(bool set)
{
Q_UNUSED(set)
}
void ShellClient::takeFocus()
{
if (rules()->checkAcceptFocus(wantsInput())) {
if (m_xdgShellSurface) {
const qint32 pingSerial = static_cast<XdgShellInterface *>(m_xdgShellSurface->global())->ping(m_xdgShellSurface);
m_pingSerials.insert(pingSerial, PingReason::FocusWindow);
}
setActive(true);
}
if (!keepAbove() && !isOnScreenDisplay() && !belongsToDesktop()) {
workspace()->setShowingDesktop(false);
}
}
void ShellClient::doSetActive()
{
if (!isActive()) {
return;
}
StackingUpdatesBlocker blocker(workspace());
workspace()->focusToNull();
}
bool ShellClient::userCanSetFullScreen() const
{
if (m_xdgShellSurface) {
return true;
}
return false;
}
bool ShellClient::userCanSetNoBorder() const
{
if (m_serverDecoration && m_serverDecoration->mode() == ServerSideDecorationManagerInterface::Mode::Server) {
return !isFullScreen() && !isShade() && !tabGroup();
}
if (m_xdgDecoration && m_xdgDecoration->requestedMode() != XdgDecorationInterface::Mode::ClientSide) {
return !isFullScreen() && !isShade() && !tabGroup();
}
return false;
}
bool ShellClient::wantsInput() const
{
return rules()->checkAcceptFocus(acceptsFocus());
}
bool ShellClient::acceptsFocus() const
{
if (waylandServer()->inputMethodConnection() == surface()->client()) {
return false;
}
if (m_plasmaShellSurface) {
if (m_plasmaShellSurface->role() == PlasmaShellSurfaceInterface::Role::OnScreenDisplay ||
m_plasmaShellSurface->role() == PlasmaShellSurfaceInterface::Role::ToolTip ||
m_plasmaShellSurface->role() == PlasmaShellSurfaceInterface::Role::Notification ||
m_plasmaShellSurface->role() == PlasmaShellSurfaceInterface::Role::CriticalNotification) {
return false;
}
}
if (m_closing) {
// a closing window does not accept focus
return false;
}
if (m_unmapped) {
// an unmapped window does not accept focus
return false;
}
if (m_shellSurface) {
if (m_shellSurface->isPopup()) {
return false;
}
return m_shellSurface->acceptsKeyboardFocus();
}
if (m_xdgShellSurface) {
// TODO: proper
return true;
}
return false;
}
void ShellClient::createWindowId()
{
if (!m_internal) {
m_windowId = waylandServer()->createWindowId(surface());
}
}
pid_t ShellClient::pid() const
{
return surface()->client()->processId();
}
bool ShellClient::isLockScreen() const
{
return surface()->client() == waylandServer()->screenLockerClientConnection();
}
bool ShellClient::isInputMethod() const
{
return surface()->client() == waylandServer()->inputMethodConnection();
}
bool ShellClient::requestGeometry(const QRect &rect)
{
if (m_requestGeometryBlockCounter != 0) {
m_blockedRequestGeometry = rect;
return false;
}
QSize size;
if (rect.isValid()) {
size = toWindowGeometry(rect.size());
} else {
size = QSize(0, 0);
}
m_requestedClientSize = size;
quint64 serialId = 0;
if (m_shellSurface && !size.isEmpty()) {
m_shellSurface->requestSize(size);
}
if (m_xdgShellSurface) {
serialId = m_xdgShellSurface->configure(xdgSurfaceStates(), size);
}
if (m_xdgShellPopup) {
auto parent = transientFor();
if (parent) {
const QPoint globalClientContentPos = parent->geometry().topLeft() + parent->clientPos();
const QPoint relativeOffset = rect.topLeft() - globalClientContentPos;
serialId = m_xdgShellPopup->configure(QRect(relativeOffset, size));
}
}
if (rect.isValid()) { //if there's no requested size, then there's implicity no positional information worth using
PendingConfigureRequest configureRequest;
configureRequest.serialId = serialId;
configureRequest.positionAfterResize = rect.topLeft();
configureRequest.maximizeMode = m_requestedMaximizeMode;
m_pendingConfigureRequests.append(configureRequest);
}
m_blockedRequestGeometry = QRect();
return true;
}
void ShellClient::updatePendingGeometry()
{
QPoint position = 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(geometry());
}
position = it->positionAfterResize;
maximizeMode = it->maximizeMode;
m_pendingConfigureRequests.erase(m_pendingConfigureRequests.begin(), ++it);
break;
}
//else serialId < m_lastAckedConfigureRequest and the state is now irrelevant and can be ignored
}
doSetGeometry(QRect(position, m_clientSize + QSize(borderLeft() + borderRight(), borderTop() + borderBottom())));
updateMaximizeMode(maximizeMode);
}
void ShellClient::clientFullScreenChanged(bool fullScreen)
{
setFullScreen(fullScreen, false);
}
void ShellClient::resizeWithChecks(int w, int h, ForceGeometry_t force)
{
Q_UNUSED(force)
QRect area = workspace()->clientArea(WorkArea, this);
// don't allow growing larger than workarea
if (w > area.width()) {
w = area.width();
}
if (h > area.height()) {
h = area.height();
}
if (m_shellSurface) {
m_shellSurface->requestSize(QSize(w, h));
}
if (m_xdgShellSurface) {
m_xdgShellSurface->configure(xdgSurfaceStates(), QSize(w, h));
}
}
void ShellClient::unmap()
{
m_unmapped = true;
if (isMoveResize()) {
leaveMoveResize();
}
m_requestedClientSize = QSize(0, 0);
destroyWindowManagementInterface();
if (Workspace::self()) {
addWorkspaceRepaint(visibleRect());
workspace()->clientHidden(this);
}
emit windowHidden(this);
}
void ShellClient::installPlasmaShellSurface(PlasmaShellSurfaceInterface *surface)
{
m_plasmaShellSurface = surface;
auto updatePosition = [this, surface] {
QRect rect = QRect(surface->position(), m_clientSize + QSize(borderLeft() + borderRight(), borderTop() + borderBottom()));
// Shell surfaces of internal windows are sometimes desync to current value.
// Make sure to not set window geometry of internal windows to invalid values (bug 386304).
// This is a workaround.
if (!m_internal || rect.isValid()) {
doSetGeometry(rect);
}
};
auto updateRole = [this, surface] {
NET::WindowType type = NET::Unknown;
switch (surface->role()) {
case PlasmaShellSurfaceInterface::Role::Desktop:
type = NET::Desktop;
break;
case PlasmaShellSurfaceInterface::Role::Panel:
type = NET::Dock;
break;
case PlasmaShellSurfaceInterface::Role::OnScreenDisplay:
type = NET::OnScreenDisplay;
break;
case PlasmaShellSurfaceInterface::Role::Notification:
type = NET::Notification;
break;
case PlasmaShellSurfaceInterface::Role::ToolTip:
type = NET::Tooltip;
break;
case PlasmaShellSurfaceInterface::Role::CriticalNotification:
type = NET::CriticalNotification;
break;
case PlasmaShellSurfaceInterface::Role::Normal:
default:
type = NET::Normal;
break;
}
if (type != m_windowType) {
m_windowType = type;
if (m_windowType == NET::Desktop || type == NET::Dock || type == NET::OnScreenDisplay || type == NET::Notification || type == NET::Tooltip || type == NET::CriticalNotification) {
setOnAllDesktops(true);
}
workspace()->updateClientArea();
}
};
connect(surface, &PlasmaShellSurfaceInterface::positionChanged, this, updatePosition);
connect(surface, &PlasmaShellSurfaceInterface::roleChanged, this, updateRole);
connect(surface, &PlasmaShellSurfaceInterface::panelBehaviorChanged, this,
[this] {
updateShowOnScreenEdge();
workspace()->updateClientArea();
}
);
connect(surface, &PlasmaShellSurfaceInterface::panelAutoHideHideRequested, this,
[this] {
hideClient(true);
m_plasmaShellSurface->hideAutoHidingPanel();
updateShowOnScreenEdge();
}
);
connect(surface, &PlasmaShellSurfaceInterface::panelAutoHideShowRequested, this,
[this] {
hideClient(false);
ScreenEdges::self()->reserve(this, ElectricNone);
m_plasmaShellSurface->showAutoHidingPanel();
}
);
updatePosition();
updateRole();
updateShowOnScreenEdge();
connect(this, &ShellClient::geometryChanged, this, &ShellClient::updateShowOnScreenEdge);
setSkipTaskbar(surface->skipTaskbar());
connect(surface, &PlasmaShellSurfaceInterface::skipTaskbarChanged, this, [this] {
setSkipTaskbar(m_plasmaShellSurface->skipTaskbar());
});
setSkipSwitcher(surface->skipSwitcher());
connect(surface, &PlasmaShellSurfaceInterface::skipSwitcherChanged, this, [this] {
setSkipSwitcher(m_plasmaShellSurface->skipSwitcher());
});
}
void ShellClient::updateShowOnScreenEdge()
{
if (!ScreenEdges::self()) {
return;
}
if (m_unmapped || !m_plasmaShellSurface || m_plasmaShellSurface->role() != PlasmaShellSurfaceInterface::Role::Panel) {
ScreenEdges::self()->reserve(this, ElectricNone);
return;
}
if ((m_plasmaShellSurface->panelBehavior() == PlasmaShellSurfaceInterface::PanelBehavior::AutoHide && m_hidden) ||
m_plasmaShellSurface->panelBehavior() == PlasmaShellSurfaceInterface::PanelBehavior::WindowsCanCover) {
// screen edge API requires an edge, thus we need to figure out which edge the window borders
const QRect clientGeometry = geometry();
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 ShellClient::isInitialPositionSet() const
{
if (m_plasmaShellSurface) {
return m_plasmaShellSurface->isPositionSet();
}
return false;
}
void ShellClient::installAppMenu(AppMenuInterface *menu)
{
m_appMenuInterface = menu;
auto updateMenu = [this](AppMenuInterface::InterfaceAddress address) {
updateApplicationMenuServiceName(address.serviceName);
updateApplicationMenuObjectPath(address.objectPath);
};
connect(m_appMenuInterface, &AppMenuInterface::addressChanged, this, [=](AppMenuInterface::InterfaceAddress address) {
updateMenu(address);
});
updateMenu(menu->address());
}
void ShellClient::installPalette(ServerSideDecorationPaletteInterface *palette)
{
m_paletteInterface = palette;
auto updatePalette = [this](const QString &palette) {
AbstractClient::updateColorScheme(rules()->checkDecoColor(palette));
};
connect(m_paletteInterface, &ServerSideDecorationPaletteInterface::paletteChanged, this, [=](const QString &palette) {
updatePalette(palette);
});
connect(m_paletteInterface, &QObject::destroyed, this, [=]() {
updatePalette(QString());
});
updatePalette(palette->palette());
}
void ShellClient::updateColorScheme()
{
if (m_paletteInterface) {
AbstractClient::updateColorScheme(rules()->checkDecoColor(m_paletteInterface->palette()));
} else {
AbstractClient::updateColorScheme(rules()->checkDecoColor(QString()));
}
}
void ShellClient::updateMaximizeMode(MaximizeMode maximizeMode)
{
if (maximizeMode == m_maximizeMode) {
return;
}
m_maximizeMode = maximizeMode;
updateWindowRules(Rules::MaximizeHoriz | Rules::MaximizeVert | Rules::Position | Rules::Size);
emit clientMaximizedStateChanged(this, m_maximizeMode);
emit clientMaximizedStateChanged(this, m_maximizeMode & MaximizeHorizontal, m_maximizeMode & MaximizeVertical);
}
bool ShellClient::hasStrut() const
{
if (!isShown(true)) {
return false;
}
if (!m_plasmaShellSurface) {
return false;
}
if (m_plasmaShellSurface->role() != PlasmaShellSurfaceInterface::Role::Panel) {
return false;
}
return m_plasmaShellSurface->panelBehavior() == PlasmaShellSurfaceInterface::PanelBehavior::AlwaysVisible;
}
void ShellClient::updateIcon()
{
const QString waylandIconName = QStringLiteral("wayland");
const QString dfIconName = iconFromDesktopFile();
const QString iconName = dfIconName.isEmpty() ? waylandIconName : dfIconName;
if (iconName == icon().name()) {
return;
}
setIcon(QIcon::fromTheme(iconName));
}
bool ShellClient::isTransient() const
{
return m_transient;
}
void ShellClient::setTransient()
{
SurfaceInterface *s = nullptr;
if (m_shellSurface) {
s = m_shellSurface->transientFor().data();
}
if (m_xdgShellSurface) {
if (auto transient = m_xdgShellSurface->transientFor().data()) {
s = transient->surface();
}
}
if (m_xdgShellPopup) {
s = m_xdgShellPopup->transientFor().data();
}
if (!s) {
s = waylandServer()->findForeignTransientForSurface(surface());
}
auto t = waylandServer()->findClient(s);
if (t != transientFor()) {
// remove from main client
if (transientFor())
transientFor()->removeTransient(this);
setTransientFor(t);
if (t) {
t->addTransient(this);
}
}
m_transient = (s != nullptr);
}
bool ShellClient::hasTransientPlacementHint() const
{
return isTransient() && transientFor() != nullptr &&
(m_shellSurface || m_xdgShellPopup);
}
QRect ShellClient::transientPlacement(const QRect &bounds) const
{
QRect anchorRect;
Qt::Edges anchorEdge;
Qt::Edges gravity;
QPoint offset;
PositionerConstraints constraintAdjustments;
QSize size = geometry().size();
const QPoint parentClientPos = transientFor()->pos() + transientFor()->clientPos();
QRect popupPosition;
// returns if a target is within the supplied bounds, optional edges argument states which side to check
auto inBounds = [bounds](const QRect &target, Qt::Edges edges = Qt::LeftEdge | Qt::RightEdge | Qt::TopEdge | Qt::BottomEdge) -> bool {
if (edges & Qt::LeftEdge && target.left() < bounds.left()) {
return false;
}
if (edges & Qt::TopEdge && target.top() < bounds.top()) {
return false;
}
if (edges & Qt::RightEdge && target.right() > bounds.right()) {
//normal QRect::right issue cancels out
return false;
}
if (edges & Qt::BottomEdge && target.bottom() > bounds.bottom()) {
return false;
}
return true;
};
if (m_shellSurface) {
anchorRect = QRect(m_shellSurface->transientOffset(), QSize(1,1));
anchorEdge = Qt::TopEdge | Qt::LeftEdge;
gravity = Qt::BottomEdge | Qt::RightEdge; //our single point represents the top left of the popup
constraintAdjustments = (PositionerConstraint::SlideX | PositionerConstraint::SlideY);
} else if (m_xdgShellPopup) {
anchorRect = m_xdgShellPopup->anchorRect();
anchorEdge = m_xdgShellPopup->anchorEdge();
gravity = m_xdgShellPopup->gravity();
offset = m_xdgShellPopup->anchorOffset();
constraintAdjustments = m_xdgShellPopup->constraintAdjustments();
if (!size.isValid()) {
size = m_xdgShellPopup->initialSize();
}
} else {
Q_UNREACHABLE();
}
//initial position
popupPosition = QRect(popupOffset(anchorRect, anchorEdge, gravity, size) + offset + parentClientPos, size);
//if that fits, we don't need to do anything
if (inBounds(popupPosition)) {
return popupPosition;
}
//otherwise apply constraint adjustment per axis in order XDG Shell Popup states
if (constraintAdjustments & PositionerConstraint::FlipX) {
if (!inBounds(popupPosition, Qt::LeftEdge | Qt::RightEdge)) {
//flip both edges (if either bit is set, XOR both)
auto flippedAnchorEdge = anchorEdge;
if (flippedAnchorEdge & (Qt::LeftEdge | Qt::RightEdge)) {
flippedAnchorEdge ^= (Qt::LeftEdge | Qt::RightEdge);
}
auto flippedGravity = gravity;
if (flippedGravity & (Qt::LeftEdge | Qt::RightEdge)) {
flippedGravity ^= (Qt::LeftEdge | Qt::RightEdge);
}
auto flippedPopupPosition = QRect(popupOffset(anchorRect, flippedAnchorEdge, flippedGravity, size) + offset + parentClientPos, size);
//if it still doesn't fit we should continue with the unflipped version
if (inBounds(flippedPopupPosition, Qt::LeftEdge | Qt::RightEdge)) {
popupPosition.moveLeft(flippedPopupPosition.x());
}
}
}
if (constraintAdjustments & PositionerConstraint::SlideX) {
if (!inBounds(popupPosition, Qt::LeftEdge)) {
popupPosition.moveLeft(bounds.x());
}
if (!inBounds(popupPosition, Qt::RightEdge)) {
// moveRight suffers from the classic QRect off by one issue
popupPosition.moveLeft(bounds.x() + bounds.width() - size.width());
}
}
if (constraintAdjustments & PositionerConstraint::ResizeX) {
//TODO
//but we need to sort out when this is run as resize should only happen before first configure
}
if (constraintAdjustments & PositionerConstraint::FlipY) {
if (!inBounds(popupPosition, Qt::TopEdge | Qt::BottomEdge)) {
//flip both edges (if either bit is set, XOR both)
auto flippedAnchorEdge = anchorEdge;
if (flippedAnchorEdge & (Qt::TopEdge | Qt::BottomEdge)) {
flippedAnchorEdge ^= (Qt::TopEdge | Qt::BottomEdge);
}
auto flippedGravity = gravity;
if (flippedGravity & (Qt::TopEdge | Qt::BottomEdge)) {
flippedGravity ^= (Qt::TopEdge | Qt::BottomEdge);
}
auto flippedPopupPosition = QRect(popupOffset(anchorRect, flippedAnchorEdge, flippedGravity, size) + offset + parentClientPos, size);
//if it still doesn't fit we should continue with the unflipped version
if (inBounds(flippedPopupPosition, Qt::TopEdge | Qt::BottomEdge)) {
popupPosition.moveTop(flippedPopupPosition.y());
}
}
}
if (constraintAdjustments & PositionerConstraint::SlideY) {
if (!inBounds(popupPosition, Qt::TopEdge)) {
popupPosition.moveTop(bounds.y());
}
if (!inBounds(popupPosition, Qt::BottomEdge)) {
popupPosition.moveTop(bounds.y() + bounds.height() - size.height());
}
}
if (constraintAdjustments & PositionerConstraint::ResizeY) {
//TODO
}
return popupPosition;
}
QPoint ShellClient::popupOffset(const QRect &anchorRect, const Qt::Edges anchorEdge, const Qt::Edges gravity, const QSize popupSize) const
{
QPoint anchorPoint;
switch (anchorEdge & (Qt::LeftEdge | Qt::RightEdge)) {
case Qt::LeftEdge:
anchorPoint.setX(anchorRect.x());
break;
case Qt::RightEdge:
anchorPoint.setX(anchorRect.x() + anchorRect.width());
break;
default:
anchorPoint.setX(qRound(anchorRect.x() + anchorRect.width() / 2.0));
}
switch (anchorEdge & (Qt::TopEdge | Qt::BottomEdge)) {
case Qt::TopEdge:
anchorPoint.setY(anchorRect.y());
break;
case Qt::BottomEdge:
anchorPoint.setY(anchorRect.y() + anchorRect.height());
break;
default:
anchorPoint.setY(qRound(anchorRect.y() + anchorRect.height() / 2.0));
}
// calculate where the top left point of the popup will end up with the applied gravity
// gravity indicates direction. i.e if gravitating towards the top the popup's bottom edge
// will next to the anchor point
QPoint popupPosAdjust;
switch (gravity & (Qt::LeftEdge | Qt::RightEdge)) {
case Qt::LeftEdge:
popupPosAdjust.setX(-popupSize.width());
break;
case Qt::RightEdge:
popupPosAdjust.setX(0);
break;
default:
popupPosAdjust.setX(qRound(-popupSize.width() / 2.0));
}
switch (gravity & (Qt::TopEdge | Qt::BottomEdge)) {
case Qt::TopEdge:
popupPosAdjust.setY(-popupSize.height());
break;
case Qt::BottomEdge:
popupPosAdjust.setY(0);
break;
default:
popupPosAdjust.setY(qRound(-popupSize.height() / 2.0));
}
return anchorPoint + popupPosAdjust;
}
bool ShellClient::isWaitingForMoveResizeSync() const
{
if (m_shellSurface) {
return !m_pendingConfigureRequests.isEmpty();
}
return false;
}
void ShellClient::doResizeSync()
{
requestGeometry(moveResizeGeometry());
}
QMatrix4x4 ShellClient::inputTransformation() const
{
QMatrix4x4 m = Toplevel::inputTransformation();
m.translate(-borderLeft(), -borderTop());
return m;
}
void ShellClient::installServerSideDecoration(KWayland::Server::ServerSideDecorationInterface *deco)
{
if (m_serverDecoration == deco) {
return;
}
m_serverDecoration = deco;
connect(m_serverDecoration, &ServerSideDecorationInterface::destroyed, this,
[this] {
m_serverDecoration = nullptr;
if (m_closing || !Workspace::self()) {
return;
}
if (!m_unmapped) {
// maybe delay to next event cycle in case the ShellClient is getting destroyed, too
updateDecoration(true);
}
}
);
if (!m_unmapped) {
updateDecoration(true);
}
connect(m_serverDecoration, &ServerSideDecorationInterface::modeRequested, this,
[this] (ServerSideDecorationManagerInterface::Mode mode) {
const bool changed = mode != m_serverDecoration->mode();
if (changed && !m_unmapped) {
updateDecoration(false);
}
}
);
}
void ShellClient::installXdgDecoration(XdgDecorationInterface *deco)
{
Q_ASSERT(m_xdgShellSurface);
m_xdgDecoration = deco;
connect(m_xdgDecoration, &QObject::destroyed, this,
[this] {
m_xdgDecoration = nullptr;
if (m_closing || !Workspace::self()) {
return;
}
updateDecoration(true);
}
);
connect(m_xdgDecoration, &XdgDecorationInterface::modeRequested, this,
[this] () {
//force is true as we must send a new configure response
updateDecoration(false, true);
});
}
bool ShellClient::shouldExposeToWindowManagement()
{
if (m_internal) {
return false;
}
if (isLockScreen()) {
return false;
}
if (m_xdgShellPopup) {
return false;
}
if (m_shellSurface) {
if (m_shellSurface->isTransient() && !m_shellSurface->acceptsKeyboardFocus()) {
return false;
}
}
return true;
}
KWayland::Server::XdgShellSurfaceInterface::States ShellClient::xdgSurfaceStates() const
{
XdgShellSurfaceInterface::States states;
if (isActive()) {
states |= XdgShellSurfaceInterface::State::Activated;
}
if (isFullScreen()) {
states |= XdgShellSurfaceInterface::State::Fullscreen;
}
if (m_requestedMaximizeMode == MaximizeMode::MaximizeFull) {
states |= XdgShellSurfaceInterface::State::Maximized;
}
if (isResize()) {
states |= XdgShellSurfaceInterface::State::Resizing;
}
return states;
}
void ShellClient::doMinimize()
{
if (isMinimized()) {
workspace()->clientHidden(this);
} else {
emit windowShown(this);
}
workspace()->updateMinimizedOfTransients(this);
}
bool ShellClient::setupCompositing()
{
if (m_compositingSetup) {
return true;
}
m_compositingSetup = Toplevel::setupCompositing();
return m_compositingSetup;
}
void ShellClient::finishCompositing(ReleaseReason releaseReason)
{
m_compositingSetup = false;
Toplevel::finishCompositing(releaseReason);
}
void ShellClient::placeIn(QRect &area)
{
Placement::self()->place(this, area);
setGeometryRestore(geometry());
}
void ShellClient::showOnScreenEdge()
{
if (!m_plasmaShellSurface || m_unmapped) {
return;
}
hideClient(false);
workspace()->raiseClient(this);
if (m_plasmaShellSurface->panelBehavior() == PlasmaShellSurfaceInterface::PanelBehavior::AutoHide) {
m_plasmaShellSurface->showAutoHidingPanel();
}
}
bool ShellClient::dockWantsInput() const
{
if (m_plasmaShellSurface) {
if (m_plasmaShellSurface->role() == PlasmaShellSurfaceInterface::Role::Panel) {
return m_plasmaShellSurface->panelTakesFocus();
}
}
return false;
}
void ShellClient::killWindow()
{
if (!surface()) {
return;
}
auto c = surface()->client();
if (c->processId() == getpid() || c->processId() == 0) {
c->destroy();
return;
}
::kill(c->processId(), SIGTERM);
// give it time to terminate and only if terminate fails, try destroy Wayland connection
QTimer::singleShot(5000, c, &ClientConnection::destroy);
}
bool ShellClient::hasPopupGrab() const
{
return m_hasPopupGrab;
}
void ShellClient::popupDone()
{
if (m_shellSurface) {
m_shellSurface->popupDone();
}
if (m_xdgShellPopup) {
m_xdgShellPopup->popupDone();
}
}
void ShellClient::updateClientOutputs()
{
QVector<OutputInterface*> clientOutputs;
const auto outputs = waylandServer()->display()->outputs();
for (OutputInterface* output: qAsConst(outputs)) {
const QRect outputGeom(output->globalPosition(), output->pixelSize() / output->scale());
if (geometry().intersects(outputGeom)) {
clientOutputs << output;
}
}
surface()->setOutputs(clientOutputs);
}
void ShellClient::updateWindowMargins()
{
QRect windowGeometry;
QSize clientSize = m_clientSize;
if (m_xdgShellSurface) {
windowGeometry = m_xdgShellSurface->windowGeometry();
} else if (m_xdgShellPopup) {
windowGeometry = m_xdgShellPopup->windowGeometry();
if (!clientSize.isValid()) {
clientSize = m_xdgShellPopup->initialSize();
}
} else {
return;
}
if (windowGeometry.isEmpty() ||
windowGeometry.width() > clientSize.width() ||
windowGeometry.height() > clientSize.height()) {
m_windowMargins = QMargins();
} else {
m_windowMargins = QMargins(windowGeometry.left(),
windowGeometry.top(),
clientSize.width() - (windowGeometry.right() + 1),
clientSize.height() - (windowGeometry.bottom() + 1));
}
}
bool ShellClient::isPopupWindow() const
{
if (Toplevel::isPopupWindow()) {
return true;
}
if (m_shellSurface != nullptr) {
return m_shellSurface->isPopup();
}
if (m_xdgShellPopup != nullptr) {
return true;
}
return false;
}
QWindow *ShellClient::internalWindow() const
{
return nullptr;
}
bool ShellClient::supportsWindowRules() const
{
if (m_plasmaShellSurface) {
return false;
}
return m_xdgShellSurface;
}
}