Index: plugin/appmenumodel.h =================================================================== --- plugin/appmenumodel.h +++ plugin/appmenumodel.h @@ -19,11 +19,15 @@ * ******************************************************************/ +#ifndef APPMENUMODEL_H +#define APPMENUMODEL_H + #include #include #include #include #include +#include class QMenu; class QAction; @@ -36,25 +40,33 @@ Q_OBJECT Q_PROPERTY(bool menuAvailable READ menuAvailable WRITE setMenuAvailable NOTIFY menuAvailableChanged) + Q_PROPERTY(bool visible READ visible NOTIFY visibleChanged) + + Q_PROPERTY(QRect screenGeometry READ screenGeometry WRITE setScreenGeometry NOTIFY screenGeometryChanged) public: - explicit AppMenuModel(QObject *parent = 0); - ~AppMenuModel(); + explicit AppMenuModel(QObject *parent = nullptr); + ~AppMenuModel() override; enum AppMenuRole { - MenuRole = Qt::UserRole+1, + MenuRole = Qt::UserRole+1, // TODO this should be Qt::DisplayRole ActionRole }; - QVariant data(const QModelIndex &index, int role) const Q_DECL_OVERRIDE; - int rowCount(const QModelIndex &parent = QModelIndex()) const Q_DECL_OVERRIDE; - QHash roleNames() const Q_DECL_OVERRIDE; + 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 visible() const; + + QRect screenGeometry() const; + void setScreenGeometry(QRect geometry); + signals: void requestActivateIndex(int index); @@ -63,20 +75,29 @@ private Q_SLOTS: void onActiveWindowChanged(WId id); + void onWindowChanged(WId id); + void setVisible(bool visible); void update(); signals: void menuAvailableChanged(); void modelNeedsUpdate(); + void screenGeometryChanged(); + void visibleChanged(); private: bool m_menuAvailable; + bool m_updatePending = false; + bool m_visible = true; + + 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; - QStringList m_activeMenu; - QList m_activeActions; QDBusServiceWatcher *m_serviceWatcher; QString m_serviceName; @@ -85,3 +106,4 @@ QPointer m_importer; }; +#endif Index: plugin/appmenumodel.cpp =================================================================== --- plugin/appmenumodel.cpp +++ plugin/appmenumodel.cpp @@ -31,12 +31,11 @@ #endif #include -#include #include -#include #include #include #include +#include #include @@ -70,7 +69,22 @@ m_serviceWatcher(new QDBusServiceWatcher(this)) { connect(KWindowSystem::self(), &KWindowSystem::activeWindowChanged, this, &AppMenuModel::onActiveWindowChanged); - connect(this, &AppMenuModel::modelNeedsUpdate, this, &AppMenuModel::update, Qt::UniqueConnection); + 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()); @@ -96,33 +110,54 @@ { if (m_menuAvailable != set) { m_menuAvailable = set; + setVisible(true); emit menuAvailableChanged(); } } -int AppMenuModel::rowCount(const QModelIndex &parent) const +bool AppMenuModel::visible() const { - Q_UNUSED(parent); - return m_activeMenu.count(); + return m_visible; } -void AppMenuModel::update() +void AppMenuModel::setVisible(bool visible) { - beginResetModel(); - if (!m_activeMenu.isEmpty() && !m_activeActions.isEmpty()) { - m_activeMenu.clear(); - m_activeActions.clear(); + if (m_visible != visible) { + m_visible = visible; + emit visibleChanged(); } +} - if (m_menu && m_menuAvailable) { - const auto &actions = m_menu->actions(); - for (QAction *action : actions) { - m_activeActions.append(action); - m_activeMenu.append(action->text()); - } +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; } @@ -144,7 +179,7 @@ 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, Q_NULLPTR)); + QScopedPointer atomReply(xcb_intern_atom_reply(c, atomCookie, nullptr)); if (atomReply.isNull()) { return value; } @@ -157,7 +192,7 @@ 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, NULL)); + QScopedPointer propertyReply(xcb_get_property_reply(c, propertyCookie, nullptr)); if (propertyReply.isNull()) { return value; } @@ -191,23 +226,27 @@ return; } + m_currentWindowId = id; + WId transientId = info.transientFor(); // lok at transient windows first while (transientId) { if (updateMenuFromWindowIfHasMenu(transientId)) { + setVisible(true); return; } - transientId = KWindowInfo(transientId, 0, NET::WM2TransientFor).transientFor(); + transientId = KWindowInfo(transientId, nullptr, NET::WM2TransientFor).transientFor(); } if (updateMenuFromWindowIfHasMenu(id)) { + setVisible(true); 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); @@ -217,27 +256,40 @@ } +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()); + + setVisible(contained && !info.isMinimized()); + } +} QHash AppMenuModel::roleNames() const { QHash roleNames; - roleNames[MenuRole] = "activeMenu"; - roleNames[ActionRole] = "activeActions"; + 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 ) { + 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) { - return m_activeMenu.at(row); - } else if(role == ActionRole) { - const QVariant data = qVariantFromValue((void *) m_activeActions.at(row)); - return data; + 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(); @@ -272,6 +324,19 @@ //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()); } @@ -283,9 +348,14 @@ 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()); + 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()); } }); } @@ -303,7 +373,7 @@ 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); Index: plugin/appmenuplugin.cpp =================================================================== --- plugin/appmenuplugin.cpp +++ plugin/appmenuplugin.cpp @@ -21,10 +21,9 @@ #include "appmenuplugin.h" #include "appmenumodel.h" - -#include #include + void AppmenuPlugin::registerTypes(const char *uri) { Q_ASSERT(uri == QLatin1String("org.kde.private.activeWindowControl")); Index: plugin/libdbusmenuqt/dbusmenuimporter.cpp =================================================================== --- plugin/libdbusmenuqt/dbusmenuimporter.cpp +++ plugin/libdbusmenuqt/dbusmenuimporter.cpp @@ -410,7 +410,9 @@ for (QAction *action: menu->actions()) { int id = action->property(DBUSMENU_PROPERTY_ID).toInt(); if (! newDBusMenuItemIds.contains(id)) { - menu->removeAction(action); + // Not calling removeAction() as QMenu will immediately close when it becomes empty, + // which can happen when an application completely reloads this menu. + // When the action is deleted deferred, it is removed from the menu. action->deleteLater(); d->m_actionForId.remove(id); }