File indexing completed on 2024-12-22 05:15:15

0001 /*
0002     SPDX-FileCopyrightText: 2016 Kai Uwe Broulik <kde@privat.broulik.de>
0003     SPDX-FileCopyrightText: 2016 Chinmoy Ranjan Pradhan <chinmoyrp65@gmail.com>
0004 
0005     SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
0006 */
0007 
0008 #include "appmenumodel.h"
0009 
0010 #include <QDBusConnection>
0011 #include <QDBusConnectionInterface>
0012 #include <QDBusServiceWatcher>
0013 #include <QGuiApplication>
0014 #include <QMenu>
0015 
0016 // Includes for the menu search.
0017 #include <KLocalizedString>
0018 #include <QLineEdit>
0019 #include <QListView>
0020 #include <QWidgetAction>
0021 
0022 #include <abstracttasksmodel.h>
0023 #include <dbusmenuimporter.h>
0024 
0025 class KDBusMenuImporter : public DBusMenuImporter
0026 {
0027 public:
0028     KDBusMenuImporter(const QString &service, const QString &path, QObject *parent)
0029         : DBusMenuImporter(service, path, parent)
0030     {
0031     }
0032 
0033 protected:
0034     QIcon iconForName(const QString &name) override
0035     {
0036         return QIcon::fromTheme(name);
0037     }
0038 };
0039 
0040 AppMenuModel::AppMenuModel(QObject *parent)
0041     : QAbstractListModel(parent)
0042     , m_tasksModel(new TaskManager::TasksModel(this))
0043     , m_serviceWatcher(new QDBusServiceWatcher(this))
0044 {
0045     m_tasksModel->setFilterByScreen(true);
0046     connect(m_tasksModel, &TaskManager::TasksModel::activeTaskChanged, this, &AppMenuModel::onActiveWindowChanged);
0047     connect(m_tasksModel,
0048             &TaskManager::TasksModel::dataChanged,
0049             [this](const QModelIndex &topLeft, const QModelIndex &bottomRight, const QList<int> &roles = QList<int>()) {
0050                 Q_UNUSED(topLeft)
0051                 Q_UNUSED(bottomRight)
0052                 if (roles.contains(TaskManager::AbstractTasksModel::ApplicationMenuObjectPath)
0053                     || roles.contains(TaskManager::AbstractTasksModel::ApplicationMenuServiceName) || roles.isEmpty()) {
0054                     onActiveWindowChanged();
0055                 }
0056             });
0057     connect(m_tasksModel, &TaskManager::TasksModel::activityChanged, this, &AppMenuModel::onActiveWindowChanged);
0058     connect(m_tasksModel, &TaskManager::TasksModel::virtualDesktopChanged, this, &AppMenuModel::onActiveWindowChanged);
0059     connect(m_tasksModel, &TaskManager::TasksModel::countChanged, this, &AppMenuModel::onActiveWindowChanged);
0060     connect(m_tasksModel, &TaskManager::TasksModel::screenGeometryChanged, this, &AppMenuModel::screenGeometryChanged);
0061 
0062     connect(this, &AppMenuModel::modelNeedsUpdate, this, [this] {
0063         if (!m_updatePending) {
0064             m_updatePending = true;
0065             QMetaObject::invokeMethod(this, "update", Qt::QueuedConnection);
0066         }
0067     });
0068 
0069     onActiveWindowChanged();
0070 
0071     m_serviceWatcher->setConnection(QDBusConnection::sessionBus());
0072     // if our current DBus connection gets lost, close the menu
0073     // we'll select the new menu when the focus changes
0074     connect(m_serviceWatcher, &QDBusServiceWatcher::serviceUnregistered, this, [this](const QString &serviceName) {
0075         if (serviceName == m_serviceName) {
0076             setMenuAvailable(false);
0077             Q_EMIT modelNeedsUpdate();
0078         }
0079     });
0080 
0081     // X11 has funky menu behaviour that prevents this from working properly.
0082     if (KWindowSystem::isPlatformWayland()) {
0083         m_searchAction = new QAction(this);
0084         m_searchAction->setText(i18n("Search"));
0085         m_searchAction->setObjectName(QStringLiteral("appmenu"));
0086 
0087         m_searchMenu.reset(new QMenu);
0088         auto searchAction = new QWidgetAction(this);
0089         auto searchBar = new QLineEdit;
0090         searchBar->setClearButtonEnabled(true);
0091         searchBar->setPlaceholderText(i18n("Search…"));
0092         searchBar->setMinimumWidth(200);
0093         searchBar->setContentsMargins(4, 4, 4, 4);
0094         connect(m_tasksModel, &TaskManager::TasksModel::activeTaskChanged, [searchBar]() {
0095             searchBar->setText(QString());
0096         });
0097         connect(searchBar, &QLineEdit::textChanged, [searchBar, this]() mutable {
0098             insertSearchActionsIntoMenu(searchBar->text());
0099         });
0100         connect(searchBar, &QLineEdit::returnPressed, [this]() mutable {
0101             if (!m_currentSearchActions.empty()) {
0102                 m_currentSearchActions.constFirst()->trigger();
0103             }
0104         });
0105         connect(this, &AppMenuModel::modelNeedsUpdate, this, [this, searchBar]() mutable {
0106             insertSearchActionsIntoMenu(searchBar->text());
0107         });
0108         searchAction->setDefaultWidget(searchBar);
0109         m_searchMenu->addAction(searchAction);
0110         m_searchMenu->addSeparator();
0111         m_searchAction->setMenu(m_searchMenu.get());
0112     }
0113 }
0114 
0115 AppMenuModel::~AppMenuModel() = default;
0116 
0117 bool AppMenuModel::menuAvailable() const
0118 {
0119     return m_menuAvailable;
0120 }
0121 
0122 void AppMenuModel::setMenuAvailable(bool set)
0123 {
0124     if (m_menuAvailable != set) {
0125         m_menuAvailable = set;
0126         setVisible(true);
0127         Q_EMIT menuAvailableChanged();
0128     }
0129 }
0130 
0131 bool AppMenuModel::visible() const
0132 {
0133     return m_visible;
0134 }
0135 
0136 void AppMenuModel::setVisible(bool visible)
0137 {
0138     if (m_visible != visible) {
0139         m_visible = visible;
0140         Q_EMIT visibleChanged();
0141     }
0142 }
0143 
0144 QRect AppMenuModel::screenGeometry() const
0145 {
0146     return m_tasksModel->screenGeometry();
0147 }
0148 
0149 void AppMenuModel::setScreenGeometry(QRect geometry)
0150 {
0151     m_tasksModel->setScreenGeometry(geometry);
0152 }
0153 
0154 int AppMenuModel::rowCount(const QModelIndex &parent) const
0155 {
0156     Q_UNUSED(parent);
0157     if (!m_menuAvailable || !m_menu) {
0158         return 0;
0159     }
0160 
0161     return m_menu->actions().count() + (m_searchAction ? 1 : 0);
0162 }
0163 
0164 void AppMenuModel::removeSearchActionsFromMenu()
0165 {
0166     for (auto action : std::as_const(m_currentSearchActions)) {
0167         m_searchAction->menu()->removeAction(action);
0168     }
0169     m_currentSearchActions = QList<QAction *>();
0170 }
0171 
0172 void AppMenuModel::insertSearchActionsIntoMenu(const QString &filter)
0173 {
0174     removeSearchActionsFromMenu();
0175     if (filter.isEmpty()) {
0176         return;
0177     }
0178     const auto actions = flatActionList();
0179     for (const auto &action : actions) {
0180         if (action->text().contains(filter, Qt::CaseInsensitive)) {
0181             m_searchAction->menu()->addAction(action);
0182             m_currentSearchActions << action;
0183         }
0184     }
0185 }
0186 
0187 void AppMenuModel::update()
0188 {
0189     beginResetModel();
0190     endResetModel();
0191     m_updatePending = false;
0192 }
0193 
0194 void AppMenuModel::onActiveWindowChanged()
0195 {
0196     // Do not change active window when panel gets focus
0197     // See ShellCorona::init() in shell/shellcorona.cpp
0198     if (m_containmentStatus == Plasma::Types::AcceptingInputStatus) {
0199         return;
0200     }
0201 
0202     const QModelIndex activeTaskIndex = m_tasksModel->activeTask();
0203     const QString objectPath = m_tasksModel->data(activeTaskIndex, TaskManager::AbstractTasksModel::ApplicationMenuObjectPath).toString();
0204     const QString serviceName = m_tasksModel->data(activeTaskIndex, TaskManager::AbstractTasksModel::ApplicationMenuServiceName).toString();
0205 
0206     if (!objectPath.isEmpty() && !serviceName.isEmpty()) {
0207         setMenuAvailable(true);
0208         updateApplicationMenu(serviceName, objectPath);
0209         setVisible(true);
0210         Q_EMIT modelNeedsUpdate();
0211     } else {
0212         setMenuAvailable(false);
0213         setVisible(false);
0214     }
0215 }
0216 
0217 QHash<int, QByteArray> AppMenuModel::roleNames() const
0218 {
0219     QHash<int, QByteArray> roleNames;
0220     roleNames[MenuRole] = QByteArrayLiteral("activeMenu");
0221     roleNames[ActionRole] = QByteArrayLiteral("activeActions");
0222     return roleNames;
0223 }
0224 
0225 QList<QAction *> AppMenuModel::flatActionList()
0226 {
0227     QList<QAction *> ret;
0228     if (!m_menuAvailable || !m_menu) {
0229         return ret;
0230     }
0231     const auto actions = m_menu->findChildren<QAction *>();
0232     for (auto &action : actions) {
0233         if (action->menu() == nullptr) {
0234             ret << action;
0235         }
0236     }
0237     return ret;
0238 }
0239 
0240 QVariant AppMenuModel::data(const QModelIndex &index, int role) const
0241 {
0242     if (!m_menuAvailable || !m_menu) {
0243         return QVariant();
0244     }
0245 
0246     if (!index.isValid()) {
0247         if (role == MenuRole) {
0248             return QString();
0249         } else if (role == ActionRole) {
0250             return QVariant::fromValue(m_menu->menuAction());
0251         }
0252     }
0253 
0254     const auto actions = m_menu->actions();
0255     const int row = index.row();
0256     if (row == actions.count() && m_searchAction) {
0257         if (role == MenuRole) {
0258             return m_searchAction->text();
0259         } else if (role == ActionRole) {
0260             return QVariant::fromValue(m_searchAction.data());
0261         }
0262     }
0263     if (row >= actions.count()) {
0264         return QVariant();
0265     }
0266 
0267     if (role == MenuRole) { // TODO this should be Qt::DisplayRole
0268         return actions.at(row)->text();
0269     } else if (role == ActionRole) {
0270         return QVariant::fromValue(actions.at(row));
0271     }
0272 
0273     return QVariant();
0274 }
0275 
0276 void AppMenuModel::updateApplicationMenu(const QString &serviceName, const QString &menuObjectPath)
0277 {
0278     if (m_serviceName == serviceName && m_menuObjectPath == menuObjectPath) {
0279         if (m_importer) {
0280             QMetaObject::invokeMethod(m_importer, "updateMenu", Qt::QueuedConnection);
0281         }
0282         return;
0283     }
0284 
0285     m_serviceName = serviceName;
0286     m_serviceWatcher->setWatchedServices(QStringList({m_serviceName}));
0287 
0288     m_menuObjectPath = menuObjectPath;
0289 
0290     if (m_importer) {
0291         m_importer->deleteLater();
0292     }
0293 
0294     m_importer = new KDBusMenuImporter(serviceName, menuObjectPath, this);
0295     QMetaObject::invokeMethod(m_importer, "updateMenu", Qt::QueuedConnection);
0296 
0297     connect(m_importer.data(), &DBusMenuImporter::menuUpdated, this, [=, this](QMenu *menu) {
0298         m_menu = m_importer->menu();
0299         if (m_menu.isNull() || menu != m_menu) {
0300             return;
0301         }
0302 
0303         // cache first layer of sub menus, which we'll be popping up
0304         const auto actions = m_menu->actions();
0305         for (QAction *a : actions) {
0306             // signal dataChanged when the action changes
0307             connect(a, &QAction::changed, this, [this, a] {
0308                 if (m_menuAvailable && m_menu) {
0309                     const int actionIdx = m_menu->actions().indexOf(a);
0310                     if (actionIdx > -1) {
0311                         const QModelIndex modelIdx = index(actionIdx, 0);
0312                         Q_EMIT dataChanged(modelIdx, modelIdx);
0313                     }
0314                 }
0315             });
0316 
0317             connect(a, &QAction::destroyed, this, &AppMenuModel::modelNeedsUpdate);
0318 
0319             if (a->menu()) {
0320                 m_importer->updateMenu(a->menu());
0321             }
0322         }
0323 
0324         setMenuAvailable(true);
0325         Q_EMIT modelNeedsUpdate();
0326     });
0327 
0328     connect(m_importer.data(), &DBusMenuImporter::actionActivationRequested, this, [this](QAction *action) {
0329         // TODO submenus
0330         if (!m_menuAvailable || !m_menu) {
0331             return;
0332         }
0333 
0334         const auto actions = m_menu->actions();
0335         auto it = std::find(actions.begin(), actions.end(), action);
0336         if (it != actions.end()) {
0337             Q_EMIT requestActivateIndex(it - actions.begin());
0338         }
0339     });
0340 }