You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
334 lines
10 KiB
334 lines
10 KiB
/* |
|
SPDX-FileCopyrightText: 2016 Kai Uwe Broulik <kde@privat.broulik.de> |
|
SPDX-FileCopyrightText: 2016 Chinmoy Ranjan Pradhan <chinmoyrp65@gmail.com> |
|
|
|
SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL |
|
*/ |
|
|
|
#include "appmenumodel.h" |
|
|
|
#include <QDBusConnection> |
|
#include <QDBusConnectionInterface> |
|
#include <QDBusServiceWatcher> |
|
#include <QGuiApplication> |
|
#include <QMenu> |
|
|
|
// Includes for the menu search. |
|
#include <KLocalizedString> |
|
#include <QLineEdit> |
|
#include <QListView> |
|
#include <QWidgetAction> |
|
|
|
#include <abstracttasksmodel.h> |
|
#include <dbusmenuimporter.h> |
|
|
|
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, |
|
[=](const QModelIndex &topLeft, const QModelIndex &bottomRight, const QVector<int> &roles = QVector<int>()) { |
|
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->setText(QString()); |
|
}); |
|
connect(searchBar, &QLineEdit::textChanged, [=]() mutable { |
|
insertSearchActionsIntoMenu(searchBar->text()); |
|
}); |
|
connect(searchBar, &QLineEdit::returnPressed, [=]() mutable { |
|
if (m_currentSearchActions.first()) { |
|
m_currentSearchActions.first()->trigger(); |
|
} |
|
}); |
|
connect(this, &AppMenuModel::modelNeedsUpdate, this, [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() + (KWindowSystem::isPlatformWayland() ? 1 : 0); |
|
} |
|
|
|
void AppMenuModel::removeSearchActionsFromMenu() |
|
{ |
|
for (const auto &action : std::as_const(m_currentSearchActions)) { |
|
m_searchAction->menu()->removeAction(action); |
|
} |
|
m_currentSearchActions = QList<QAction *>(); |
|
} |
|
|
|
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() |
|
{ |
|
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<int, QByteArray> AppMenuModel::roleNames() const |
|
{ |
|
QHash<int, QByteArray> roleNames; |
|
roleNames[MenuRole] = QByteArrayLiteral("activeMenu"); |
|
roleNames[ActionRole] = QByteArrayLiteral("activeActions"); |
|
return roleNames; |
|
} |
|
|
|
QList<QAction *> AppMenuModel::flatActionList() |
|
{ |
|
QList<QAction *> ret; |
|
if (!m_menuAvailable || !m_menu) { |
|
return ret; |
|
} |
|
const auto actions = m_menu->findChildren<QAction *>(); |
|
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() && KWindowSystem::isPlatformWayland()) { |
|
if (role == MenuRole) { |
|
return m_searchAction->text(); |
|
} else if (role == ActionRole) { |
|
return QVariant::fromValue((void *)m_searchAction); |
|
} |
|
} |
|
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, [=](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()); |
|
} |
|
}); |
|
}
|
|
|