1 /*
2 KWin - the KDE window manager
3 This file is part of the KDE project.
4
5 SPDX-FileCopyrightText: 2009 Martin Gräßlin <mgraesslin@kde.org>
6
7 SPDX-License-Identifier: GPL-2.0-or-later
8 */
9
10 // own
11 #include "tabboxhandler.h"
12 #include <config-kwin.h>
13 #include <kwinglobals.h>
14 #include "xcbutils.h"
15 // tabbox
16 #include "clientmodel.h"
17 #include "desktopmodel.h"
18 #include "scripting/scripting.h"
19 #include "switcheritem.h"
20 #include "tabbox_logging.h"
21 // Qt
22 #include <QKeyEvent>
23 #include <QStandardPaths>
24 #include <QTimer>
25 #include <QQmlContext>
26 #include <QQmlComponent>
27 #include <QQmlEngine>
28 #include <QQuickItem>
29 #include <QQuickWindow>
30 #include <qpa/qwindowsysteminterface.h>
31 // KDE
32 #include <KLocalizedString>
33 #include <KProcess>
34 #include <KPackage/Package>
35 #include <KPackage/PackageLoader>
36
37 namespace KWin
38 {
39 namespace TabBox
40 {
41
42 class TabBoxHandlerPrivate
43 {
44 public:
45 TabBoxHandlerPrivate(TabBoxHandler *q);
46
47 ~TabBoxHandlerPrivate();
48
49 /**
50 * Updates the current highlight window state
51 */
52 void updateHighlightWindows();
53 /**
54 * Ends window highlighting
55 */
56 void endHighlightWindows(bool abort = false);
57
58 void show();
59 QQuickWindow *window() const;
60 SwitcherItem *switcherItem() const;
61
62 ClientModel* clientModel() const;
63 DesktopModel* desktopModel() const;
64
65 TabBoxHandler *q; // public pointer
66 // members
67 TabBoxConfig config;
68 QScopedPointer<QQmlContext> m_qmlContext;
69 QScopedPointer<QQmlComponent> m_qmlComponent;
70 QObject *m_mainItem;
71 QMap<QString, QObject*> m_clientTabBoxes;
72 QMap<QString, QObject*> m_desktopTabBoxes;
73 ClientModel* m_clientModel;
74 DesktopModel* m_desktopModel;
75 QModelIndex index;
76 /**
77 * Indicates if the tabbox is shown.
78 */
79 bool isShown;
80 TabBoxClient *lastRaisedClient, *lastRaisedClientSucc;
81 int wheelAngleDelta = 0;
82
83 private:
84 QObject *createSwitcherItem(bool desktopMode);
85 };
86
TabBoxHandlerPrivate(TabBoxHandler * q)87 TabBoxHandlerPrivate::TabBoxHandlerPrivate(TabBoxHandler *q)
88 : m_qmlContext()
89 , m_qmlComponent()
90 , m_mainItem(nullptr)
91 {
92 this->q = q;
93 isShown = false;
94 lastRaisedClient = nullptr;
95 lastRaisedClientSucc = nullptr;
96 config = TabBoxConfig();
97 m_clientModel = new ClientModel(q);
98 m_desktopModel = new DesktopModel(q);
99 }
100
~TabBoxHandlerPrivate()101 TabBoxHandlerPrivate::~TabBoxHandlerPrivate()
102 {
103 for (auto it = m_clientTabBoxes.constBegin(); it != m_clientTabBoxes.constEnd(); ++it) {
104 delete it.value();
105 }
106 for (auto it = m_desktopTabBoxes.constBegin(); it != m_desktopTabBoxes.constEnd(); ++it) {
107 delete it.value();
108 }
109 }
110
window() const111 QQuickWindow *TabBoxHandlerPrivate::window() const
112 {
113 if (!m_mainItem) {
114 return nullptr;
115 }
116 if (QQuickWindow *w = qobject_cast<QQuickWindow*>(m_mainItem)) {
117 return w;
118 }
119 return m_mainItem->findChild<QQuickWindow*>();
120 }
121
122 #ifndef KWIN_UNIT_TEST
switcherItem() const123 SwitcherItem *TabBoxHandlerPrivate::switcherItem() const
124 {
125 if (!m_mainItem) {
126 return nullptr;
127 }
128 if (SwitcherItem *i = qobject_cast<SwitcherItem*>(m_mainItem)) {
129 return i;
130 } else if (QQuickWindow *w = qobject_cast<QQuickWindow*>(m_mainItem)) {
131 return w->contentItem()->findChild<SwitcherItem*>();
132 }
133 return m_mainItem->findChild<SwitcherItem*>();
134 }
135 #endif
136
clientModel() const137 ClientModel* TabBoxHandlerPrivate::clientModel() const
138 {
139 return m_clientModel;
140 }
141
desktopModel() const142 DesktopModel* TabBoxHandlerPrivate::desktopModel() const
143 {
144 return m_desktopModel;
145 }
146
updateHighlightWindows()147 void TabBoxHandlerPrivate::updateHighlightWindows()
148 {
149 if (!isShown || config.tabBoxMode() != TabBoxConfig::ClientTabBox)
150 return;
151
152 TabBoxClient *currentClient = q->client(index);
153 QWindow *w = window();
154
155 if (q->isKWinCompositing()) {
156 if (lastRaisedClient)
157 q->elevateClient(lastRaisedClient, w, false);
158 lastRaisedClient = currentClient;
159 if (currentClient)
160 q->elevateClient(currentClient, w, true);
161 } else {
162 if (lastRaisedClient) {
163 q->shadeClient(lastRaisedClient, true);
164 if (lastRaisedClientSucc)
165 q->restack(lastRaisedClient, lastRaisedClientSucc);
166 // TODO lastRaisedClient->setMinimized( lastRaisedClientWasMinimized );
167 }
168
169 lastRaisedClient = currentClient;
170 if (lastRaisedClient) {
171 q->shadeClient(lastRaisedClient, false);
172 // TODO if ( (lastRaisedClientWasMinimized = lastRaisedClient->isMinimized()) )
173 // lastRaisedClient->setMinimized( false );
174 TabBoxClientList order = q->stackingOrder();
175 int succIdx = order.count() + 1;
176 for (int i=0; i<order.count(); ++i) {
177 if (order.at(i).toStrongRef() == lastRaisedClient) {
178 succIdx = i + 1;
179 break;
180 }
181 }
182 lastRaisedClientSucc = (succIdx < order.count()) ? order.at(succIdx).toStrongRef().data() : nullptr;
183 q->raiseClient(lastRaisedClient);
184 }
185 }
186
187 if (config.isShowTabBox() && w) {
188 q->highlightWindows(currentClient, w);
189 } else {
190 q->highlightWindows(currentClient);
191 }
192 }
193
endHighlightWindows(bool abort)194 void TabBoxHandlerPrivate::endHighlightWindows(bool abort)
195 {
196 TabBoxClient *currentClient = q->client(index);
197 if (config.isHighlightWindows() && q->isKWinCompositing()) {
198 Q_FOREACH (const QWeakPointer<TabBoxClient> &clientPointer, q->stackingOrder()) {
199 if (QSharedPointer<TabBoxClient> client = clientPointer.toStrongRef())
200 if (client != currentClient) // to not mess up with wanted ShadeActive/ShadeHover state
201 q->shadeClient(client.data(), true);
202 }
203 }
204 QWindow *w = window();
205 if (currentClient)
206 q->elevateClient(currentClient, w, false);
207 if (abort && lastRaisedClient && lastRaisedClientSucc)
208 q->restack(lastRaisedClient, lastRaisedClientSucc);
209 lastRaisedClient = nullptr;
210 lastRaisedClientSucc = nullptr;
211 // highlight windows
212 q->highlightWindows();
213 }
214
215 #ifndef KWIN_UNIT_TEST
createSwitcherItem(bool desktopMode)216 QObject *TabBoxHandlerPrivate::createSwitcherItem(bool desktopMode)
217 {
218 // first try look'n'feel package
219 QString file = QStandardPaths::locate(
220 QStandardPaths::GenericDataLocation,
221 QStringLiteral("plasma/look-and-feel/%1/contents/%2")
222 .arg(config.layoutName(),
223 desktopMode ? QStringLiteral("desktopswitcher/DesktopSwitcher.qml") : QStringLiteral("windowswitcher/WindowSwitcher.qml")));
224 if (file.isNull()) {
225 const QString folderName = QLatin1String(KWIN_NAME) + (desktopMode ? QLatin1String("/desktoptabbox/") : QLatin1String("/tabbox/"));
226 auto findSwitcher = [this, desktopMode, folderName] {
227 const QString type = desktopMode ? QStringLiteral("KWin/DesktopSwitcher") : QStringLiteral("KWin/WindowSwitcher");
228 auto offers = KPackage::PackageLoader::self()->findPackages(type, folderName,
229 [this] (const KPluginMetaData &data) {
230 return data.pluginId().compare(config.layoutName(), Qt::CaseInsensitive) == 0;
231 }
232 );
233 if (offers.isEmpty()) {
234 // load default
235 offers = KPackage::PackageLoader::self()->findPackages(type, folderName,
236 [] (const KPluginMetaData &data) {
237 return data.pluginId().compare(QStringLiteral("informative"), Qt::CaseInsensitive) == 0;
238 }
239 );
240 if (offers.isEmpty()) {
241 qCDebug(KWIN_TABBOX) << "could not find default window switcher layout";
242 return KPluginMetaData();
243 }
244 }
245 return offers.first();
246 };
247 auto service = findSwitcher();
248 if (!service.isValid()) {
249 return nullptr;
250 }
251 if (service.value(QStringLiteral("X-Plasma-API")) != QLatin1String("declarativeappletscript")) {
252 qCDebug(KWIN_TABBOX) << "Window Switcher Layout is no declarativeappletscript";
253 return nullptr;
254 }
255 auto findScriptFile = [service, folderName] {
256 const QString pluginName = service.pluginId();
257 const QString scriptName = service.value(QStringLiteral("X-Plasma-MainScript"));
258 return QStandardPaths::locate(QStandardPaths::GenericDataLocation, folderName + pluginName + QLatin1String("/contents/") + scriptName);
259 };
260 file = findScriptFile();
261 }
262 if (file.isNull()) {
263 qCDebug(KWIN_TABBOX) << "Could not find QML file for window switcher";
264 return nullptr;
265 }
266 m_qmlComponent->loadUrl(QUrl::fromLocalFile(file));
267 if (m_qmlComponent->isError()) {
268 qCDebug(KWIN_TABBOX) << "Component failed to load: " << m_qmlComponent->errors();
269 QStringList args;
270 args << QStringLiteral("--passivepopup") << i18n("The Window Switcher installation is broken, resources are missing.\n"
271 "Contact your distribution about this.") << QStringLiteral("20");
272 KProcess::startDetached(QStringLiteral("kdialog"), args);
273 } else {
274 QObject *object = m_qmlComponent->create(m_qmlContext.data());
275 if (desktopMode) {
276 m_desktopTabBoxes.insert(config.layoutName(), object);
277 } else {
278 m_clientTabBoxes.insert(config.layoutName(), object);
279 }
280 return object;
281 }
282 return nullptr;
283 }
284 #endif
285
show()286 void TabBoxHandlerPrivate::show()
287 {
288 #ifndef KWIN_UNIT_TEST
289 if (m_qmlContext.isNull()) {
290 qmlRegisterType<SwitcherItem>("org.kde.kwin", 2, 0, "Switcher");
291 m_qmlContext.reset(new QQmlContext(Scripting::self()->qmlEngine()));
292 }
293 if (m_qmlComponent.isNull()) {
294 m_qmlComponent.reset(new QQmlComponent(Scripting::self()->qmlEngine()));
295 }
296 const bool desktopMode = (config.tabBoxMode() == TabBoxConfig::DesktopTabBox);
297 auto findMainItem = [this](const QMap<QString, QObject *> &tabBoxes) -> QObject* {
298 auto it = tabBoxes.constFind(config.layoutName());
299 if (it != tabBoxes.constEnd()) {
300 return it.value();
301 }
302 return nullptr;
303 };
304 m_mainItem = nullptr;
305 m_mainItem = desktopMode ? findMainItem(m_desktopTabBoxes) : findMainItem(m_clientTabBoxes);
306 if (!m_mainItem) {
307 m_mainItem = createSwitcherItem(desktopMode);
308 if (!m_mainItem) {
309 return;
310 }
311 }
312 if (SwitcherItem *item = switcherItem()) {
313 // In case the model isn't yet set (see below), index will be reset and therefore we
314 // need to save the current index row (https://bugs.kde.org/show_bug.cgi?id=333511).
315 int indexRow = index.row();
316 if (!item->model()) {
317 QAbstractItemModel *model = nullptr;
318 if (desktopMode) {
319 model = desktopModel();
320 } else {
321 model = clientModel();
322 }
323 item->setModel(model);
324 }
325 item->setAllDesktops(config.clientDesktopMode() == TabBoxConfig::AllDesktopsClients);
326 item->setCurrentIndex(indexRow);
327 item->setNoModifierGrab(q->noModifierGrab());
328 // everything is prepared, so let's make the whole thing visible
329 item->setVisible(true);
330 }
331 if (QWindow *w = window()) {
332 wheelAngleDelta = 0;
333 w->installEventFilter(q);
334 // pretend to activate the window to enable accessibility notifications
335 QWindowSystemInterface::handleWindowActivated(w, Qt::TabFocusReason);
336 }
337 #endif
338 }
339
340 /***********************************************
341 * TabBoxHandler
342 ***********************************************/
343
TabBoxHandler(QObject * parent)344 TabBoxHandler::TabBoxHandler(QObject *parent)
345 : QObject(parent)
346 {
347 KWin::TabBox::tabBox = this;
348 d = new TabBoxHandlerPrivate(this);
349 }
350
~TabBoxHandler()351 TabBoxHandler::~TabBoxHandler()
352 {
353 delete d;
354 }
355
config() const356 const KWin::TabBox::TabBoxConfig& TabBoxHandler::config() const
357 {
358 return d->config;
359 }
360
setConfig(const TabBoxConfig & config)361 void TabBoxHandler::setConfig(const TabBoxConfig& config)
362 {
363 d->config = config;
364 Q_EMIT configChanged();
365 }
366
show()367 void TabBoxHandler::show()
368 {
369 d->isShown = true;
370 d->lastRaisedClient = nullptr;
371 d->lastRaisedClientSucc = nullptr;
372 if (d->config.isShowTabBox()) {
373 d->show();
374 }
375 if (d->config.isHighlightWindows()) {
376 if (kwinApp()->x11Connection()) {
377 Xcb::sync();
378 }
379 // TODO this should be
380 // QMetaObject::invokeMethod(this, "initHighlightWindows", Qt::QueuedConnection);
381 // but we somehow need to cross > 1 event cycle (likely because of queued invocation in the effects)
382 // to ensure the EffectWindow is present when updateHighlightWindows, thus elevating the window/tabbox
383 QTimer::singleShot(1, this, &TabBoxHandler::initHighlightWindows);
384 }
385 }
386
initHighlightWindows()387 void TabBoxHandler::initHighlightWindows()
388 {
389 if (isKWinCompositing()) {
390 Q_FOREACH (const QWeakPointer<TabBoxClient> &clientPointer, stackingOrder()) {
391 if (QSharedPointer<TabBoxClient> client = clientPointer.toStrongRef())
392 shadeClient(client.data(), false);
393 }
394 }
395 d->updateHighlightWindows();
396 }
397
hide(bool abort)398 void TabBoxHandler::hide(bool abort)
399 {
400 d->isShown = false;
401 if (d->config.isHighlightWindows()) {
402 d->endHighlightWindows(abort);
403 }
404 #ifndef KWIN_UNIT_TEST
405 if (SwitcherItem *item = d->switcherItem()) {
406 item->setVisible(false);
407 }
408 #endif
409 if (QQuickWindow *w = d->window()) {
410 w->hide();
411 w->destroy();
412 }
413 d->m_mainItem = nullptr;
414 }
415
nextPrev(bool forward) const416 QModelIndex TabBoxHandler::nextPrev(bool forward) const
417 {
418 QModelIndex ret;
419 QAbstractItemModel* model;
420 switch(d->config.tabBoxMode()) {
421 case TabBoxConfig::ClientTabBox:
422 model = d->clientModel();
423 break;
424 case TabBoxConfig::DesktopTabBox:
425 model = d->desktopModel();
426 break;
427 default:
428 Q_UNREACHABLE();
429 }
430 if (forward) {
431 int column = d->index.column() + 1;
432 int row = d->index.row();
433 if (column == model->columnCount()) {
434 column = 0;
435 row++;
436 if (row == model->rowCount())
437 row = 0;
438 }
439 ret = model->index(row, column);
440 if (!ret.isValid())
441 ret = model->index(0, 0);
442 } else {
443 int column = d->index.column() - 1;
444 int row = d->index.row();
445 if (column < 0) {
446 column = model->columnCount() - 1;
447 row--;
448 if (row < 0)
449 row = model->rowCount() - 1;
450 }
451 ret = model->index(row, column);
452 if (!ret.isValid()) {
453 row = model->rowCount() - 1;
454 for (int i = model->columnCount() - 1; i >= 0; i--) {
455 ret = model->index(row, i);
456 if (ret.isValid())
457 break;
458 }
459 }
460 }
461 if (ret.isValid())
462 return ret;
463 else
464 return d->index;
465 }
466
desktopIndex(int desktop) const467 QModelIndex TabBoxHandler::desktopIndex(int desktop) const
468 {
469 if (d->config.tabBoxMode() != TabBoxConfig::DesktopTabBox)
470 return QModelIndex();
471 return d->desktopModel()->desktopIndex(desktop);
472 }
473
desktopList() const474 QList< int > TabBoxHandler::desktopList() const
475 {
476 if (d->config.tabBoxMode() != TabBoxConfig::DesktopTabBox)
477 return QList< int >();
478 return d->desktopModel()->desktopList();
479 }
480
desktop(const QModelIndex & index) const481 int TabBoxHandler::desktop(const QModelIndex& index) const
482 {
483 if (!index.isValid() || (d->config.tabBoxMode() != TabBoxConfig::DesktopTabBox))
484 return -1;
485 QVariant ret = d->desktopModel()->data(index, DesktopModel::DesktopRole);
486 if (ret.isValid())
487 return ret.toInt();
488 else
489 return -1;
490 }
491
setCurrentIndex(const QModelIndex & index)492 void TabBoxHandler::setCurrentIndex(const QModelIndex& index)
493 {
494 if (d->index == index) {
495 return;
496 }
497 if (!index.isValid()) {
498 return;
499 }
500 d->index = index;
501 if (d->config.tabBoxMode() == TabBoxConfig::ClientTabBox) {
502 if (d->config.isHighlightWindows()) {
503 d->updateHighlightWindows();
504 }
505 }
506 Q_EMIT selectedIndexChanged();
507 }
508
currentIndex() const509 const QModelIndex& TabBoxHandler::currentIndex() const
510 {
511 return d->index;
512 }
513
grabbedKeyEvent(QKeyEvent * event) const514 void TabBoxHandler::grabbedKeyEvent(QKeyEvent* event) const
515 {
516 if (!d->m_mainItem || !d->window()) {
517 return;
518 }
519 QCoreApplication::sendEvent(d->window(), event);
520 }
521
containsPos(const QPoint & pos) const522 bool TabBoxHandler::containsPos(const QPoint& pos) const
523 {
524 if (!d->m_mainItem) {
525 return false;
526 }
527 QWindow *w = d->window();
528 if (w) {
529 return w->geometry().contains(pos);
530 }
531 return false;
532 }
533
index(QWeakPointer<KWin::TabBox::TabBoxClient> client) const534 QModelIndex TabBoxHandler::index(QWeakPointer<KWin::TabBox::TabBoxClient> client) const
535 {
536 return d->clientModel()->index(client);
537 }
538
clientList() const539 TabBoxClientList TabBoxHandler::clientList() const
540 {
541 if (d->config.tabBoxMode() != TabBoxConfig::ClientTabBox)
542 return TabBoxClientList();
543 return d->clientModel()->clientList();
544 }
545
client(const QModelIndex & index) const546 TabBoxClient* TabBoxHandler::client(const QModelIndex& index) const
547 {
548 if ((!index.isValid()) ||
549 (d->config.tabBoxMode() != TabBoxConfig::ClientTabBox))
550 return nullptr;
551 TabBoxClient* c = static_cast< TabBoxClient* >(
552 d->clientModel()->data(index, ClientModel::ClientRole).value<void *>());
553 return c;
554 }
555
createModel(bool partialReset)556 void TabBoxHandler::createModel(bool partialReset)
557 {
558 switch(d->config.tabBoxMode()) {
559 case TabBoxConfig::ClientTabBox: {
560 d->clientModel()->createClientList(partialReset);
561 // TODO: C++11 use lambda function
562 bool lastRaised = false;
563 bool lastRaisedSucc = false;
564 Q_FOREACH (const QWeakPointer<TabBoxClient> &clientPointer, stackingOrder()) {
565 QSharedPointer<TabBoxClient> client = clientPointer.toStrongRef();
566 if (!client) {
567 continue;
568 }
569 if (client.data() == d->lastRaisedClient) {
570 lastRaised = true;
571 }
572 if (client.data() == d->lastRaisedClientSucc) {
573 lastRaisedSucc = true;
574 }
575 }
576 if (d->lastRaisedClient && !lastRaised)
577 d->lastRaisedClient = nullptr;
578 if (d->lastRaisedClientSucc && !lastRaisedSucc)
579 d->lastRaisedClientSucc = nullptr;
580 break;
581 }
582 case TabBoxConfig::DesktopTabBox:
583 d->desktopModel()->createDesktopList();
584 break;
585 }
586 }
587
first() const588 QModelIndex TabBoxHandler::first() const
589 {
590 QAbstractItemModel* model;
591 switch(d->config.tabBoxMode()) {
592 case TabBoxConfig::ClientTabBox:
593 model = d->clientModel();
594 break;
595 case TabBoxConfig::DesktopTabBox:
596 model = d->desktopModel();
597 break;
598 default:
599 Q_UNREACHABLE();
600 }
601 return model->index(0, 0);
602 }
603
eventFilter(QObject * watched,QEvent * e)604 bool TabBoxHandler::eventFilter(QObject *watched, QEvent *e)
605 {
606 if (e->type() == QEvent::Wheel && watched == d->window()) {
607 QWheelEvent *event = static_cast<QWheelEvent*>(e);
608 // On x11 the delta for vertical scrolling might also be on X for whatever reason
609 const int delta = qAbs(event->angleDelta().x()) > qAbs(event->angleDelta().y()) ? event->angleDelta().x() : event->angleDelta().y();
610 d->wheelAngleDelta += delta;
611 while (d->wheelAngleDelta <= -120) {
612 d->wheelAngleDelta += 120;
613 const QModelIndex index = nextPrev(true);
614 if (index.isValid()) {
615 setCurrentIndex(index);
616 }
617 }
618 while (d->wheelAngleDelta >= 120) {
619 d->wheelAngleDelta -= 120;
620 const QModelIndex index = nextPrev(false);
621 if (index.isValid()) {
622 setCurrentIndex(index);
623 }
624 }
625 return true;
626 }
627 // pass on
628 return QObject::eventFilter(watched, e);
629 }
630
631 TabBoxHandler* tabBox = nullptr;
632
TabBoxClient()633 TabBoxClient::TabBoxClient()
634 {
635 }
636
~TabBoxClient()637 TabBoxClient::~TabBoxClient()
638 {
639 }
640
641 } // namespace TabBox
642 } // namespace KWin
643