/* KWin - the KDE window manager This file is part of the KDE project. SPDX-FileCopyrightText: 1999, 2000 Matthias Ettrich SPDX-FileCopyrightText: 2003 Lubos Lunak SPDX-License-Identifier: GPL-2.0-or-later */ #include "sm.h" #include #include #include #include #include "virtualdesktops.h" #include "wayland_server.h" #include "workspace.h" #include "xdgshellwindow.h" #include #if KWIN_BUILD_X11 #include "x11window.h" #endif #include #if KWIN_BUILD_NOTIFICATIONS #include #include #include #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(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 windows = workspace()->windows(); for (auto it = windows.begin(); it != windows.end(); ++it) { X11Window *c = qobject_cast(*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 sessionIds) { // TODO clear it first KConfigGroup cg(KSharedConfig::openConfig(), QLatin1String("SubSession: ") + name); int count = 0; int active_client = -1; const QList windows = workspace()->windows(); for (auto it = windows.begin(); it != windows.end(); ++it) { X11Window *c = qobject_cast(*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(); 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(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"