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.
 
 
 
 
 
 

325 lines
10 KiB

/******************************************************************
* Copyright 2016 Kai Uwe Broulik <kde@privat.broulik.de>
* Copyright 2016 Chinmoy Ranjan Pradhan <chinmoyrp65@gmail.com>
*
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License as
* published by the Free Software Foundation; either version 2 of
* the License or (at your option) version 3 or any later version
* accepted by the membership of KDE e.V. (or its successor approved
* by the membership of KDE e.V.), which shall act as a proxy
* defined in Section 14 of version 3 of the license.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
******************************************************************/
#include "appmenumodel.h"
#include <config-X11.h>
#if HAVE_X11
#include <QX11Info>
#include <xcb/xcb.h>
#endif
#include <QAction>
#include <QGuiApplication>
#include <QMenu>
#include <QDebug>
#include <QDBusConnection>
#include <QDBusConnectionInterface>
#include <QDBusServiceWatcher>
#include <dbusmenuimporter.h>
static const QByteArray s_x11AppMenuServiceNamePropertyName = QByteArrayLiteral("_KDE_NET_WM_APPMENU_SERVICE_NAME");
static const QByteArray s_x11AppMenuObjectPathPropertyName = QByteArrayLiteral("_KDE_NET_WM_APPMENU_OBJECT_PATH");
#if HAVE_X11
static QHash<QByteArray, xcb_atom_t> s_atoms;
#endif
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_serviceWatcher(new QDBusServiceWatcher(this))
{
connect(KWindowSystem::self(), &KWindowSystem::activeWindowChanged, this, &AppMenuModel::onActiveWindowChanged);
connect(this, &AppMenuModel::modelNeedsUpdate, this, &AppMenuModel::update, Qt::UniqueConnection);
onActiveWindowChanged(KWindowSystem::activeWindow());
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);
emit modelNeedsUpdate();
}
});
}
AppMenuModel::~AppMenuModel() = default;
bool AppMenuModel::menuAvailable() const
{
return m_menuAvailable;
}
void AppMenuModel::setMenuAvailable(bool set)
{
if (m_menuAvailable != set) {
m_menuAvailable = set;
emit menuAvailableChanged();
}
}
int AppMenuModel::rowCount(const QModelIndex &parent) const
{
Q_UNUSED(parent);
return m_activeMenu.count();
}
void AppMenuModel::update()
{
beginResetModel();
if (!m_activeMenu.isEmpty() && !m_activeActions.isEmpty()) {
m_activeMenu.clear();
m_activeActions.clear();
}
if (m_menu && m_menuAvailable) {
const auto &actions = m_menu->actions();
for (QAction *action : actions) {
m_activeActions.append(action);
m_activeMenu.append(action->text());
}
}
endResetModel();
}
void AppMenuModel::onActiveWindowChanged(WId id)
{
qApp->removeNativeEventFilter(this);
if (!id) {
setMenuAvailable(false);
emit modelNeedsUpdate();
return;
}
#if HAVE_X11
if (KWindowSystem::isPlatformX11()) {
auto *c = QX11Info::connection();
auto getWindowPropertyString = [c, this](WId id, const QByteArray &name) -> QByteArray {
QByteArray value;
if (!s_atoms.contains(name)) {
const xcb_intern_atom_cookie_t atomCookie = xcb_intern_atom(c, false, name.length(), name.constData());
QScopedPointer<xcb_intern_atom_reply_t, QScopedPointerPodDeleter> atomReply(xcb_intern_atom_reply(c, atomCookie, Q_NULLPTR));
if (atomReply.isNull()) {
return value;
}
s_atoms[name] = atomReply->atom;
if (s_atoms[name] == XCB_ATOM_NONE) {
return value;
}
}
static const long MAX_PROP_SIZE = 10000;
auto propertyCookie = xcb_get_property(c, false, id, s_atoms[name], XCB_ATOM_STRING, 0, MAX_PROP_SIZE);
QScopedPointer<xcb_get_property_reply_t, QScopedPointerPodDeleter> propertyReply(xcb_get_property_reply(c, propertyCookie, NULL));
if (propertyReply.isNull()) {
return value;
}
if (propertyReply->type == XCB_ATOM_STRING && propertyReply->format == 8 && propertyReply->value_len > 0) {
const char *data = (const char *) xcb_get_property_value(propertyReply.data());
int len = propertyReply->value_len;
if (data) {
value = QByteArray(data, data[len - 1] ? len : len - 1);
}
}
return value;
};
auto updateMenuFromWindowIfHasMenu = [this, &getWindowPropertyString](WId id) {
const QString serviceName = QString::fromUtf8(getWindowPropertyString(id, s_x11AppMenuServiceNamePropertyName));
const QString menuObjectPath = QString::fromUtf8(getWindowPropertyString(id, s_x11AppMenuObjectPathPropertyName));
if (!serviceName.isEmpty() && !menuObjectPath.isEmpty()) {
updateApplicationMenu(serviceName, menuObjectPath);
return true;
}
return false;
};
KWindowInfo info(id, NET::WMState | NET::WMWindowType, NET::WM2TransientFor);
if (info.hasState(NET::SkipTaskbar) ||
info.windowType(NET::UtilityMask) == NET::Utility ||
info.windowType(NET::DesktopMask) == NET::Desktop) {
return;
}
WId transientId = info.transientFor();
// lok at transient windows first
while (transientId) {
if (updateMenuFromWindowIfHasMenu(transientId)) {
return;
}
transientId = KWindowInfo(transientId, 0, NET::WM2TransientFor).transientFor();
}
if (updateMenuFromWindowIfHasMenu(id)) {
return;
}
// monitor whether an app menu becomes available later
// this can happen when an app starts, shows its window, and only later announces global menu (e.g. Firefox)
qApp->installNativeEventFilter(this);
m_currentWindowId = id;
//no menu found, set it to unavailable
setMenuAvailable(false);
emit modelNeedsUpdate();
}
#endif
}
QHash<int, QByteArray> AppMenuModel::roleNames() const
{
QHash<int, QByteArray> roleNames;
roleNames[MenuRole] = QByteArrayLiteral("activeMenu");
roleNames[ActionRole] = QByteArrayLiteral("activeActions");
return roleNames;
}
QVariant AppMenuModel::data(const QModelIndex &index, int role) const
{
int row = index.row();
if (row < 0 ) {
return QVariant();
}
if (role == MenuRole) {
return m_activeMenu.at(row);
} else if(role == ActionRole) {
const QVariant data = qVariantFromValue((void *) m_activeActions.at(row));
return data;
}
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
for(QAction *a: m_menu->actions()) {
if (a->menu()) {
m_importer->updateMenu(a->menu());
}
}
setMenuAvailable(true);
emit modelNeedsUpdate();
});
connect(m_importer.data(), &DBusMenuImporter::actionActivationRequested, this, [this](QAction *action) {
// TODO submenus
auto it = std::find(m_activeActions.constBegin(), m_activeActions.constEnd(), action);
if (it != m_activeActions.constEnd()) {
requestActivateIndex(it - m_activeActions.constBegin());
}
});
}
bool AppMenuModel::nativeEventFilter(const QByteArray &eventType, void *message, long *result)
{
Q_UNUSED(result);
if (!KWindowSystem::isPlatformX11() || eventType != "xcb_generic_event_t") {
return false;
}
#if HAVE_X11
auto e = static_cast<xcb_generic_event_t *>(message);
const uint8_t type = e->response_type & ~0x80;
if (type == XCB_PROPERTY_NOTIFY) {
auto *event = reinterpret_cast<xcb_property_notify_event_t *>(e);
if (event->window == m_currentWindowId) {
auto serviceNameAtom = s_atoms.value(s_x11AppMenuServiceNamePropertyName);
auto objectPathAtom = s_atoms.value(s_x11AppMenuObjectPathPropertyName);
if (serviceNameAtom != XCB_ATOM_NONE && objectPathAtom != XCB_ATOM_NONE) { // shouldn't happen
if (event->atom == serviceNameAtom || event->atom == objectPathAtom) {
// see if we now have a menu
onActiveWindowChanged(KWindowSystem::activeWindow());
}
}
}
}
#else
Q_UNUSED(message);
#endif
return false;
}