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