KWin
Loading...
Searching...
No Matches
windowsrunnerinterface.cpp
Go to the documentation of this file.
1/*
2 KWin - the KDE window manager
3 This file is part of the KDE project.
4
5 SPDX-FileCopyrightText: 2009 Martin Gräßlin <kde@martin-graesslin.com>
6 SPDX-FileCopyrightText: 2020 Benjamin Port <benjamin.port@enioka.com>
7
8 SPDX-License-Identifier: GPL-2.0-or-later
9*/
10
12
13#include "virtualdesktops.h"
14#include "window.h"
15#include "workspace.h"
16
17#include "krunner1adaptor.h"
18#include <KLocalizedString>
19
20namespace KWin
21{
22
24{
25 new Krunner1Adaptor(this);
26 qDBusRegisterMetaType<RemoteMatch>();
27 qDBusRegisterMetaType<RemoteMatches>();
28 qDBusRegisterMetaType<RemoteAction>();
29 qDBusRegisterMetaType<RemoteActions>();
30 qDBusRegisterMetaType<RemoteImage>();
31 QDBusConnection::sessionBus().registerObject(QStringLiteral("/WindowsRunner"), this);
32 QDBusConnection::sessionBus().registerService(QStringLiteral("org.kde.KWin"));
33}
34
35RemoteMatches WindowsRunner::Match(const QString &searchTerm)
36{
37 RemoteMatches matches;
38
39 QString term = searchTerm;
40 WindowsRunnerAction action = ActivateAction;
41 if (QString keyword = i18nc("Note this is a KRunner keyword", "activate"); term.endsWith(keyword, Qt::CaseInsensitive)) {
42 action = ActivateAction;
43 term = term.left(term.lastIndexOf(keyword) - 1);
44 } else if (QString keyword = i18nc("Note this is a KRunner keyword", "close"); term.endsWith(keyword, Qt::CaseInsensitive)) {
45 action = CloseAction;
46 term = term.left(term.lastIndexOf(keyword) - 1);
47 } else if (QString keyword = i18nc("Note this is a KRunner keyword", "min"); term.endsWith(keyword, Qt::CaseInsensitive)) {
48 action = MinimizeAction;
49 term = term.left(term.lastIndexOf(keyword) - 1);
50 } else if (QString keyword = i18nc("Note this is a KRunner keyword", "minimize"); term.endsWith(keyword, Qt::CaseInsensitive)) {
51 action = MinimizeAction;
52 term = term.left(term.lastIndexOf(keyword) - 1);
53 } else if (QString keyword = i18nc("Note this is a KRunner keyword", "max"); term.endsWith(keyword, Qt::CaseInsensitive)) {
54 action = MaximizeAction;
55 term = term.left(term.lastIndexOf(keyword) - 1);
56 } else if (term.endsWith(i18nc("Note this is a KRunner keyword", "maximize"), Qt::CaseInsensitive)) {
57 action = MaximizeAction;
58 term = term.left(term.lastIndexOf(i18nc("Note this is a KRunner keyword", "maximize")) - 1);
59 } else if (QString keyword = i18nc("Note this is a KRunner keyword", "fullscreen"); term.endsWith(keyword, Qt::CaseInsensitive)) {
60 action = FullscreenAction;
61 term = term.left(term.lastIndexOf(keyword) - 1);
62 } else if (QString keyword = i18nc("Note this is a KRunner keyword", "shade"); term.endsWith(keyword, Qt::CaseInsensitive)) {
63 action = ShadeAction;
64 term = term.left(term.lastIndexOf(keyword) - 1);
65 } else if (QString keyword = i18nc("Note this is a KRunner keyword", "keep above"); term.endsWith(keyword, Qt::CaseInsensitive)) {
66 action = KeepAboveAction;
67 term = term.left(term.lastIndexOf(keyword) - 1);
68 } else if (QString keyword = i18nc("Note this is a KRunner keyword", "keep below"); term.endsWith(keyword, Qt::CaseInsensitive)) {
69 action = KeepBelowAction;
70 term = term.left(term.lastIndexOf(keyword) - 1);
71 }
72
73 // keyword match: when term starts with "window" we list all windows
74 // the list can be restricted to windows matching a given name, class, role or desktop
75 if (term.startsWith(i18nc("Note this is a KRunner keyword", "window"), Qt::CaseInsensitive)) {
76 const QStringList keywords = term.split(QLatin1Char(' '));
77 QString windowName;
78 QString windowAppName;
79 VirtualDesktop *targetDesktop = nullptr;
80 QVariant desktopId;
81 for (const QString &keyword : keywords) {
82 if (keyword.endsWith(QLatin1Char('='))) {
83 continue;
84 }
85 if (keyword.startsWith(i18nc("Note this is a KRunner keyword", "name") + QLatin1Char('='), Qt::CaseInsensitive)) {
86 windowName = keyword.split(QLatin1Char('='))[1];
87 } else if (keyword.startsWith(i18nc("Note this is a KRunner keyword", "appname") + QLatin1Char('='), Qt::CaseInsensitive)) {
88 windowAppName = keyword.split(QLatin1Char('='))[1];
89 } else if (keyword.startsWith(i18nc("Note this is a KRunner keyword", "desktop") + QLatin1Char('='), Qt::CaseInsensitive)) {
90 desktopId = keyword.split(QLatin1Char('='))[1];
91 for (const auto desktop : VirtualDesktopManager::self()->desktops()) {
92 if (desktop->name().contains(desktopId.toString(), Qt::CaseInsensitive) || desktop->x11DesktopNumber() == desktopId.toUInt()) {
93 targetDesktop = desktop;
94 }
95 }
96 } else {
97 // not a keyword - use as name if name is unused, but another option is set
98 if (windowName.isEmpty() && !keyword.contains(QLatin1Char('=')) && (!windowAppName.isEmpty() || targetDesktop)) {
99 windowName = keyword;
100 }
101 }
102 }
103
104 for (const Window *window : Workspace::self()->windows()) {
105 if (window->isUnmanaged()) {
106 continue;
107 }
108 if (!window->isNormalWindow()) {
109 continue;
110 }
111 const QString appName = window->resourceClass();
112 const QString name = window->caption();
113 if (!windowName.isEmpty() && !name.startsWith(windowName, Qt::CaseInsensitive)) {
114 continue;
115 }
116 if (!windowAppName.isEmpty() && !appName.contains(windowAppName, Qt::CaseInsensitive)) {
117 continue;
118 }
119
120 if (targetDesktop && !window->desktops().contains(targetDesktop) && !window->isOnAllDesktops()) {
121 continue;
122 }
123 // check for windows when no keywords were used
124 // check the name and app name for containing the query without the keyword
125 if (windowName.isEmpty() && windowAppName.isEmpty() && !targetDesktop) {
126 const QString &test = term.mid(keywords[0].length() + 1);
127 if (!name.contains(test, Qt::CaseInsensitive) && !appName.contains(test, Qt::CaseInsensitive)) {
128 continue;
129 }
130 }
131 // blacklisted everything else: we have a match
132 if (actionSupported(window, action)) {
133 matches << windowsMatch(window, action);
134 }
135 }
136
137 if (!matches.isEmpty()) {
138 // the window keyword found matches - do not process other syntax possibilities
139 return matches;
140 }
141 }
142
143 bool desktopAdded = false;
144 // check for desktop keyword
145 if (term.startsWith(i18nc("Note this is a KRunner keyword", "desktop"), Qt::CaseInsensitive)) {
146 const QStringList parts = term.split(QLatin1Char(' '));
147 if (parts.size() == 1) {
148 // only keyword - list all desktops
149 for (auto desktop : VirtualDesktopManager::self()->desktops()) {
150 matches << desktopMatch(desktop);
151 desktopAdded = true;
152 }
153 }
154 }
155
156 // check for matching desktops by name
157 for (const Window *window : Workspace::self()->windows()) {
158 if (window->isUnmanaged()) {
159 continue;
160 }
161 if (!window->isNormalWindow()) {
162 continue;
163 }
164 const QString appName = window->resourceClass();
165 const QString name = window->caption();
166 if (name.startsWith(term, Qt::CaseInsensitive) || appName.startsWith(term, Qt::CaseInsensitive)) {
167 matches << windowsMatch(window, action, 0.8, HighestCategoryRelevance);
168 } else if ((name.contains(term, Qt::CaseInsensitive) || appName.contains(term, Qt::CaseInsensitive)) && actionSupported(window, action)) {
169 matches << windowsMatch(window, action, 0.7, LowCategoryRelevance);
170 }
171 }
172
173 for (auto *desktop : VirtualDesktopManager::self()->desktops()) {
174 if (desktop->name().contains(term, Qt::CaseInsensitive)) {
175 if (!desktopAdded && desktop != VirtualDesktopManager::self()->currentDesktop()) {
176 matches << desktopMatch(desktop, ActivateDesktopAction, 0.8);
177 }
178 // search for windows on desktop and list them with less relevance
179 for (const Window *window : Workspace::self()->windows()) {
180 if (window->isUnmanaged()) {
181 continue;
182 }
183 if (!window->isNormalWindow()) {
184 continue;
185 }
186 if ((window->desktops().contains(desktop) || window->isOnAllDesktops()) && actionSupported(window, action)) {
187 matches << windowsMatch(window, action, 0.5, LowCategoryRelevance);
188 }
189 }
190 }
191 }
192
193 return matches;
194}
195
196void WindowsRunner::Run(const QString &id, const QString &actionId)
197{
198 // Split id to get actionId and realId. We don't use actionId because our actions list is not constant
199 const QStringList parts = id.split(QLatin1Char('_'));
200 auto action = WindowsRunnerAction(parts[0].toInt());
201 auto objectId = parts[1];
202
203 if (action == ActivateDesktopAction) {
204 QByteArray desktopId = objectId.toLocal8Bit();
205 auto desktop = VirtualDesktopManager::self()->desktopForId(desktopId);
206 VirtualDesktopManager::self()->setCurrent(desktop);
207 return;
208 }
209
210 const auto window = workspace()->findWindow(QUuid::fromString(objectId));
211 if (!window || !window->isClient()) {
212 return;
213 }
214
215 switch (action) {
216 case ActivateAction:
217 workspace()->activateWindow(window);
218 break;
219 case CloseAction:
220 window->closeWindow();
221 break;
222 case MinimizeAction:
223 window->setMinimized(!window->isMinimized());
224 break;
225 case MaximizeAction:
226 window->setMaximize(window->maximizeMode() == MaximizeRestore, window->maximizeMode() == MaximizeRestore);
227 break;
228 case FullscreenAction:
229 window->setFullScreen(!window->isFullScreen());
230 break;
231 case ShadeAction:
232 window->toggleShade();
233 break;
234 case KeepAboveAction:
235 window->setKeepAbove(!window->keepAbove());
236 break;
237 case KeepBelowAction:
238 window->setKeepBelow(!window->keepBelow());
239 break;
240 case ActivateDesktopAction:
241 Q_UNREACHABLE();
242 }
243}
244
245RemoteMatch WindowsRunner::desktopMatch(const VirtualDesktop *desktop, const WindowsRunnerAction action, qreal relevance) const
246{
248 match.id = QString::number(action) + QLatin1Char('_') + desktop->id();
249 match.categoryRelevance = HighestCategoryRelevance;
250 match.iconName = QStringLiteral("user-desktop");
251 match.text = desktop->name();
252 match.relevance = relevance;
253 match.properties.insert(QStringLiteral("subtext"), i18n("Switch to desktop %1", desktop->name()));
254 return match;
255}
256
257RemoteMatch WindowsRunner::windowsMatch(const Window *window, const WindowsRunnerAction action, qreal relevance, qreal categoryRelevance) const
258{
260 match.id = QString::number((int)action) + QLatin1Char('_') + window->internalId().toString();
261 match.text = window->caption();
262 match.iconName = window->icon().name();
263 match.relevance = relevance;
264 match.categoryRelevance = categoryRelevance;
265
266 const QList<VirtualDesktop *> desktops = window->desktops();
267 bool allDesktops = window->isOnAllDesktops();
268
269 const VirtualDesktop *targetDesktop = VirtualDesktopManager::self()->currentDesktop();
270 // Show on current desktop unless window is only attached to other desktop, in this case show on the first attached desktop
271 if (!allDesktops && !window->isOnCurrentDesktop() && !desktops.isEmpty()) {
272 targetDesktop = desktops.first();
273 }
274
275 // When there is no icon name, send a pixmap along instead
276 if (match.iconName.isEmpty()) {
277 QImage convertedImage = window->icon().pixmap(QSize(64, 64)).toImage().convertToFormat(QImage::Format_RGBA8888);
278 RemoteImage remoteImage{
279 convertedImage.width(),
280 convertedImage.height(),
281 static_cast<int>(convertedImage.bytesPerLine()),
282 true, // hasAlpha
283 8, // bitsPerSample
284 4, // channels
285 QByteArray(reinterpret_cast<const char *>(convertedImage.constBits()), convertedImage.sizeInBytes())};
286 match.properties.insert(QStringLiteral("icon-data"), QVariant::fromValue(remoteImage));
287 }
288
289 const QString desktopName = targetDesktop->name();
290 switch (action) {
291 case CloseAction:
292 match.properties[QStringLiteral("subtext")] = i18n("Close running window on %1", desktopName);
293 break;
294 case MinimizeAction:
295 match.properties[QStringLiteral("subtext")] = i18n("(Un)minimize running window on %1", desktopName);
296 break;
297 case MaximizeAction:
298 match.properties[QStringLiteral("subtext")] = i18n("Maximize/restore running window on %1", desktopName);
299 break;
300 case FullscreenAction:
301 match.properties[QStringLiteral("subtext")] = i18n("Toggle fullscreen for running window on %1", desktopName);
302 break;
303 case ShadeAction:
304 match.properties[QStringLiteral("subtext")] = i18n("(Un)shade running window on %1", desktopName);
305 break;
306 case KeepAboveAction:
307 match.properties[QStringLiteral("subtext")] = i18n("Toggle keep above for running window on %1", desktopName);
308 break;
309 case KeepBelowAction:
310 match.properties[QStringLiteral("subtext")] = i18n("Toggle keep below running window on %1", desktopName);
311 break;
312 case ActivateAction:
313 default:
314 match.properties[QStringLiteral("subtext")] = i18n("Activate running window on %1", desktopName);
315 break;
316 }
317 return match;
318}
319
320bool WindowsRunner::actionSupported(const Window *window, const WindowsRunnerAction action) const
321{
322 switch (action) {
323 case CloseAction:
324 return window->isCloseable();
325 case MinimizeAction:
326 return window->isMinimizable();
327 case MaximizeAction:
328 return window->isMaximizable();
329 case ShadeAction:
330 return window->isShadeable();
331 case FullscreenAction:
332 return window->isFullScreenable();
333 case KeepAboveAction:
334 case KeepBelowAction:
335 case ActivateAction:
336 default:
337 return true;
338 }
339}
340
341}
342
343#include "moc_windowsrunnerinterface.cpp"
RemoteMatches Match(const QString &searchTerm)
void Run(const QString &id, const QString &actionId)
void activateWindow(Window *window, bool force=false)
Window * findWindow(const QUuid &internalId) const
static Workspace * self()
Definition workspace.h:91
const QList< Window * > windows() const
Definition workspace.h:248
QList< RemoteMatch > RemoteMatches
Definition dbusutils_p.h:32
const qreal LowCategoryRelevance
Definition dbusutils_p.h:19
const qreal HighestCategoryRelevance
Definition dbusutils_p.h:18
@ MaximizeRestore
The window is not maximized in any direction.
Definition common.h:75
Workspace * workspace()
Definition workspace.h:830
bool match(QList< GlobalShortcut > &shortcuts, Args... args)
QString id
Definition dbusutils_p.h:24