kwin/src/sm.cpp
Aleix Pol Gonzalez bf1ce85474 Make it possible to build KWin without libxcb
Now that we have Wayland around, there's a whole branch of dependencies
that shouldn't be necessary anymore.
This allows to build KWin without all of it, allowing us to have a much
more compact alignment for cases where all the legacy software isn't
necessary anymore.

Bundle KWindowSystem X11-specific headers into it too, since it's part
of the same process.

Signed-off-by: Victoria Fischer <victoria.fischer@mbition.io>
2024-02-28 16:03:50 +00:00

510 lines
19 KiB
C++

/*
KWin - the KDE window manager
This file is part of the KDE project.
SPDX-FileCopyrightText: 1999, 2000 Matthias Ettrich <ettrich@kde.org>
SPDX-FileCopyrightText: 2003 Lubos Lunak <l.lunak@kde.org>
SPDX-License-Identifier: GPL-2.0-or-later
*/
#include "sm.h"
#include <cstdlib>
#include <kconfig.h>
#include <pwd.h>
#include <unistd.h>
#include "virtualdesktops.h"
#include "wayland_server.h"
#include "workspace.h"
#include "xdgshellwindow.h"
#include <QDebug>
#if KWIN_BUILD_X11
#include "x11window.h"
#endif
#include <QSessionManager>
#if KWIN_BUILD_NOTIFICATIONS
#include <KLocalizedString>
#include <KNotification>
#include <KService>
#endif
#include "sessionadaptor.h"
using namespace Qt::StringLiterals;
namespace KWin
{
static KConfig *sessionConfig(QString id, QString key)
{
static KConfig *config = nullptr;
static QString lastId;
static QString lastKey;
static QString pattern = QString(QLatin1String("session/%1_%2_%3")).arg(qApp->applicationName());
if (id != lastId || key != lastKey) {
delete config;
config = nullptr;
}
lastId = id;
lastKey = key;
if (!config) {
config = new KConfig(pattern.arg(id, key), KConfig::SimpleConfig);
}
return config;
}
static const char *const window_type_names[] = {
"Unknown", "Normal", "Desktop", "Dock", "Toolbar", "Menu", "Dialog",
"Override", "TopMenu", "Utility", "Splash"};
// change also the two functions below when adding new entries
static const char *windowTypeToTxt(WindowType type)
{
if (type >= WindowType::Unknown && type <= WindowType::Splash) {
return window_type_names[int(type) + 1]; // +1 (unknown==-1)
}
if (type == WindowType::Undefined) { // undefined (not really part of WindowType)
return "Undefined";
}
qFatal("Unknown Window Type");
return nullptr;
}
static WindowType txtToWindowType(const char *txt)
{
for (int i = int(WindowType::Unknown); i <= int(WindowType::Splash); ++i) {
if (qstrcmp(txt, window_type_names[i + 1]) == 0) { // +1
return static_cast<WindowType>(i);
}
}
return WindowType::Undefined;
}
/**
* Stores the current session in the config file
*
* @see loadSessionInfo
*/
void SessionManager::storeSession(const QString &sessionName, SMSavePhase phase)
{
qCDebug(KWIN_CORE) << "storing session" << sessionName << "in phase" << phase;
KConfig *config = sessionConfig(sessionName, QString());
KConfigGroup cg(config, QStringLiteral("Session"));
int count = 0;
int active_client = -1;
#if KWIN_BUILD_X11
const QList<Window *> windows = workspace()->windows();
for (auto it = windows.begin(); it != windows.end(); ++it) {
X11Window *c = qobject_cast<X11Window *>(*it);
if (!c || c->isUnmanaged()) {
continue;
}
if (c->windowType() > WindowType::Splash) {
// window types outside this are not tooltips/menus/OSDs
// typically these will be unmanaged and not in this list anyway, but that is not enforced
continue;
}
QByteArray sessionId = c->sessionId();
QString wmCommand = c->wmCommand();
if (sessionId.isEmpty()) {
// remember also applications that are not XSMP capable
// and use the obsolete WM_COMMAND / WM_SAVE_YOURSELF
if (wmCommand.isEmpty()) {
continue;
}
}
count++;
if (c->isActive()) {
active_client = count;
}
if (phase == SMSavePhase2 || phase == SMSavePhase2Full) {
storeClient(cg, count, c);
}
}
#endif
if (phase == SMSavePhase0) {
// it would be much simpler to save these values to the config file,
// but both Qt and KDE treat phase1 and phase2 separately,
// which results in different sessionkey and different config file :(
m_sessionActiveClient = active_client;
m_sessionDesktop = VirtualDesktopManager::self()->current();
} else if (phase == SMSavePhase2) {
cg.writeEntry("count", count);
cg.writeEntry("active", m_sessionActiveClient);
cg.writeEntry("desktop", m_sessionDesktop);
} else { // SMSavePhase2Full
cg.writeEntry("count", count);
cg.writeEntry("active", m_sessionActiveClient);
cg.writeEntry("desktop", VirtualDesktopManager::self()->current());
}
config->sync(); // it previously did some "revert to defaults" stuff for phase1 I think
}
#if KWIN_BUILD_X11
void SessionManager::storeClient(KConfigGroup &cg, int num, X11Window *c)
{
c->setSessionActivityOverride(false); // make sure we get the real values
QString n = QString::number(num);
cg.writeEntry(QLatin1String("sessionId") + n, c->sessionId().constData());
cg.writeEntry(QLatin1String("windowRole") + n, c->windowRole());
cg.writeEntry(QLatin1String("wmCommand") + n, c->wmCommand());
cg.writeEntry(QLatin1String("resourceName") + n, c->resourceName());
cg.writeEntry(QLatin1String("resourceClass") + n, c->resourceClass());
cg.writeEntry(QLatin1String("geometry") + n, QRectF(c->calculateGravitation(true), c->clientSize()).toRect()); // FRAME
cg.writeEntry(QLatin1String("restore") + n, c->geometryRestore());
cg.writeEntry(QLatin1String("fsrestore") + n, c->fullscreenGeometryRestore());
cg.writeEntry(QLatin1String("maximize") + n, (int)c->maximizeMode());
cg.writeEntry(QLatin1String("fullscreen") + n, (int)c->fullScreenMode());
cg.writeEntry(QLatin1String("desktop") + n, c->desktopId());
// the config entry is called "iconified" for back. comp. reasons
// (kconf_update script for updating session files would be too complicated)
cg.writeEntry(QLatin1String("iconified") + n, c->isMinimized());
cg.writeEntry(QLatin1String("opacity") + n, c->opacity());
// the config entry is called "sticky" for back. comp. reasons
cg.writeEntry(QLatin1String("sticky") + n, c->isOnAllDesktops());
cg.writeEntry(QLatin1String("shaded") + n, c->isShade());
// the config entry is called "staysOnTop" for back. comp. reasons
cg.writeEntry(QLatin1String("staysOnTop") + n, c->keepAbove());
cg.writeEntry(QLatin1String("keepBelow") + n, c->keepBelow());
cg.writeEntry(QLatin1String("skipTaskbar") + n, c->originalSkipTaskbar());
cg.writeEntry(QLatin1String("skipPager") + n, c->skipPager());
cg.writeEntry(QLatin1String("skipSwitcher") + n, c->skipSwitcher());
// not really just set by user, but name kept for back. comp. reasons
cg.writeEntry(QLatin1String("userNoBorder") + n, c->userNoBorder());
cg.writeEntry(QLatin1String("windowType") + n, windowTypeToTxt(c->windowType()));
cg.writeEntry(QLatin1String("shortcut") + n, c->shortcut().toString());
cg.writeEntry(QLatin1String("stackingOrder") + n, workspace()->unconstrainedStackingOrder().indexOf(c));
cg.writeEntry(QLatin1String("activities") + n, c->activities());
}
#endif
#if KWIN_BUILD_X11
void SessionManager::storeSubSession(const QString &name, QSet<QByteArray> sessionIds)
{
// TODO clear it first
KConfigGroup cg(KSharedConfig::openConfig(), QLatin1String("SubSession: ") + name);
int count = 0;
int active_client = -1;
const QList<Window *> windows = workspace()->windows();
for (auto it = windows.begin(); it != windows.end(); ++it) {
X11Window *c = qobject_cast<X11Window *>(*it);
if (!c || c->isUnmanaged()) {
continue;
}
if (c->windowType() > WindowType::Splash) {
continue;
}
QByteArray sessionId = c->sessionId();
QString wmCommand = c->wmCommand();
if (sessionId.isEmpty()) {
// remember also applications that are not XSMP capable
// and use the obsolete WM_COMMAND / WM_SAVE_YOURSELF
if (wmCommand.isEmpty()) {
continue;
}
}
if (!sessionIds.contains(sessionId)) {
continue;
}
qCDebug(KWIN_CORE) << "storing" << sessionId;
count++;
if (c->isActive()) {
active_client = count;
}
storeClient(cg, count, c);
}
cg.writeEntry("count", count);
cg.writeEntry("active", active_client);
// cg.writeEntry( "desktop", currentDesktop());
}
#endif
/**
* Loads the session information from the config file.
*
* @see storeSession
*/
void SessionManager::loadSession(const QString &sessionName)
{
session.clear();
KConfigGroup cg(sessionConfig(sessionName, QString()), QStringLiteral("Session"));
Q_EMIT loadSessionRequested(sessionName);
addSessionInfo(cg);
}
void SessionManager::addSessionInfo(KConfigGroup &cg)
{
workspace()->setInitialDesktop(cg.readEntry("desktop", 1));
int count = cg.readEntry("count", 0);
int active_client = cg.readEntry("active", 0);
for (int i = 1; i <= count; i++) {
QString n = QString::number(i);
SessionInfo *info = new SessionInfo;
session.append(info);
info->sessionId = cg.readEntry(QLatin1String("sessionId") + n, QString()).toLatin1();
info->windowRole = cg.readEntry(QLatin1String("windowRole") + n, QString());
info->wmCommand = cg.readEntry(QLatin1String("wmCommand") + n, QString()).toLatin1();
info->resourceName = cg.readEntry(QLatin1String("resourceName") + n, QString());
info->resourceClass = cg.readEntry(QLatin1String("resourceClass") + n, QString()).toLower();
info->geometry = cg.readEntry(QLatin1String("geometry") + n, QRect());
info->restore = cg.readEntry(QLatin1String("restore") + n, QRect());
info->fsrestore = cg.readEntry(QLatin1String("fsrestore") + n, QRect());
info->maximized = cg.readEntry(QLatin1String("maximize") + n, 0);
info->fullscreen = cg.readEntry(QLatin1String("fullscreen") + n, 0);
info->desktop = cg.readEntry(QLatin1String("desktop") + n, 0);
info->minimized = cg.readEntry(QLatin1String("iconified") + n, false);
info->opacity = cg.readEntry(QLatin1String("opacity") + n, 1.0);
info->onAllDesktops = cg.readEntry(QLatin1String("sticky") + n, false);
info->shaded = cg.readEntry(QLatin1String("shaded") + n, false);
info->keepAbove = cg.readEntry(QLatin1String("staysOnTop") + n, false);
info->keepBelow = cg.readEntry(QLatin1String("keepBelow") + n, false);
info->skipTaskbar = cg.readEntry(QLatin1String("skipTaskbar") + n, false);
info->skipPager = cg.readEntry(QLatin1String("skipPager") + n, false);
info->skipSwitcher = cg.readEntry(QLatin1String("skipSwitcher") + n, false);
info->noBorder = cg.readEntry(QLatin1String("userNoBorder") + n, false);
info->windowType = txtToWindowType(cg.readEntry(QLatin1String("windowType") + n, QString()).toLatin1().constData());
info->shortcut = cg.readEntry(QLatin1String("shortcut") + n, QString());
info->active = (active_client == i);
info->stackingOrder = cg.readEntry(QLatin1String("stackingOrder") + n, -1);
info->activities = cg.readEntry(QLatin1String("activities") + n, QStringList());
}
}
void SessionManager::loadSubSessionInfo(const QString &name)
{
KConfigGroup cg(KSharedConfig::openConfig(), QLatin1String("SubSession: ") + name);
addSessionInfo(cg);
}
#if KWIN_BUILD_X11
static bool sessionInfoWindowTypeMatch(X11Window *c, SessionInfo *info)
{
if (int(info->windowType) == -2) {
// undefined (not really part of NET::WindowType)
return !c->isSpecialWindow();
}
return info->windowType == c->windowType();
}
/**
* Returns a SessionInfo for client \a c. The returned session
* info is removed from the storage. It's up to the caller to delete it.
*
* This function is called when a new window is mapped and must be managed.
* We try to find a matching entry in the session.
*
* May return 0 if there's no session info for the client.
*/
SessionInfo *SessionManager::takeSessionInfo(X11Window *c)
{
SessionInfo *realInfo = nullptr;
QByteArray sessionId = c->sessionId();
QString windowRole = c->windowRole();
QString wmCommand = c->wmCommand();
QString resourceName = c->resourceName();
QString resourceClass = c->resourceClass();
// First search ``session''
if (!sessionId.isEmpty()) {
// look for a real session managed client (algorithm suggested by ICCCM)
for (SessionInfo *info : std::as_const(session)) {
if (realInfo) {
break;
}
if (info->sessionId == sessionId && sessionInfoWindowTypeMatch(c, info)) {
if (!windowRole.isEmpty()) {
if (info->windowRole == windowRole) {
realInfo = info;
session.removeAll(info);
}
} else {
if (info->windowRole.isEmpty()
&& info->resourceName == resourceName
&& info->resourceClass == resourceClass) {
realInfo = info;
session.removeAll(info);
}
}
}
}
} else {
// look for a sessioninfo with matching features.
for (SessionInfo *info : std::as_const(session)) {
if (realInfo) {
break;
}
if (info->resourceName == resourceName
&& info->resourceClass == resourceClass
&& sessionInfoWindowTypeMatch(c, info)) {
if (wmCommand.isEmpty() || info->wmCommand == wmCommand) {
realInfo = info;
session.removeAll(info);
}
}
}
}
return realInfo;
}
#endif
SessionManager::SessionManager(QObject *parent)
: QObject(parent)
{
new SessionAdaptor(this);
QDBusConnection::sessionBus().registerObject(QStringLiteral("/Session"), this);
}
SessionManager::~SessionManager()
{
qDeleteAll(session);
}
SessionState SessionManager::state() const
{
return m_sessionState;
}
void SessionManager::setState(uint state)
{
switch (state) {
case 0:
setState(SessionState::Saving);
break;
case 1:
setState(SessionState::Quitting);
break;
default:
setState(SessionState::Normal);
}
}
// TODO should we rethink this now that we have dedicated start end end save methods?
void SessionManager::setState(SessionState state)
{
if (state == m_sessionState) {
return;
}
// If we're starting to save a session
if (state == SessionState::Saving) {
workspace()->rulebook()->setUpdatesDisabled(true);
}
// If we're ending a save session due to either completion or cancellation
if (m_sessionState == SessionState::Saving) {
workspace()->rulebook()->setUpdatesDisabled(false);
#if KWIN_BUILD_X11
Workspace::self()->forEachClient([](X11Window *client) {
client->setSessionActivityOverride(false);
});
#endif
}
m_sessionState = state;
Q_EMIT stateChanged();
}
void SessionManager::aboutToSaveSession(const QString &name)
{
Q_EMIT prepareSessionSaveRequested(name);
storeSession(name, SMSavePhase0);
}
void SessionManager::finishSaveSession(const QString &name)
{
Q_EMIT finishSessionSaveRequested(name);
storeSession(name, SMSavePhase2);
}
bool SessionManager::closeWaylandWindows()
{
Q_ASSERT(calledFromDBus());
if (!waylandServer()) {
return true;
}
if (m_closingWindowsGuard) {
sendErrorReply(QDBusError::Failed, u"Operation already in progress"_s);
return false;
}
m_closingWindowsGuard = std::make_unique<QObject>();
qCDebug(KWIN_CORE) << "Closing windows";
auto dbusMessage = message();
setDelayedReply(true);
const auto windows = workspace()->windows();
m_pendingWindows.clear();
m_pendingWindows.reserve(windows.size());
for (const auto window : windows) {
if (auto toplevelWindow = qobject_cast<XdgToplevelWindow *>(window)) {
connect(toplevelWindow, &XdgToplevelWindow::closed, m_closingWindowsGuard.get(), [this, toplevelWindow, dbusMessage] {
m_pendingWindows.removeOne(toplevelWindow);
if (m_pendingWindows.empty()) {
m_closeTimer.stop();
m_closingWindowsGuard.reset();
QDBusConnection::sessionBus().send(dbusMessage.createReply(true));
}
});
m_pendingWindows.push_back(toplevelWindow);
toplevelWindow->closeWindow();
}
}
if (m_pendingWindows.empty()) {
m_closingWindowsGuard.reset();
QDBusConnection::sessionBus().send(dbusMessage.createReply(true));
return true;
}
m_closeTimer.start(std::chrono::seconds(10));
m_closeTimer.setSingleShot(true);
connect(&m_closeTimer, &QTimer::timeout, m_closingWindowsGuard.get(), [this, dbusMessage] {
#if KWIN_BUILD_NOTIFICATIONS
QStringList apps;
apps.reserve(m_pendingWindows.size());
std::transform(m_pendingWindows.cbegin(), m_pendingWindows.cend(), std::back_inserter(apps), [](const XdgToplevelWindow *window) -> QString {
const auto service = KService::serviceByDesktopName(window->desktopFileName());
return QChar(u'') + (service ? service->name() : window->caption());
});
apps.removeDuplicates();
qCDebug(KWIN_CORE) << "Not closed windows" << apps;
auto notification = new KNotification("cancellogout", KNotification::DefaultEvent | KNotification::Persistent);
notification->setText(i18n("The following applications did not close:\n%1", apps.join('\n')));
auto cancel = notification->addAction(i18nc("@action:button", "Cancel Logout"));
auto quit = notification->addAction(i18nc("@action::button", "Log Out Anyway"));
connect(cancel, &KNotificationAction::activated, m_closingWindowsGuard.get(), [dbusMessage, this] {
m_closingWindowsGuard.reset();
QDBusConnection::sessionBus().send(dbusMessage.createReply(false));
});
connect(quit, &KNotificationAction::activated, m_closingWindowsGuard.get(), [dbusMessage, this] {
m_closingWindowsGuard.reset();
QDBusConnection::sessionBus().send(dbusMessage.createReply(true));
});
connect(notification, &KNotification::closed, m_closingWindowsGuard.get(), [dbusMessage, this] {
m_closingWindowsGuard.reset();
QDBusConnection::sessionBus().send(dbusMessage.createReply(false));
});
notification->sendEvent();
#else
m_closingWindowsGuard.reset();
QDBusConnection::sessionBus().send(dbusMessage.createReply(false));
#endif
});
return true;
}
void SessionManager::quit()
{
qApp->quit();
}
} // namespace
#include "moc_sm.cpp"