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 }