File indexing completed on 2024-11-10 04:57:02

0001 /*
0002     KWin - the KDE window manager
0003     This file is part of the KDE project.
0004 
0005     SPDX-FileCopyrightText: 2009 Martin Gräßlin <kde@martin-graesslin.com>
0006     SPDX-FileCopyrightText: 2020 Benjamin Port <benjamin.port@enioka.com>
0007 
0008     SPDX-License-Identifier: GPL-2.0-or-later
0009 */
0010 
0011 #include "windowsrunnerinterface.h"
0012 
0013 #include "virtualdesktops.h"
0014 #include "window.h"
0015 #include "workspace.h"
0016 
0017 #include "krunner1adaptor.h"
0018 #include <KLocalizedString>
0019 
0020 namespace KWin
0021 {
0022 
0023 WindowsRunner::WindowsRunner()
0024 {
0025     new Krunner1Adaptor(this);
0026     qDBusRegisterMetaType<RemoteMatch>();
0027     qDBusRegisterMetaType<RemoteMatches>();
0028     qDBusRegisterMetaType<RemoteAction>();
0029     qDBusRegisterMetaType<RemoteActions>();
0030     qDBusRegisterMetaType<RemoteImage>();
0031     QDBusConnection::sessionBus().registerObject(QStringLiteral("/WindowsRunner"), this);
0032     QDBusConnection::sessionBus().registerService(QStringLiteral("org.kde.KWin"));
0033 }
0034 
0035 RemoteMatches WindowsRunner::Match(const QString &searchTerm)
0036 {
0037     RemoteMatches matches;
0038 
0039     QString term = searchTerm;
0040     WindowsRunnerAction action = ActivateAction;
0041     if (QString keyword = i18nc("Note this is a KRunner keyword", "activate"); term.endsWith(keyword, Qt::CaseInsensitive)) {
0042         action = ActivateAction;
0043         term = term.left(term.lastIndexOf(keyword) - 1);
0044     } else if (QString keyword = i18nc("Note this is a KRunner keyword", "close"); term.endsWith(keyword, Qt::CaseInsensitive)) {
0045         action = CloseAction;
0046         term = term.left(term.lastIndexOf(keyword) - 1);
0047     } else if (QString keyword = i18nc("Note this is a KRunner keyword", "min"); term.endsWith(keyword, Qt::CaseInsensitive)) {
0048         action = MinimizeAction;
0049         term = term.left(term.lastIndexOf(keyword) - 1);
0050     } else if (QString keyword = i18nc("Note this is a KRunner keyword", "minimize"); term.endsWith(keyword, Qt::CaseInsensitive)) {
0051         action = MinimizeAction;
0052         term = term.left(term.lastIndexOf(keyword) - 1);
0053     } else if (QString keyword = i18nc("Note this is a KRunner keyword", "max"); term.endsWith(keyword, Qt::CaseInsensitive)) {
0054         action = MaximizeAction;
0055         term = term.left(term.lastIndexOf(keyword) - 1);
0056     } else if (term.endsWith(i18nc("Note this is a KRunner keyword", "maximize"), Qt::CaseInsensitive)) {
0057         action = MaximizeAction;
0058         term = term.left(term.lastIndexOf(i18nc("Note this is a KRunner keyword", "maximize")) - 1);
0059     } else if (QString keyword = i18nc("Note this is a KRunner keyword", "fullscreen"); term.endsWith(keyword, Qt::CaseInsensitive)) {
0060         action = FullscreenAction;
0061         term = term.left(term.lastIndexOf(keyword) - 1);
0062     } else if (QString keyword = i18nc("Note this is a KRunner keyword", "shade"); term.endsWith(keyword, Qt::CaseInsensitive)) {
0063         action = ShadeAction;
0064         term = term.left(term.lastIndexOf(keyword) - 1);
0065     } else if (QString keyword = i18nc("Note this is a KRunner keyword", "keep above"); term.endsWith(keyword, Qt::CaseInsensitive)) {
0066         action = KeepAboveAction;
0067         term = term.left(term.lastIndexOf(keyword) - 1);
0068     } else if (QString keyword = i18nc("Note this is a KRunner keyword", "keep below"); term.endsWith(keyword, Qt::CaseInsensitive)) {
0069         action = KeepBelowAction;
0070         term = term.left(term.lastIndexOf(keyword) - 1);
0071     }
0072 
0073     // keyword match: when term starts with "window" we list all windows
0074     // the list can be restricted to windows matching a given name, class, role or desktop
0075     if (term.startsWith(i18nc("Note this is a KRunner keyword", "window"), Qt::CaseInsensitive)) {
0076         const QStringList keywords = term.split(QLatin1Char(' '));
0077         QString windowName;
0078         QString windowAppName;
0079         VirtualDesktop *targetDesktop = nullptr;
0080         QVariant desktopId;
0081         for (const QString &keyword : keywords) {
0082             if (keyword.endsWith(QLatin1Char('='))) {
0083                 continue;
0084             }
0085             if (keyword.startsWith(i18nc("Note this is a KRunner keyword", "name") + QLatin1Char('='), Qt::CaseInsensitive)) {
0086                 windowName = keyword.split(QLatin1Char('='))[1];
0087             } else if (keyword.startsWith(i18nc("Note this is a KRunner keyword", "appname") + QLatin1Char('='), Qt::CaseInsensitive)) {
0088                 windowAppName = keyword.split(QLatin1Char('='))[1];
0089             } else if (keyword.startsWith(i18nc("Note this is a KRunner keyword", "desktop") + QLatin1Char('='), Qt::CaseInsensitive)) {
0090                 desktopId = keyword.split(QLatin1Char('='))[1];
0091                 for (const auto desktop : VirtualDesktopManager::self()->desktops()) {
0092                     if (desktop->name().contains(desktopId.toString(), Qt::CaseInsensitive) || desktop->x11DesktopNumber() == desktopId.toUInt()) {
0093                         targetDesktop = desktop;
0094                     }
0095                 }
0096             } else {
0097                 // not a keyword - use as name if name is unused, but another option is set
0098                 if (windowName.isEmpty() && !keyword.contains(QLatin1Char('=')) && (!windowAppName.isEmpty() || targetDesktop)) {
0099                     windowName = keyword;
0100                 }
0101             }
0102         }
0103 
0104         for (const Window *window : Workspace::self()->windows()) {
0105             if (window->isUnmanaged()) {
0106                 continue;
0107             }
0108             if (!window->isNormalWindow()) {
0109                 continue;
0110             }
0111             const QString appName = window->resourceClass();
0112             const QString name = window->caption();
0113             if (!windowName.isEmpty() && !name.startsWith(windowName, Qt::CaseInsensitive)) {
0114                 continue;
0115             }
0116             if (!windowAppName.isEmpty() && !appName.contains(windowAppName, Qt::CaseInsensitive)) {
0117                 continue;
0118             }
0119 
0120             if (targetDesktop && !window->desktops().contains(targetDesktop) && !window->isOnAllDesktops()) {
0121                 continue;
0122             }
0123             // check for windows when no keywords were used
0124             // check the name and app name for containing the query without the keyword
0125             if (windowName.isEmpty() && windowAppName.isEmpty() && !targetDesktop) {
0126                 const QString &test = term.mid(keywords[0].length() + 1);
0127                 if (!name.contains(test, Qt::CaseInsensitive) && !appName.contains(test, Qt::CaseInsensitive)) {
0128                     continue;
0129                 }
0130             }
0131             // blacklisted everything else: we have a match
0132             if (actionSupported(window, action)) {
0133                 matches << windowsMatch(window, action);
0134             }
0135         }
0136 
0137         if (!matches.isEmpty()) {
0138             // the window keyword found matches - do not process other syntax possibilities
0139             return matches;
0140         }
0141     }
0142 
0143     bool desktopAdded = false;
0144     // check for desktop keyword
0145     if (term.startsWith(i18nc("Note this is a KRunner keyword", "desktop"), Qt::CaseInsensitive)) {
0146         const QStringList parts = term.split(QLatin1Char(' '));
0147         if (parts.size() == 1) {
0148             // only keyword - list all desktops
0149             for (auto desktop : VirtualDesktopManager::self()->desktops()) {
0150                 matches << desktopMatch(desktop);
0151                 desktopAdded = true;
0152             }
0153         }
0154     }
0155 
0156     // check for matching desktops by name
0157     for (const Window *window : Workspace::self()->windows()) {
0158         if (window->isUnmanaged()) {
0159             continue;
0160         }
0161         if (!window->isNormalWindow()) {
0162             continue;
0163         }
0164         const QString appName = window->resourceClass();
0165         const QString name = window->caption();
0166         if (name.startsWith(term, Qt::CaseInsensitive) || appName.startsWith(term, Qt::CaseInsensitive)) {
0167             matches << windowsMatch(window, action, 0.8, HighestCategoryRelevance);
0168         } else if ((name.contains(term, Qt::CaseInsensitive) || appName.contains(term, Qt::CaseInsensitive)) && actionSupported(window, action)) {
0169             matches << windowsMatch(window, action, 0.7, LowCategoryRelevance);
0170         }
0171     }
0172 
0173     for (auto *desktop : VirtualDesktopManager::self()->desktops()) {
0174         if (desktop->name().contains(term, Qt::CaseInsensitive)) {
0175             if (!desktopAdded && desktop != VirtualDesktopManager::self()->currentDesktop()) {
0176                 matches << desktopMatch(desktop, ActivateDesktopAction, 0.8);
0177             }
0178             // search for windows on desktop and list them with less relevance
0179             for (const Window *window : Workspace::self()->windows()) {
0180                 if (window->isUnmanaged()) {
0181                     continue;
0182                 }
0183                 if (!window->isNormalWindow()) {
0184                     continue;
0185                 }
0186                 if ((window->desktops().contains(desktop) || window->isOnAllDesktops()) && actionSupported(window, action)) {
0187                     matches << windowsMatch(window, action, 0.5, LowCategoryRelevance);
0188                 }
0189             }
0190         }
0191     }
0192 
0193     return matches;
0194 }
0195 
0196 void WindowsRunner::Run(const QString &id, const QString &actionId)
0197 {
0198     // Split id to get actionId and realId. We don't use actionId because our actions list is not constant
0199     const QStringList parts = id.split(QLatin1Char('_'));
0200     auto action = WindowsRunnerAction(parts[0].toInt());
0201     auto objectId = parts[1];
0202 
0203     if (action == ActivateDesktopAction) {
0204         QByteArray desktopId = objectId.toLocal8Bit();
0205         auto desktop = VirtualDesktopManager::self()->desktopForId(desktopId);
0206         VirtualDesktopManager::self()->setCurrent(desktop);
0207         return;
0208     }
0209 
0210     const auto window = workspace()->findWindow(QUuid::fromString(objectId));
0211     if (!window || !window->isClient()) {
0212         return;
0213     }
0214 
0215     switch (action) {
0216     case ActivateAction:
0217         workspace()->activateWindow(window);
0218         break;
0219     case CloseAction:
0220         window->closeWindow();
0221         break;
0222     case MinimizeAction:
0223         window->setMinimized(!window->isMinimized());
0224         break;
0225     case MaximizeAction:
0226         window->setMaximize(window->maximizeMode() == MaximizeRestore, window->maximizeMode() == MaximizeRestore);
0227         break;
0228     case FullscreenAction:
0229         window->setFullScreen(!window->isFullScreen());
0230         break;
0231     case ShadeAction:
0232         window->toggleShade();
0233         break;
0234     case KeepAboveAction:
0235         window->setKeepAbove(!window->keepAbove());
0236         break;
0237     case KeepBelowAction:
0238         window->setKeepBelow(!window->keepBelow());
0239         break;
0240     case ActivateDesktopAction:
0241         Q_UNREACHABLE();
0242     }
0243 }
0244 
0245 RemoteMatch WindowsRunner::desktopMatch(const VirtualDesktop *desktop, const WindowsRunnerAction action, qreal relevance) const
0246 {
0247     RemoteMatch match;
0248     match.id = QString::number(action) + QLatin1Char('_') + desktop->id();
0249     match.categoryRelevance = HighestCategoryRelevance;
0250     match.iconName = QStringLiteral("user-desktop");
0251     match.text = desktop->name();
0252     match.relevance = relevance;
0253     match.properties.insert(QStringLiteral("subtext"), i18n("Switch to desktop %1", desktop->name()));
0254     return match;
0255 }
0256 
0257 RemoteMatch WindowsRunner::windowsMatch(const Window *window, const WindowsRunnerAction action, qreal relevance, qreal categoryRelevance) const
0258 {
0259     RemoteMatch match;
0260     match.id = QString::number((int)action) + QLatin1Char('_') + window->internalId().toString();
0261     match.text = window->caption();
0262     match.iconName = window->icon().name();
0263     match.relevance = relevance;
0264     match.categoryRelevance = categoryRelevance;
0265 
0266     const QList<VirtualDesktop *> desktops = window->desktops();
0267     bool allDesktops = window->isOnAllDesktops();
0268 
0269     const VirtualDesktop *targetDesktop = VirtualDesktopManager::self()->currentDesktop();
0270     // Show on current desktop unless window is only attached to other desktop, in this case show on the first attached desktop
0271     if (!allDesktops && !window->isOnCurrentDesktop() && !desktops.isEmpty()) {
0272         targetDesktop = desktops.first();
0273     }
0274 
0275     // When there is no icon name, send a pixmap along instead
0276     if (match.iconName.isEmpty()) {
0277         QImage convertedImage = window->icon().pixmap(QSize(64, 64)).toImage().convertToFormat(QImage::Format_RGBA8888);
0278         RemoteImage remoteImage{
0279             convertedImage.width(),
0280             convertedImage.height(),
0281             static_cast<int>(convertedImage.bytesPerLine()),
0282             true, // hasAlpha
0283             8, // bitsPerSample
0284             4, // channels
0285             QByteArray(reinterpret_cast<const char *>(convertedImage.constBits()), convertedImage.sizeInBytes())};
0286         match.properties.insert(QStringLiteral("icon-data"), QVariant::fromValue(remoteImage));
0287     }
0288 
0289     const QString desktopName = targetDesktop->name();
0290     switch (action) {
0291     case CloseAction:
0292         match.properties[QStringLiteral("subtext")] = i18n("Close running window on %1", desktopName);
0293         break;
0294     case MinimizeAction:
0295         match.properties[QStringLiteral("subtext")] = i18n("(Un)minimize running window on %1", desktopName);
0296         break;
0297     case MaximizeAction:
0298         match.properties[QStringLiteral("subtext")] = i18n("Maximize/restore running window on %1", desktopName);
0299         break;
0300     case FullscreenAction:
0301         match.properties[QStringLiteral("subtext")] = i18n("Toggle fullscreen for running window on %1", desktopName);
0302         break;
0303     case ShadeAction:
0304         match.properties[QStringLiteral("subtext")] = i18n("(Un)shade running window on %1", desktopName);
0305         break;
0306     case KeepAboveAction:
0307         match.properties[QStringLiteral("subtext")] = i18n("Toggle keep above for running window on %1", desktopName);
0308         break;
0309     case KeepBelowAction:
0310         match.properties[QStringLiteral("subtext")] = i18n("Toggle keep below running window on %1", desktopName);
0311         break;
0312     case ActivateAction:
0313     default:
0314         match.properties[QStringLiteral("subtext")] = i18n("Activate running window on %1", desktopName);
0315         break;
0316     }
0317     return match;
0318 }
0319 
0320 bool WindowsRunner::actionSupported(const Window *window, const WindowsRunnerAction action) const
0321 {
0322     switch (action) {
0323     case CloseAction:
0324         return window->isCloseable();
0325     case MinimizeAction:
0326         return window->isMinimizable();
0327     case MaximizeAction:
0328         return window->isMaximizable();
0329     case ShadeAction:
0330         return window->isShadeable();
0331     case FullscreenAction:
0332         return window->isFullScreenable();
0333     case KeepAboveAction:
0334     case KeepBelowAction:
0335     case ActivateAction:
0336     default:
0337         return true;
0338     }
0339 }
0340 
0341 }
0342 
0343 #include "moc_windowsrunnerinterface.cpp"