diff --git a/applets/appmenu/package/contents/ui/main.qml b/applets/appmenu/package/contents/ui/main.qml index 41d729adf..83d7ba03e 100644 --- a/applets/appmenu/package/contents/ui/main.qml +++ b/applets/appmenu/package/contents/ui/main.qml @@ -1,156 +1,157 @@ /* * Copyright 2013 Heena Mahour * Copyright 2013 Sebastian Kügler * Copyright 2016 Kai Uwe Broulik * * 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) any later version. * * 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 . */ import QtQuick 2.0 import QtQuick.Layouts 1.1 import QtQuick.Controls 1.4 import org.kde.plasma.plasmoid 2.0 import org.kde.kquickcontrolsaddons 2.0 import org.kde.plasma.core 2.0 as PlasmaCore import org.kde.plasma.components 2.0 as PlasmaComponents import org.kde.plasma.private.appmenu 1.0 as AppMenuPrivate Item { id: root readonly property bool vertical: plasmoid.formFactor === PlasmaCore.Types.Vertical readonly property bool view: plasmoid.configuration.compactView readonly property bool menuAvailable: appMenuModel.menuAvailable readonly property bool kcmAuthorized: KCMShell.authorize(["style.desktop"]).length > 0 onViewChanged: { plasmoid.nativeInterface.view = view } Plasmoid.preferredRepresentation: (plasmoid.configuration.compactView || vertical) ? Plasmoid.compactRepresentation : Plasmoid.fullRepresentation Plasmoid.compactRepresentation: PlasmaComponents.ToolButton { readonly property int fakeIndex: 0 Layout.fillWidth: false Layout.fillHeight: false Layout.minimumWidth: implicitWidth Layout.maximumWidth: implicitWidth enabled: menuAvailable checkable: menuAvailable && plasmoid.nativeInterface.currentIndex === fakeIndex checked: checkable iconSource: i18n("application-menu") onClicked: plasmoid.nativeInterface.trigger(this, 0); } Plasmoid.fullRepresentation: GridLayout { id: buttonGrid Plasmoid.status: { if (menuAvailable && plasmoid.nativeInterface.currentIndex > -1 && buttonRepeater.count > 0) { return PlasmaCore.Types.NeedsAttentionStatus; } else if (menuAvailable){ //when we're not enabled set to active to show the configure button return buttonRepeater.count > 0 ? PlasmaCore.Types.ActiveStatus : PlasmaCore.Types.HiddenStatus; } else { return PlasmaCore.Types.PassiveStatus; } } Layout.minimumWidth: implicitWidth Layout.minimumHeight: implicitHeight flow: root.vertical ? GridLayout.TopToBottom : GridLayout.LeftToRight rowSpacing: units.smallSpacing columnSpacing: units.smallSpacing Component.onCompleted: { plasmoid.nativeInterface.buttonGrid = buttonGrid // using a Connections {} doesn't work for some reason in Qt >= 5.8 plasmoid.nativeInterface.requestActivateIndex.connect(function (index) { var idx = Math.max(0, Math.min(buttonRepeater.count - 1, index)) var button = buttonRepeater.itemAt(index) if (button) { button.clicked() } }); plasmoid.activated.connect(function () { var button = buttonRepeater.itemAt(0); if (button) { button.clicked(); } }); } // So we can show mnemonic underlines only while Alt is pressed PlasmaCore.DataSource { id: keystateSource engine: "keystate" connectedSources: ["Alt"] } Repeater { id: buttonRepeater - model: appMenuModel + model: !appMenuModel.menuHidden ? appMenuModel : null PlasmaComponents.ToolButton { readonly property int buttonIndex: index Layout.preferredWidth: minimumWidth Layout.fillWidth: root.vertical Layout.fillHeight: !root.vertical text: { var text = activeMenu; var alt = keystateSource.data.Alt; if (!alt || !alt.Pressed) { // StyleHelpers.removeMnemonics text = text.replace(/([^&]*)&(.)([^&]*)/g, function (match, p1, p2, p3) { return p1.concat(p2, p3); }); } return text; } // fake highlighted checkable: plasmoid.nativeInterface.currentIndex === index checked: checkable visible: text !== "" onClicked: { plasmoid.nativeInterface.trigger(this, index) checked = Qt.binding(function() { return plasmoid.nativeInterface.currentIndex === index; }); } // QMenu opens on press, so we'll replicate that here MouseArea { anchors.fill: parent onPressed: parent.clicked() } } } } AppMenuPrivate.AppMenuModel { id: appMenuModel + screenGeometry: plasmoid.screenGeometry onRequestActivateIndex: plasmoid.nativeInterface.requestActivateIndex(index) Component.onCompleted: { plasmoid.nativeInterface.model = appMenuModel } } } diff --git a/applets/appmenu/plugin/appmenumodel.cpp b/applets/appmenu/plugin/appmenumodel.cpp index 1b5dc7bd6..394f3e263 100644 --- a/applets/appmenu/plugin/appmenumodel.cpp +++ b/applets/appmenu/plugin/appmenumodel.cpp @@ -1,343 +1,395 @@ /****************************************************************** * Copyright 2016 Kai Uwe Broulik * Copyright 2016 Chinmoy Ranjan Pradhan * * * 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 . * ******************************************************************/ #include "appmenumodel.h" #include #if HAVE_X11 #include #include #endif #include #include #include #include #include #include #include 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 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(KWindowSystem::self() + , static_cast(&KWindowSystem::windowChanged) + , this + , &AppMenuModel::onWindowChanged); + connect(this, &AppMenuModel::modelNeedsUpdate, this, [this] { if (!m_updatePending) { m_updatePending = true; QMetaObject::invokeMethod(this, "update", Qt::QueuedConnection); } }); + + connect(this, &AppMenuModel::screenGeometryChanged, this, [this] { + onWindowChanged(m_currentWindowId); + }); + 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; + setMenuHidden(false); emit menuAvailableChanged(); } } +bool AppMenuModel::menuHidden() const +{ + return m_menuHidden; +} + +void AppMenuModel::setMenuHidden(bool hide) +{ + if (m_menuHidden != hide) { + m_menuHidden = hide; + emit menuHiddenChanged(); + } +} + +QRect AppMenuModel::screenGeometry() const +{ + return m_screenGeometry; +} + +void AppMenuModel::setScreenGeometry(QRect geometry) +{ + if (m_screenGeometry == geometry) { + return; + } + + m_screenGeometry = geometry; + emit screenGeometryChanged(); +} + int AppMenuModel::rowCount(const QModelIndex &parent) const { Q_UNUSED(parent); if (!m_menuAvailable || !m_menu) { return 0; } return m_menu->actions().count(); } void AppMenuModel::update() { beginResetModel(); endResetModel(); m_updatePending = false; } 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 atomReply(xcb_intern_atom_reply(c, atomCookie, 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 propertyReply(xcb_get_property_reply(c, propertyCookie, nullptr)); 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; } + m_currentWindowId = id; + WId transientId = info.transientFor(); // lok at transient windows first while (transientId) { if (updateMenuFromWindowIfHasMenu(transientId)) { + setMenuHidden(false); return; } transientId = KWindowInfo(transientId, nullptr, NET::WM2TransientFor).transientFor(); } if (updateMenuFromWindowIfHasMenu(id)) { + setMenuHidden(false); 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; + m_delayedMenuWindowId = id; //no menu found, set it to unavailable setMenuAvailable(false); emit modelNeedsUpdate(); } #endif } +void AppMenuModel::onWindowChanged(WId id) +{ + if (m_currentWindowId == id) { + KWindowInfo info(id, NET::WMState | NET::WMGeometry); + const bool contained = m_screenGeometry.isNull() || m_screenGeometry.contains(info.geometry().center()); + + setMenuHidden(info.isMinimized() || !contained); + } +} QHash AppMenuModel::roleNames() const { QHash roleNames; roleNames[MenuRole] = QByteArrayLiteral("activeMenu"); roleNames[ActionRole] = QByteArrayLiteral("activeActions"); return roleNames; } QVariant AppMenuModel::data(const QModelIndex &index, int role) const { const int row = index.row(); if (row < 0 || !m_menuAvailable || !m_menu) { return QVariant(); } const auto actions = m_menu->actions(); 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 qVariantFromValue((void *) 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 for(QAction *a: m_menu->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); emit dataChanged(modelIdx, modelIdx); } } }); connect(a, &QAction::destroyed, this, &AppMenuModel::modelNeedsUpdate); if (a->menu()) { m_importer->updateMenu(a->menu()); } } setMenuAvailable(true); 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()) { requestActivateIndex(it - actions.begin()); } }); } 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(message); const uint8_t type = e->response_type & ~0x80; if (type == XCB_PROPERTY_NOTIFY) { auto *event = reinterpret_cast(e); - if (event->window == m_currentWindowId) { + if (event->window == m_delayedMenuWindowId) { 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; } diff --git a/applets/appmenu/plugin/appmenumodel.h b/applets/appmenu/plugin/appmenumodel.h index 32fba6a5c..b2c7b3ee5 100644 --- a/applets/appmenu/plugin/appmenumodel.h +++ b/applets/appmenu/plugin/appmenumodel.h @@ -1,90 +1,109 @@ /****************************************************************** * Copyright 2016 Chinmoy Ranjan Pradhan * * 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 . * ******************************************************************/ #ifndef APPMENUMODEL_H #define APPMENUMODEL_H #include #include #include #include #include +#include class QMenu; class QAction; class QModelIndex; class QDBusServiceWatcher; class KDBusMenuImporter; class AppMenuModel : public QAbstractListModel, public QAbstractNativeEventFilter { Q_OBJECT Q_PROPERTY(bool menuAvailable READ menuAvailable WRITE setMenuAvailable NOTIFY menuAvailableChanged) + Q_PROPERTY(bool menuHidden READ menuHidden NOTIFY menuHiddenChanged) + + Q_PROPERTY(QRect screenGeometry READ screenGeometry WRITE setScreenGeometry NOTIFY screenGeometryChanged) public: explicit AppMenuModel(QObject *parent = nullptr); ~AppMenuModel() override; enum AppMenuRole { MenuRole = Qt::UserRole+1, // TODO this should be Qt::DisplayRole ActionRole }; QVariant data(const QModelIndex &index, int role) const override; int rowCount(const QModelIndex &parent = QModelIndex()) const override; QHash roleNames() const override; void updateApplicationMenu(const QString &serviceName, const QString &menuObjectPath); bool menuAvailable() const; void setMenuAvailable(bool set); + bool menuHidden() const; + + QRect screenGeometry() const; + void setScreenGeometry(QRect geometry); + signals: void requestActivateIndex(int index); protected: bool nativeEventFilter(const QByteArray &eventType, void *message, long int *result) override; private Q_SLOTS: void onActiveWindowChanged(WId id); + void onWindowChanged(WId id); + void setMenuHidden(bool hide); void update(); signals: void menuAvailableChanged(); + void menuHiddenChanged(); void modelNeedsUpdate(); + void screenGeometryChanged(); private: bool m_menuAvailable; + bool m_menuHidden = false; bool m_updatePending = false; + QRect m_screenGeometry; + + //! current active window used WId m_currentWindowId = 0; + //! window that its menu initialization may be delayed + WId m_delayedMenuWindowId = 0; QPointer m_menu; QDBusServiceWatcher *m_serviceWatcher; QString m_serviceName; QString m_menuObjectPath; QPointer m_importer; }; #endif