/* KWin - the KDE window manager This file is part of the KDE project. SPDX-FileCopyrightText: 2009 Martin Gräßlin SPDX-FileCopyrightText: 2020 Benjamin Port SPDX-License-Identifier: GPL-2.0-or-later */ #include "windowsrunnerinterface.h" #include "abstract_client.h" #include "workspace.h" #include "krunner1adaptor.h" #include namespace KWin { WindowsRunner::WindowsRunner(QObject *parent) : Plugin(parent) { if (workspace()) { initialize(); } else { connect(kwinApp(), &Application::workspaceCreated, this, &WindowsRunner::initialize); } } WindowsRunner::~WindowsRunner() { } void WindowsRunner::initialize() { new Krunner1Adaptor(this); qDBusRegisterMetaType(); qDBusRegisterMetaType(); qDBusRegisterMetaType(); qDBusRegisterMetaType(); qDBusRegisterMetaType(); QDBusConnection::sessionBus().registerObject(QStringLiteral("/WindowsRunner"), this); QDBusConnection::sessionBus().registerService(QStringLiteral("org.kde.KWin")); } RemoteActions WindowsRunner::Actions() { RemoteActions actions; return actions; } RemoteMatches WindowsRunner::Match(const QString &searchTerm) { RemoteMatches matches; auto term = searchTerm; WindowsRunnerAction action = ActivateAction; if (term.endsWith(i18nc("Note this is a KRunner keyword", "activate") , Qt::CaseInsensitive)) { action = ActivateAction; term = term.left(term.lastIndexOf(i18nc("Note this is a KRunner keyword", "activate")) - 1); } else if (term.endsWith(i18nc("Note this is a KRunner keyword", "close") , Qt::CaseInsensitive)) { action = CloseAction; term = term.left(term.lastIndexOf(i18nc("Note this is a KRunner keyword", "close")) - 1); } else if (term.endsWith(i18nc("Note this is a KRunner keyword", "min") , Qt::CaseInsensitive)) { action = MinimizeAction; term = term.left(term.lastIndexOf(i18nc("Note this is a KRunner keyword", "min")) - 1); } else if (term.endsWith(i18nc("Note this is a KRunner keyword", "minimize") , Qt::CaseInsensitive)) { action = MinimizeAction; term = term.left(term.lastIndexOf(i18nc("Note this is a KRunner keyword", "minimize")) - 1); } else if (term.endsWith(i18nc("Note this is a KRunner keyword", "max") , Qt::CaseInsensitive)) { action = MaximizeAction; term = term.left(term.lastIndexOf(i18nc("Note this is a KRunner keyword", "max")) - 1); } else if (term.endsWith(i18nc("Note this is a KRunner keyword", "maximize") , Qt::CaseInsensitive)) { action = MaximizeAction; term = term.left(term.lastIndexOf(i18nc("Note this is a KRunner keyword", "maximize")) - 1); } else if (term.endsWith(i18nc("Note this is a KRunner keyword", "fullscreen") , Qt::CaseInsensitive)) { action = FullscreenAction; term = term.left(term.lastIndexOf(i18nc("Note this is a KRunner keyword", "fullscreen")) - 1); } else if (term.endsWith(i18nc("Note this is a KRunner keyword", "shade") , Qt::CaseInsensitive)) { action = ShadeAction; term = term.left(term.lastIndexOf(i18nc("Note this is a KRunner keyword", "shade")) - 1); } else if (term.endsWith(i18nc("Note this is a KRunner keyword", "keep above") , Qt::CaseInsensitive)) { action = KeepAboveAction; term = term.left(term.lastIndexOf(i18nc("Note this is a KRunner keyword", "keep above")) - 1); } else if (term.endsWith(i18nc("Note this is a KRunner keyword", "keep below") , Qt::CaseInsensitive)) { action = KeepBelowAction; term = term.left(term.lastIndexOf(i18nc("Note this is a KRunner keyword", "keep below")) - 1); } // keyword match: when term starts with "window" we list all windows // the list can be restricted to windows matching a given name, class, role or desktop if (term.startsWith(i18nc("Note this is a KRunner keyword", "window") , Qt::CaseInsensitive)) { const QStringList keywords = term.split(QLatin1Char(' ')); QString windowName; QString windowAppName; VirtualDesktop *targetDesktop = nullptr; QVariant desktopId; for (const QString& keyword : keywords) { if (keyword.endsWith(QLatin1Char('='))) { continue; } if (keyword.startsWith(i18nc("Note this is a KRunner keyword", "name") + QStringLiteral("=") , Qt::CaseInsensitive)) { windowName = keyword.split(QStringLiteral("="))[1]; } else if (keyword.startsWith(i18nc("Note this is a KRunner keyword", "appname") + QStringLiteral("=") , Qt::CaseInsensitive)) { windowAppName = keyword.split(QStringLiteral("="))[1]; } else if (keyword.startsWith(i18nc("Note this is a KRunner keyword", "desktop") + QStringLiteral("=") , Qt::CaseInsensitive)) { desktopId = keyword.split(QStringLiteral("="))[1]; for (const auto desktop : VirtualDesktopManager::self()->desktops()) { if (desktop->name().contains(desktopId.toString(), Qt::CaseInsensitive) || desktop->x11DesktopNumber() == desktopId.toUInt()) { targetDesktop = desktop; } } } else { // not a keyword - use as name if name is unused, but another option is set if (windowName.isEmpty() && !keyword.contains(QLatin1Char('=')) && (!windowAppName.isEmpty() || targetDesktop)) { windowName = keyword; } } } for (const AbstractClient *client : Workspace::self()->allClientList()) { if (!client->isNormalWindow()) { continue; } const QString appName = client->resourceClass(); const QString name = client->caption(); if (!windowName.isEmpty() && !name.startsWith(windowName, Qt::CaseInsensitive)) { continue; } if (!windowAppName.isEmpty() && !appName.contains(windowAppName, Qt::CaseInsensitive)) { continue; } if (targetDesktop && !client->desktops().contains(targetDesktop) && !client->isOnAllDesktops()) { continue; } // check for windows when no keywords were used // check the name and app name for containing the query without the keyword if (windowName.isEmpty() && windowAppName.isEmpty() && !targetDesktop) { const QString& test = term.mid(keywords[0].length() + 1); if (!name.contains(test, Qt::CaseInsensitive) && !appName.contains(test, Qt::CaseInsensitive)) { continue; } } // blacklisted everything else: we have a match if (actionSupported(client, action)){ matches << windowsMatch(client, action); } } if (!matches.isEmpty()) { // the window keyword found matches - do not process other syntax possibilities return matches; } } bool desktopAdded = false; // check for desktop keyword if (term.startsWith(i18nc("Note this is a KRunner keyword", "desktop") , Qt::CaseInsensitive)) { const QStringList parts = term.split(QLatin1Char(' ')); if (parts.size() == 1) { // only keyword - list all desktops for (auto desktop : VirtualDesktopManager::self()->desktops()) { matches << desktopMatch(desktop); desktopAdded = true; } } } // check for matching desktops by name for (const AbstractClient *client : Workspace::self()->allClientList()) { if (!client->isNormalWindow()) { continue; } const QString appName = client->resourceClass(); const QString name = client->caption(); if (name.startsWith(term, Qt::CaseInsensitive) || appName.startsWith(term, Qt::CaseInsensitive)) { matches << windowsMatch(client, action, 0.8, Plasma::QueryMatch::ExactMatch); } else if ((name.contains(term, Qt::CaseInsensitive) || appName.contains(term, Qt::CaseInsensitive)) && actionSupported(client, action)) { matches << windowsMatch(client, action, 0.7, Plasma::QueryMatch::PossibleMatch); } } for (auto *desktop : VirtualDesktopManager::self()->desktops()) { if (desktop->name().contains(term, Qt::CaseInsensitive)) { if (!desktopAdded && desktop != VirtualDesktopManager::self()->currentDesktop()) { matches << desktopMatch(desktop, ActivateDesktopAction, 0.8); } // search for windows on desktop and list them with less relevance for (const AbstractClient *client : Workspace::self()->allClientList()) { if (!client->isNormalWindow()) { continue; } if ((client->desktops().contains(desktop) || client->isOnAllDesktops()) && actionSupported(client, action)) { matches << windowsMatch(client, action, 0.5, Plasma::QueryMatch::PossibleMatch); } } } } return matches; } void WindowsRunner::Run(const QString &id, const QString &actionId) { // Split id to get actionId and realId. We don't use actionId because our actions list is not constant const QStringList parts = id.split(QLatin1Char('_')); auto action = WindowsRunnerAction(parts[0].toInt()); auto objectId = parts[1]; if (action == ActivateDesktopAction) { QByteArray desktopId = objectId.toLocal8Bit(); auto desktop = VirtualDesktopManager::self()->desktopForId(desktopId); VirtualDesktopManager::self()->setCurrent(desktop); return; } const auto uuid = QUuid::fromString(objectId); const auto client = workspace()->findAbstractClient(uuid); switch (action) { case ActivateAction: workspace()->activateClient(client); break; case CloseAction: client->closeWindow(); break; case MinimizeAction: client->setMinimized(!client->isMinimized()); break; case MaximizeAction: client->setMaximize(client->maximizeMode() == MaximizeRestore, client->maximizeMode() == MaximizeRestore); break; case FullscreenAction: client->setFullScreen(!client->isFullScreen()); break; case ShadeAction: client->toggleShade(); break; case KeepAboveAction: client->setKeepAbove(!client->keepAbove()); break; case KeepBelowAction: client->setKeepBelow(!client->keepBelow()); break; } } RemoteMatch WindowsRunner::desktopMatch(const VirtualDesktop *desktop, const WindowsRunnerAction action, qreal relevance) const { RemoteMatch match; match.id = QString::number((int)action) + QLatin1Char('_') + desktop->id(); match.type = Plasma::QueryMatch::ExactMatch; match.iconName = QStringLiteral("user-desktop"); match.text = desktop->name(); match.relevance = relevance; QVariantMap properties; properties[QStringLiteral("subtext")] = i18n("Switch to desktop %1", desktop->name()); match.properties = properties; return match; } RemoteMatch WindowsRunner::windowsMatch(const AbstractClient *client, const WindowsRunnerAction action, qreal relevance, Plasma::QueryMatch::Type type) const { RemoteMatch match; match.id = QString::number((int)action) + QLatin1Char('_') + client->internalId().toString(); match.text = client->caption(); match.iconName = client->icon().name(); match.relevance = relevance; match.type = type; QVariantMap properties; const QVector desktops = client->desktops(); bool allDesktops = client->isOnAllDesktops(); const VirtualDesktop *targetDesktop = VirtualDesktopManager::self()->currentDesktop(); // Show on current desktop unless window is only attached to other desktop, in this case show on the first attached desktop if (!allDesktops && !client->isOnCurrentDesktop() && !desktops.isEmpty()) { targetDesktop = desktops.first(); } // When there is no icon name, send a pixmap along instead if (match.iconName.isEmpty()) { QImage convertedImage = client->icon().pixmap(QSize(16,16)).toImage().convertToFormat(QImage::Format_RGBA8888); RemoteImage remoteImage{ convertedImage.width(), convertedImage.height(), convertedImage.bytesPerLine(), true, // hasAlpha 8, // bitsPerSample 4, // channels QByteArray(reinterpret_cast(convertedImage.constBits()), convertedImage.sizeInBytes()) }; properties.insert(QStringLiteral("icon-data"), QVariant::fromValue(remoteImage)); } const QString desktopName = targetDesktop->name(); switch (action) { case CloseAction: properties[QStringLiteral("subtext")] = i18n("Close running window on %1", desktopName); break; case MinimizeAction: properties[QStringLiteral("subtext")] = i18n("(Un)minimize running window on %1", desktopName); break; case MaximizeAction: properties[QStringLiteral("subtext")] = i18n("Maximize/restore running window on %1", desktopName); break; case FullscreenAction: properties[QStringLiteral("subtext")] = i18n("Toggle fullscreen for running window on %1", desktopName); break; case ShadeAction: properties[QStringLiteral("subtext")] = i18n("(Un)shade running window on %1", desktopName); break; case KeepAboveAction: properties[QStringLiteral("subtext")] = i18n("Toggle keep above for running window on %1", desktopName); break; case KeepBelowAction: properties[QStringLiteral("subtext")] = i18n("Toggle keep below running window on %1", desktopName); break; case ActivateAction: default: properties[QStringLiteral("subtext")] = i18n("Activate running window on %1", desktopName); break; } match.properties = properties; return match; } bool WindowsRunner::actionSupported(const AbstractClient *client, const WindowsRunnerAction action) const { switch (action) { case CloseAction: return client->isCloseable(); case MinimizeAction: return client->isMinimizable(); case MaximizeAction: return client->isMaximizable(); case ShadeAction: return client->isShadeable(); case FullscreenAction: return client->isFullScreenable(); case KeepAboveAction: case KeepBelowAction: case ActivateAction: default: return true; } } }