/* SPDX-FileCopyrightText: 2016 Kai Uwe Broulik SPDX-FileCopyrightText: 2016 Chinmoy Ranjan Pradhan SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL */ #include "appmenumodel.h" #include #include #include #include #include // Includes for the menu search. #include #include #include #include #include #include class KDBusMenuImporter : public DBusMenuImporter { public: KDBusMenuImporter(const QString &service, const QString &path, QObject *parent) : DBusMenuImporter(service, path, parent) { } protected: QIcon iconForName(const QString &name) override { return QIcon::fromTheme(name); } }; AppMenuModel::AppMenuModel(QObject *parent) : QAbstractListModel(parent) , m_tasksModel(new TaskManager::TasksModel(this)) , m_serviceWatcher(new QDBusServiceWatcher(this)) { m_tasksModel->setFilterByScreen(true); connect(m_tasksModel, &TaskManager::TasksModel::activeTaskChanged, this, &AppMenuModel::onActiveWindowChanged); connect(m_tasksModel, &TaskManager::TasksModel::dataChanged, [this](const QModelIndex &topLeft, const QModelIndex &bottomRight, const QList &roles = QList()) { Q_UNUSED(topLeft) Q_UNUSED(bottomRight) if (roles.contains(TaskManager::AbstractTasksModel::ApplicationMenuObjectPath) || roles.contains(TaskManager::AbstractTasksModel::ApplicationMenuServiceName) || roles.isEmpty()) { onActiveWindowChanged(); } }); connect(m_tasksModel, &TaskManager::TasksModel::activityChanged, this, &AppMenuModel::onActiveWindowChanged); connect(m_tasksModel, &TaskManager::TasksModel::virtualDesktopChanged, this, &AppMenuModel::onActiveWindowChanged); connect(m_tasksModel, &TaskManager::TasksModel::countChanged, this, &AppMenuModel::onActiveWindowChanged); connect(m_tasksModel, &TaskManager::TasksModel::screenGeometryChanged, this, &AppMenuModel::screenGeometryChanged); connect(this, &AppMenuModel::modelNeedsUpdate, this, [this] { if (!m_updatePending) { m_updatePending = true; QMetaObject::invokeMethod(this, "update", Qt::QueuedConnection); } }); onActiveWindowChanged(); m_serviceWatcher->setConnection(QDBusConnection::sessionBus()); // if our current DBus connection gets lost, close the menu // we'll select the new menu when the focus changes connect(m_serviceWatcher, &QDBusServiceWatcher::serviceUnregistered, this, [this](const QString &serviceName) { if (serviceName == m_serviceName) { setMenuAvailable(false); Q_EMIT modelNeedsUpdate(); } }); // X11 has funky menu behaviour that prevents this from working properly. if (KWindowSystem::isPlatformWayland()) { m_searchAction = new QAction(this); m_searchAction->setText(i18n("Search")); m_searchAction->setObjectName(QStringLiteral("appmenu")); m_searchMenu.reset(new QMenu); auto searchAction = new QWidgetAction(this); auto searchBar = new QLineEdit; searchBar->setClearButtonEnabled(true); searchBar->setPlaceholderText(i18n("Search…")); searchBar->setMinimumWidth(200); searchBar->setContentsMargins(4, 4, 4, 4); connect(m_tasksModel, &TaskManager::TasksModel::activeTaskChanged, searchBar, [searchBar]() { searchBar->setText(QString()); }); connect(searchBar, &QLineEdit::textChanged, this, [searchBar, this]() mutable { insertSearchActionsIntoMenu(searchBar->text()); }); connect(searchBar, &QLineEdit::returnPressed, this, [this]() mutable { if (!m_currentSearchActions.empty()) { m_currentSearchActions.constFirst()->trigger(); } }); connect(this, &AppMenuModel::modelNeedsUpdate, searchBar, [this, searchBar]() mutable { insertSearchActionsIntoMenu(searchBar->text()); }); searchAction->setDefaultWidget(searchBar); m_searchMenu->addAction(searchAction); m_searchMenu->addSeparator(); m_searchAction->setMenu(m_searchMenu.get()); } } AppMenuModel::~AppMenuModel() = default; bool AppMenuModel::menuAvailable() const { return m_menuAvailable; } void AppMenuModel::setMenuAvailable(bool set) { if (m_menuAvailable != set) { m_menuAvailable = set; setVisible(true); Q_EMIT menuAvailableChanged(); } } bool AppMenuModel::visible() const { return m_visible; } void AppMenuModel::setVisible(bool visible) { if (m_visible != visible) { m_visible = visible; Q_EMIT visibleChanged(); } } QRect AppMenuModel::screenGeometry() const { return m_tasksModel->screenGeometry(); } void AppMenuModel::setScreenGeometry(QRect geometry) { m_tasksModel->setScreenGeometry(geometry); } int AppMenuModel::rowCount(const QModelIndex &parent) const { Q_UNUSED(parent); if (!m_menuAvailable || !m_menu) { return 0; } return m_menu->actions().count() + (m_searchAction ? 1 : 0); } void AppMenuModel::removeSearchActionsFromMenu() { for (auto action : std::as_const(m_currentSearchActions)) { m_searchAction->menu()->removeAction(action); } m_currentSearchActions = QList(); } void AppMenuModel::insertSearchActionsIntoMenu(const QString &filter) { removeSearchActionsFromMenu(); if (filter.isEmpty()) { return; } const auto actions = flatActionList(); for (const auto &action : actions) { if (action->text().contains(filter, Qt::CaseInsensitive)) { m_searchAction->menu()->addAction(action); m_currentSearchActions << action; } } } void AppMenuModel::update() { beginResetModel(); endResetModel(); m_updatePending = false; } void AppMenuModel::onActiveWindowChanged() { // Do not change active window when panel gets focus // See ShellCorona::init() in shell/shellcorona.cpp if (m_containmentStatus == Plasma::Types::AcceptingInputStatus) { return; } const QModelIndex activeTaskIndex = m_tasksModel->activeTask(); const QString objectPath = m_tasksModel->data(activeTaskIndex, TaskManager::AbstractTasksModel::ApplicationMenuObjectPath).toString(); const QString serviceName = m_tasksModel->data(activeTaskIndex, TaskManager::AbstractTasksModel::ApplicationMenuServiceName).toString(); if (!objectPath.isEmpty() && !serviceName.isEmpty()) { setMenuAvailable(true); updateApplicationMenu(serviceName, objectPath); setVisible(true); Q_EMIT modelNeedsUpdate(); } else { setMenuAvailable(false); setVisible(false); } } QHash AppMenuModel::roleNames() const { QHash roleNames; roleNames[MenuRole] = QByteArrayLiteral("activeMenu"); roleNames[ActionRole] = QByteArrayLiteral("activeActions"); return roleNames; } QList AppMenuModel::flatActionList() { QList ret; if (!m_menuAvailable || !m_menu) { return ret; } const auto actions = m_menu->findChildren(); for (auto &action : actions) { if (action->menu() == nullptr) { ret << action; } } return ret; } QVariant AppMenuModel::data(const QModelIndex &index, int role) const { if (!m_menuAvailable || !m_menu) { return QVariant(); } if (!index.isValid()) { if (role == MenuRole) { return QString(); } else if (role == ActionRole) { return QVariant::fromValue(m_menu->menuAction()); } } const auto actions = m_menu->actions(); const int row = index.row(); if (row == actions.count() && m_searchAction) { if (role == MenuRole) { return m_searchAction->text(); } else if (role == ActionRole) { return QVariant::fromValue(m_searchAction.data()); } } if (row >= actions.count()) { return QVariant(); } if (role == MenuRole) { // TODO this should be Qt::DisplayRole return actions.at(row)->text(); } else if (role == ActionRole) { return QVariant::fromValue(actions.at(row)); } return QVariant(); } void AppMenuModel::updateApplicationMenu(const QString &serviceName, const QString &menuObjectPath) { if (m_serviceName == serviceName && m_menuObjectPath == menuObjectPath) { if (m_importer) { QMetaObject::invokeMethod(m_importer, "updateMenu", Qt::QueuedConnection); } return; } m_serviceName = serviceName; m_serviceWatcher->setWatchedServices(QStringList({m_serviceName})); m_menuObjectPath = menuObjectPath; if (m_importer) { m_importer->deleteLater(); } m_importer = new KDBusMenuImporter(serviceName, menuObjectPath, this); QMetaObject::invokeMethod(m_importer, "updateMenu", Qt::QueuedConnection); connect(m_importer.data(), &DBusMenuImporter::menuUpdated, this, [=, this](QMenu *menu) { m_menu = m_importer->menu(); if (m_menu.isNull() || menu != m_menu) { return; } // cache first layer of sub menus, which we'll be popping up const auto actions = m_menu->actions(); for (QAction *a : actions) { // signal dataChanged when the action changes connect(a, &QAction::changed, this, [this, a] { if (m_menuAvailable && m_menu) { const int actionIdx = m_menu->actions().indexOf(a); if (actionIdx > -1) { const QModelIndex modelIdx = index(actionIdx, 0); Q_EMIT dataChanged(modelIdx, modelIdx); } } }); connect(a, &QAction::destroyed, this, &AppMenuModel::modelNeedsUpdate); if (a->menu()) { m_importer->updateMenu(a->menu()); } } setMenuAvailable(true); Q_EMIT modelNeedsUpdate(); }); connect(m_importer.data(), &DBusMenuImporter::actionActivationRequested, this, [this](QAction *action) { // TODO submenus if (!m_menuAvailable || !m_menu) { return; } const auto actions = m_menu->actions(); auto it = std::find(actions.begin(), actions.end(), action); if (it != actions.end()) { Q_EMIT requestActivateIndex(it - actions.begin()); } }); }