diff --git a/components/containmentlayoutmanager/itemcontainer.cpp b/components/containmentlayoutmanager/itemcontainer.cpp index 58b33bcdf..bfdd632cf 100644 --- a/components/containmentlayoutmanager/itemcontainer.cpp +++ b/components/containmentlayoutmanager/itemcontainer.cpp @@ -1,797 +1,799 @@ /* * Copyright 2019 by Marco Martin * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Library General Public License as * published by the Free Software Foundation; either version 2, 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 Library General Public License for more details * * You should have received a copy of the GNU Library General Public * License along with this program; if not, write to the * Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * */ #include "itemcontainer.h" #include "configoverlay.h" #include #include #include #include #include #include #include #include ItemContainer::ItemContainer(QQuickItem *parent) : QQuickItem(parent) { setFiltersChildMouseEvents(true); setFlags(QQuickItem::ItemIsFocusScope); setActiveFocusOnTab(true); setAcceptedMouseButtons(Qt::LeftButton); setLayout(qobject_cast(parent)); m_editModeTimer = new QTimer(this); m_editModeTimer->setSingleShot(true); connect(this, &QQuickItem::parentChanged, this, [this]() { setLayout(qobject_cast(parentItem())); }); connect(m_editModeTimer, &QTimer::timeout, this, [this]() { setEditMode(true); }); setKeepMouseGrab(true); m_sizeHintAdjustTimer = new QTimer(this); m_sizeHintAdjustTimer->setSingleShot(true); m_sizeHintAdjustTimer->setInterval(0); connect(m_sizeHintAdjustTimer, &QTimer::timeout, this, &ItemContainer::sizeHintsChanged); } ItemContainer::~ItemContainer() { + disconnect(this, &QQuickItem::parentChanged, this, nullptr); + if (m_contentItem) { m_contentItem->setEnabled(true); } } QString ItemContainer::key() const { return m_key; } void ItemContainer::setKey(const QString &key) { if (m_key == key) { return; } m_key = key; emit keyChanged(); } bool ItemContainer::editMode() const { return m_editMode; } bool ItemContainer::dragActive() const { return m_dragActive; } void ItemContainer::cancelEdit() { m_editModeTimer->stop(); m_mouseDown = false; setEditMode(false); } void ItemContainer::setEditMode(bool editMode) { if (m_editMode == editMode) { return; } if (editMode && editModeCondition() == Locked) { return; } m_editMode = editMode; if (m_editModeCondition != AfterMouseOver || (m_layout && m_layout->editMode())) { m_contentItem->setEnabled(!editMode); } if (editMode) { setZ(1); } else { setZ(0); } if (m_mouseDown) { sendUngrabRecursive(m_contentItem); grabMouse(); } if (m_dragActive != editMode && m_mouseDown) { m_dragActive = editMode && m_mouseDown; emit dragActiveChanged(); } setConfigOverlayVisible(editMode); emit editModeChanged(editMode); } ItemContainer::EditModeCondition ItemContainer::editModeCondition() const { if (m_layout && m_layout->editModeCondition() == AppletsLayout::Locked) { return Locked; } return m_editModeCondition; } void ItemContainer::setEditModeCondition(EditModeCondition condition) { if (condition == m_editModeCondition) { return; } if (condition == Locked) { setEditMode(false); } m_editModeCondition = condition; setAcceptHoverEvents(condition == AfterMouseOver || (m_layout && m_layout->editMode())); emit editModeConditionChanged(); } AppletsLayout::PreferredLayoutDirection ItemContainer::preferredLayoutDirection() const { return m_preferredLayoutDirection; } void ItemContainer::setPreferredLayoutDirection(AppletsLayout::PreferredLayoutDirection direction) { if (direction == m_preferredLayoutDirection) { return; } m_preferredLayoutDirection = direction; emit preferredLayoutDirectionChanged(); } void ItemContainer::setLayout(AppletsLayout *layout) { if (m_layout == layout) { return; } if (m_layout) { disconnect(m_layout, &AppletsLayout::editModeConditionChanged, this, nullptr); disconnect(m_layout, &AppletsLayout::editModeChanged, this, nullptr); if (m_editMode) { m_layout->hidePlaceHolder(); } } m_layout = layout; if (!layout) { emit layoutChanged(); return; } if (parentItem() != layout) { setParentItem(layout); } connect(m_layout, &AppletsLayout::editModeConditionChanged, this, [this]() { if (m_layout->editModeCondition() == AppletsLayout::Locked) { setEditMode(false); } if ((m_layout->editModeCondition() == AppletsLayout::Locked) != (m_editModeCondition == ItemContainer::Locked)) { emit editModeConditionChanged(); } }); connect(m_layout, &AppletsLayout::editModeChanged, this, [this]() { setAcceptHoverEvents(m_editModeCondition == AfterMouseOver || m_layout->editMode()); }); emit layoutChanged(); } AppletsLayout *ItemContainer::layout() const { return m_layout; } void ItemContainer::syncChildItemsGeometry(const QSizeF &size) { if (m_contentItem) { m_contentItem->setPosition(QPointF(m_leftPadding, m_topPadding)); m_contentItem->setSize(QSizeF(size.width() - m_leftPadding - m_rightPadding, size.height() - m_topPadding - m_bottomPadding)); } if (m_backgroundItem) { m_backgroundItem->setPosition(QPointF(0, 0)); m_backgroundItem->setSize(size); } if (m_configOverlay) { m_configOverlay->setPosition(QPointF(0, 0)); m_configOverlay->setSize(size); } } QQmlComponent *ItemContainer::configOverlayComponent() const { return m_configOverlayComponent; } void ItemContainer::setConfigOverlayComponent(QQmlComponent *component) { if (component == m_configOverlayComponent) { return; } m_configOverlayComponent = component; if (m_configOverlay) { m_configOverlay->deleteLater(); m_configOverlay = nullptr; } emit configOverlayComponentChanged(); } ConfigOverlay *ItemContainer::configOverlayItem() const { return m_configOverlay; } QSizeF ItemContainer::initialSize() const { return m_initialSize; } void ItemContainer::setInitialSize(const QSizeF &size) { if (m_initialSize == size) { return; } m_initialSize = size; emit initialSizeChanged(); } bool ItemContainer::configOverlayVisible() const { return m_configOverlay && m_configOverlay->open(); } void ItemContainer::setConfigOverlayVisible(bool visible) { if (!m_configOverlayComponent) { return; } if (visible == configOverlayVisible()) { return; } if (visible && !m_configOverlay) { QQmlContext *context = QQmlEngine::contextForObject(this); Q_ASSERT(context); QObject *instance = m_configOverlayComponent->beginCreate(context); m_configOverlay = qobject_cast(instance); if (!m_configOverlay) { qWarning() << "Error: Applet configOverlay not of ConfigOverlay type"; if (instance) { instance->deleteLater(); } return; } m_configOverlay->setVisible(false); m_configOverlay->setItemContainer(this); m_configOverlay->setParentItem(this); m_configOverlay->setTouchInteraction(m_mouseSynthetizedFromTouch); m_configOverlay->setZ(999); m_configOverlay->setPosition(QPointF(0, 0)); m_configOverlay->setSize(size()); m_configOverlayComponent->completeCreate(); connect(m_configOverlay, &ConfigOverlay::openChanged, this, [this]() { emit configOverlayVisibleChanged(m_configOverlay->open()); }); emit configOverlayItemChanged(); } if (m_configOverlay) { m_configOverlay->setOpen(visible); } } void ItemContainer::contentData_append(QQmlListProperty *prop, QObject *object) { ItemContainer *container = static_cast(prop->object); if (!container) { return; } // QQuickItem *item = qobject_cast(object); container->m_contentData.append(object); } int ItemContainer::contentData_count(QQmlListProperty *prop) { ItemContainer *container = static_cast(prop->object); if (!container) { return 0; } return container->m_contentData.count(); } QObject *ItemContainer::contentData_at(QQmlListProperty *prop, int index) { ItemContainer *container = static_cast(prop->object); if (!container) { return nullptr; } if (index < 0 || index >= container->m_contentData.count()) { return nullptr; } return container->m_contentData.value(index); } void ItemContainer::contentData_clear(QQmlListProperty *prop) { ItemContainer *container = static_cast(prop->object); if (!container) { return; } return container->m_contentData.clear(); } QQmlListProperty ItemContainer::contentData() { return QQmlListProperty(this, nullptr, contentData_append, contentData_count, contentData_at, contentData_clear); } void ItemContainer::geometryChanged(const QRectF &newGeometry, const QRectF &oldGeometry) { syncChildItemsGeometry(newGeometry.size()); QQuickItem::geometryChanged(newGeometry, oldGeometry); emit contentWidthChanged(); emit contentHeightChanged(); } void ItemContainer::componentComplete() { if (!m_contentItem) { //qWarning()<<"Creating default contentItem"; m_contentItem = new QQuickItem(this); syncChildItemsGeometry(size()); } for (auto *o : m_contentData) { QQuickItem *item = qobject_cast(o); if (item) { item->setParentItem(m_contentItem); } } // Search for the Layout attached property // Qt6: this should become public api // https://bugreports.qt.io/browse/QTBUG-77103 for (auto *o : children()) { if (o->inherits("QQuickLayoutAttached")) { m_layoutAttached = o; } } if (m_layoutAttached) { //NOTE: new syntax cannot be used because we don't have access to the QQuickLayoutAttached class connect(m_layoutAttached, SIGNAL(minimumHeightChanged()), m_sizeHintAdjustTimer, SLOT(start())); connect(m_layoutAttached, SIGNAL(minimumWidthChanged()), m_sizeHintAdjustTimer, SLOT(start())); connect(m_layoutAttached, SIGNAL(preferredHeightChanged()), m_sizeHintAdjustTimer, SLOT(start())); connect(m_layoutAttached, SIGNAL(preferredWidthChanged()), m_sizeHintAdjustTimer, SLOT(start())); connect(m_layoutAttached, SIGNAL(maximumHeightChanged()), m_sizeHintAdjustTimer, SLOT(start())); connect(m_layoutAttached, SIGNAL(maximumWidthChanged()), m_sizeHintAdjustTimer, SLOT(start())); } QQuickItem::componentComplete(); } void ItemContainer::sendUngrabRecursive(QQuickItem *item) { if (!item || !item->window()) { return; } for (auto *child : item->childItems()) { sendUngrabRecursive(child); } QEvent ev(QEvent::UngrabMouse); QCoreApplication::sendEvent(item, &ev); } bool ItemContainer::childMouseEventFilter(QQuickItem *item, QEvent *event) { // Don't filter the configoverlay if (item == m_configOverlay || (m_configOverlay && m_configOverlay->isAncestorOf(item)) || (!m_editMode && m_editModeCondition == Manual)) { return QQuickItem::childMouseEventFilter(item, event); } //give more time before closing if (m_closeEditModeTimer && m_closeEditModeTimer->isActive()) { m_closeEditModeTimer->start(); } if (event->type() == QEvent::MouseButtonPress) { QMouseEvent *me = static_cast(event); if (me->button() != Qt::LeftButton && !(me->buttons() & Qt::LeftButton)) { return QQuickItem::childMouseEventFilter(item, event); } forceActiveFocus(Qt::MouseFocusReason); m_mouseDown = true; m_mouseSynthetizedFromTouch = me->source() == Qt::MouseEventSynthesizedBySystem || me->source() == Qt::MouseEventSynthesizedByQt; if (m_configOverlay) { m_configOverlay->setTouchInteraction(m_mouseSynthetizedFromTouch); } const bool wasEditMode = m_editMode; if (m_layout && m_layout->editMode()) { setEditMode(true); } else if (m_editModeCondition == AfterPressAndHold) { m_editModeTimer->start(QGuiApplication::styleHints()->mousePressAndHoldInterval()); } m_lastMousePosition = me->windowPos(); m_mouseDownPosition = me->windowPos(); if (m_editMode && !wasEditMode) { event->accept(); return true; } } else if (event->type() == QEvent::MouseMove) { QMouseEvent *me = static_cast(event); if (!m_editMode && QPointF(me->windowPos() - m_mouseDownPosition).manhattanLength() >= QGuiApplication::styleHints()->startDragDistance()) { m_editModeTimer->stop(); } else if (m_editMode) { event->accept(); } } else if (event->type() == QEvent::MouseButtonRelease) { m_editModeTimer->stop(); m_mouseDown = false; m_mouseSynthetizedFromTouch = false; ungrabMouse(); event->accept(); m_dragActive = false; if (m_editMode) { emit dragActiveChanged(); } } return QQuickItem::childMouseEventFilter(item, event); } void ItemContainer::mousePressEvent(QMouseEvent *event) { forceActiveFocus(Qt::MouseFocusReason); if (!m_editMode && m_editModeCondition == Manual) { return; } m_mouseDown = true; m_mouseSynthetizedFromTouch = event->source() == Qt::MouseEventSynthesizedBySystem || event->source() == Qt::MouseEventSynthesizedByQt; if (m_configOverlay) { m_configOverlay->setTouchInteraction(m_mouseSynthetizedFromTouch); } if (m_layout && m_layout->editMode()) { setEditMode(true); } if (m_editMode) { grabMouse(); setCursor(Qt::ClosedHandCursor); m_dragActive = true; emit dragActiveChanged(); } else if (m_editModeCondition == AfterPressAndHold) { m_editModeTimer->start(QGuiApplication::styleHints()->mousePressAndHoldInterval()); } m_lastMousePosition = event->windowPos(); m_mouseDownPosition = event->windowPos(); event->accept(); } void ItemContainer::mouseReleaseEvent(QMouseEvent *event) { Q_UNUSED(event); if (!m_layout || (!m_editMode && m_editModeCondition == Manual)) { return; } m_mouseDown = false; m_mouseSynthetizedFromTouch = false; m_editModeTimer->stop(); ungrabMouse(); if (m_editMode && !m_layout->itemIsManaged(this)) { m_layout->hidePlaceHolder(); m_layout->positionItem(this); } m_dragActive = false; if (m_editMode) { emit dragActiveChanged(); setCursor(Qt::OpenHandCursor); } event->accept(); } void ItemContainer::mouseMoveEvent(QMouseEvent *event) { if ((event->button() == Qt::NoButton && event->buttons() == Qt::NoButton) || (!m_editMode && m_editModeCondition == Manual)) { return; } if (!m_editMode && QPointF(event->windowPos() - m_mouseDownPosition).manhattanLength() >= QGuiApplication::styleHints()->startDragDistance()) { if (m_editModeCondition == AfterPress) { setEditMode(true); } else { m_editModeTimer->stop(); } } if (!m_editMode) { return; } if (m_layout && m_layout->itemIsManaged(this)) { m_layout->releaseSpace(this); grabMouse(); m_dragActive = true; emit dragActiveChanged(); } else { setPosition(QPointF(x() + event->windowPos().x() - m_lastMousePosition.x(), y() + event->windowPos().y() - m_lastMousePosition.y())); if (m_layout) { m_layout->showPlaceHolderForItem(this); } emit userDrag(QPointF(x(), y()), event->pos()); } m_lastMousePosition = event->windowPos(); event->accept(); } void ItemContainer::mouseUngrabEvent() { m_mouseDown = false; m_mouseSynthetizedFromTouch = false; m_editModeTimer->stop(); ungrabMouse(); if (m_layout && m_editMode && !m_layout->itemIsManaged(this)) { m_layout->hidePlaceHolder(); m_layout->positionItem(this); } m_dragActive = false; if (m_editMode) { emit dragActiveChanged(); } } void ItemContainer::hoverEnterEvent(QHoverEvent *event) { Q_UNUSED(event); if (m_editModeCondition != AfterMouseOver && !m_layout->editMode()) { return; } if (m_closeEditModeTimer) { m_closeEditModeTimer->stop(); } if (m_layout->editMode()) { setCursor(Qt::OpenHandCursor); setEditMode(true); } else { m_editModeTimer->start(QGuiApplication::styleHints()->mousePressAndHoldInterval()); } } void ItemContainer::hoverLeaveEvent(QHoverEvent *event) { Q_UNUSED(event); if (m_editModeCondition != AfterMouseOver && !m_layout->editMode()) { return; } m_editModeTimer->stop(); if (!m_closeEditModeTimer) { m_closeEditModeTimer = new QTimer(this); m_closeEditModeTimer->setSingleShot(true); m_closeEditModeTimer->setInterval(500); connect(m_closeEditModeTimer, &QTimer::timeout, this, [this] () { setEditMode(false); }); } m_closeEditModeTimer->start(); } QQuickItem *ItemContainer::contentItem() const { return m_contentItem; } void ItemContainer::setContentItem(QQuickItem *item) { if (m_contentItem == item) { return; } m_contentItem = item; item->setParentItem(this); m_contentItem->setPosition(QPointF(m_leftPadding, m_topPadding)); m_contentItem->setSize(QSizeF(width() - m_leftPadding - m_rightPadding, height() - m_topPadding - m_bottomPadding)); emit contentItemChanged(); } QQuickItem *ItemContainer::background() const { return m_backgroundItem; } void ItemContainer::setBackground(QQuickItem *item) { if (m_backgroundItem == item) { return; } m_backgroundItem = item; m_backgroundItem->setParentItem(this); m_backgroundItem->setPosition(QPointF(0, 0)); m_backgroundItem->setSize(size()); emit backgroundChanged(); } int ItemContainer::leftPadding() const { return m_leftPadding; } void ItemContainer::setLeftPadding(int padding) { if (m_leftPadding == padding) { return; } m_leftPadding = padding; syncChildItemsGeometry(size()); emit leftPaddingChanged(); emit contentWidthChanged(); } int ItemContainer::topPadding() const { return m_topPadding; } void ItemContainer::setTopPadding(int padding) { if (m_topPadding == padding) { return; } m_topPadding = padding; syncChildItemsGeometry(size()); emit topPaddingChanged(); emit contentHeightChanged(); } int ItemContainer::rightPadding() const { return m_rightPadding; } void ItemContainer::setRightPadding(int padding) { if (m_rightPadding == padding) { return; } m_rightPadding = padding; syncChildItemsGeometry(size()); emit rightPaddingChanged(); emit contentWidthChanged(); } int ItemContainer::bottomPadding() const { return m_bottomPadding; } void ItemContainer::setBottomPadding(int padding) { if (m_bottomPadding == padding) { return; } m_bottomPadding = padding; syncChildItemsGeometry(size()); emit bottomPaddingChanged(); emit contentHeightChanged(); } int ItemContainer::contentWidth() const { return width() - m_leftPadding - m_rightPadding; } int ItemContainer::contentHeight() const { return height() - m_topPadding - m_bottomPadding; } #include "moc_itemcontainer.cpp" diff --git a/gmenu-dbusmenu-proxy/actions.cpp b/gmenu-dbusmenu-proxy/actions.cpp index 9709595a0..e2b6a0748 100644 --- a/gmenu-dbusmenu-proxy/actions.cpp +++ b/gmenu-dbusmenu-proxy/actions.cpp @@ -1,216 +1,220 @@ /* * Copyright (C) 2018 Kai Uwe Broulik * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA * */ #include "actions.h" #include "debug.h" #include #include #include #include #include #include #include static const QString s_orgGtkActions = QStringLiteral("org.gtk.Actions"); Actions::Actions(const QString &serviceName, const QString &objectPath, QObject *parent) : QObject(parent) , m_serviceName(serviceName) , m_objectPath(objectPath) { Q_ASSERT(!serviceName.isEmpty()); Q_ASSERT(!m_objectPath.isEmpty()); if (!QDBusConnection::sessionBus().connect(serviceName, objectPath, s_orgGtkActions, QStringLiteral("Changed"), this, SLOT(onActionsChanged(QStringList,StringBoolMap,QVariantMap,GMenuActionMap)))) { qCWarning(DBUSMENUPROXY) << "Failed to subscribe to action changes for" << parent << "on" << serviceName << "at" << objectPath; } } Actions::~Actions() = default; void Actions::load() { QDBusMessage msg = QDBusMessage::createMethodCall(m_serviceName, m_objectPath, s_orgGtkActions, QStringLiteral("DescribeAll")); QDBusPendingReply reply = QDBusConnection::sessionBus().asyncCall(msg); QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(reply, this); connect(watcher, &QDBusPendingCallWatcher::finished, this, [this](QDBusPendingCallWatcher *watcher) { QDBusPendingReply reply = *watcher; if (reply.isError()) { qCWarning(DBUSMENUPROXY) << "Failed to get actions from" << m_serviceName << "at" << m_objectPath << reply.error(); emit failedToLoad(); } else { m_actions = reply.value(); emit loaded(); } watcher->deleteLater(); }); } bool Actions::get(const QString &name, GMenuAction &action) const { auto it = m_actions.find(name); if (it == m_actions.constEnd()) { return false; } action = *it; return true; } GMenuActionMap Actions::getAll() const { return m_actions; } -void Actions::trigger(const QString &name, uint timestamp) +void Actions::trigger(const QString &name, const QVariant &target, uint timestamp) { if (!m_actions.contains(name)) { qCWarning(DBUSMENUPROXY) << "Cannot invoke action" << name << "which doesn't exist"; return; } QDBusMessage msg = QDBusMessage::createMethodCall(m_serviceName, m_objectPath, s_orgGtkActions, QStringLiteral("Activate")); msg << name; - // TODO use the arguments provided by "target" in the menu item - msg << QVariant::fromValue(QVariantList()); + + QVariantList args; + if (target.isValid()) { + args << target; + } + msg << QVariant::fromValue(args); QVariantMap platformData; if (timestamp) { // From documentation: // If the startup notification id is not available, this can be just "_TIMEtime", where // time is the time stamp from the event triggering the call. // see also gtkwindow.c extract_time_from_startup_id and startup_id_is_fake platformData.insert(QStringLiteral("desktop-startup-id"), QStringLiteral("_TIME") + QString::number(timestamp)); } msg << platformData; QDBusPendingReply reply = QDBusConnection::sessionBus().asyncCall(msg); QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(reply, this); connect(watcher, &QDBusPendingCallWatcher::finished, this, [this, name](QDBusPendingCallWatcher *watcher) { QDBusPendingReply reply = *watcher; if (reply.isError()) { qCWarning(DBUSMENUPROXY) << "Failed to invoke action" << name << "on" << m_serviceName << "at" << m_objectPath << reply.error(); } watcher->deleteLater(); }); } bool Actions::isValid() const { return !m_actions.isEmpty(); } void Actions::onActionsChanged(const QStringList &removed, const StringBoolMap &enabledChanges, const QVariantMap &stateChanges, const GMenuActionMap &added) { // Collect the actions that we removed, altered, or added, so we can eventually signal changes for all menus that contain one of those actions QStringList dirtyActions; // TODO I bet for most of the loops below we could use a nice short std algorithm for (const QString &removedAction : removed) { if (m_actions.remove(removedAction)) { dirtyActions.append(removedAction); } } for (auto it = enabledChanges.constBegin(), end = enabledChanges.constEnd(); it != end; ++it) { const QString &actionName = it.key(); const bool enabled = it.value(); auto actionIt = m_actions.find(actionName); if (actionIt == m_actions.end()) { qCInfo(DBUSMENUPROXY) << "Got enabled changed for action" << actionName << "which we don't know"; continue; } GMenuAction &action = *actionIt; if (action.enabled != enabled) { action.enabled = enabled; dirtyActions.append(actionName); } else { qCInfo(DBUSMENUPROXY) << "Got enabled change for action" << actionName << "which didn't change it"; } } for (auto it = stateChanges.constBegin(), end = stateChanges.constEnd(); it != end; ++it) { const QString &actionName = it.key(); const QVariant &state = it.value(); auto actionIt = m_actions.find(actionName); if (actionIt == m_actions.end()) { qCInfo(DBUSMENUPROXY) << "Got state changed for action" << actionName << "which we don't know"; continue; } GMenuAction &action = *actionIt; if (action.state.isEmpty()) { qCDebug(DBUSMENUPROXY) << "Got new state for action" << actionName << "that didn't have any state before"; action.state.append(state); dirtyActions.append(actionName); } else { // Action state is a list but the state change only sends us a single variant, so just overwrite the first one QVariant &firstState = action.state.first(); if (firstState != state) { firstState = state; dirtyActions.append(actionName); } else { qCInfo(DBUSMENUPROXY) << "Got state change for action" << actionName << "which didn't change it"; } } } // unite() will result in keys being present multiple times, do it manually and overwrite existing ones for (auto it = added.constBegin(), end = added.constEnd(); it != end; ++it) { const QString &actionName = it.key(); if (DBUSMENUPROXY().isInfoEnabled()) { if (m_actions.contains(actionName)) { qCInfo(DBUSMENUPROXY) << "Got new action" << actionName << "that we already have, overwriting existing one"; } } m_actions.insert(actionName, it.value()); dirtyActions.append(actionName); } if (!dirtyActions.isEmpty()) { emit actionsChanged(dirtyActions); } } diff --git a/gmenu-dbusmenu-proxy/actions.h b/gmenu-dbusmenu-proxy/actions.h index 78ca17a92..565bccbe7 100644 --- a/gmenu-dbusmenu-proxy/actions.h +++ b/gmenu-dbusmenu-proxy/actions.h @@ -1,62 +1,62 @@ /* * Copyright (C) 2018 Kai Uwe Broulik * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA * */ #pragma once #include #include #include "gdbusmenutypes_p.h" class QStringList; class Actions : public QObject { Q_OBJECT public: Actions(const QString &serviceName, const QString &objectPath, QObject *parent = nullptr); ~Actions() override; void load(); bool get(const QString &name, GMenuAction &action) const; GMenuActionMap getAll() const; - void trigger(const QString &name, uint timestamp = 0); + void trigger(const QString &name, const QVariant &target, uint timestamp = 0); bool isValid() const; // basically "has actions" signals: void loaded(); void failedToLoad(); // expose error? void actionsChanged(const QStringList &dirtyActions); private slots: void onActionsChanged(const QStringList &removed, const StringBoolMap &enabledChanges, const QVariantMap &stateChanges, const GMenuActionMap &added); private: GMenuActionMap m_actions; QString m_serviceName; QString m_objectPath; }; diff --git a/gmenu-dbusmenu-proxy/menuproxy.cpp b/gmenu-dbusmenu-proxy/menuproxy.cpp index 8c2d2bd91..84d39ef84 100644 --- a/gmenu-dbusmenu-proxy/menuproxy.cpp +++ b/gmenu-dbusmenu-proxy/menuproxy.cpp @@ -1,404 +1,404 @@ /* * Copyright (C) 2018 Kai Uwe Broulik * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA * */ #include "menuproxy.h" #include #include "debug.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "window.h" static const QString s_ourServiceName = QStringLiteral("org.kde.plasma.gmenu_dbusmenu_proxy"); static const QString s_dbusMenuRegistrar = QStringLiteral("com.canonical.AppMenu.Registrar"); static const QByteArray s_gtkUniqueBusName = QByteArrayLiteral("_GTK_UNIQUE_BUS_NAME"); static const QByteArray s_gtkApplicationObjectPath = QByteArrayLiteral("_GTK_APPLICATION_OBJECT_PATH"); static const QByteArray s_unityObjectPath = QByteArrayLiteral("_UNITY_OBJECT_PATH"); static const QByteArray s_gtkWindowObjectPath = QByteArrayLiteral("_GTK_WINDOW_OBJECT_PATH"); static const QByteArray s_gtkMenuBarObjectPath = QByteArrayLiteral("_GTK_MENUBAR_OBJECT_PATH"); // that's the generic app menu with Help and Options and will be used if window doesn't have a fully-blown menu bar static const QByteArray s_gtkAppMenuObjectPath = QByteArrayLiteral("_GTK_APP_MENU_OBJECT_PATH"); static const QByteArray s_kdeNetWmAppMenuServiceName = QByteArrayLiteral("_KDE_NET_WM_APPMENU_SERVICE_NAME"); static const QByteArray s_kdeNetWmAppMenuObjectPath = QByteArrayLiteral("_KDE_NET_WM_APPMENU_OBJECT_PATH"); static const QString s_gtkModules = QStringLiteral("gtk-modules"); static const QString s_appMenuGtkModule = QStringLiteral("appmenu-gtk-module"); MenuProxy::MenuProxy() : QObject() , m_xConnection(QX11Info::connection()) , m_serviceWatcher(new QDBusServiceWatcher(this)) , m_gtk2RcWatch(new KDirWatch(this)) , m_writeGtk2SettingsTimer(new QTimer(this)) { m_serviceWatcher->setConnection(QDBusConnection::sessionBus()); m_serviceWatcher->setWatchMode(QDBusServiceWatcher::WatchForUnregistration | QDBusServiceWatcher::WatchForRegistration); m_serviceWatcher->addWatchedService(s_dbusMenuRegistrar); connect(m_serviceWatcher, &QDBusServiceWatcher::serviceRegistered, this, [this](const QString &service) { Q_UNUSED(service); qCDebug(DBUSMENUPROXY) << "Global menu service became available, starting"; init(); }); connect(m_serviceWatcher, &QDBusServiceWatcher::serviceUnregistered, this, [this](const QString &service) { Q_UNUSED(service); qCDebug(DBUSMENUPROXY) << "Global menu service disappeared, cleaning up"; teardown(); }); // It's fine to do a blocking call here as we're a separate binary with no UI if (QDBusConnection::sessionBus().interface()->isServiceRegistered(s_dbusMenuRegistrar)) { qCDebug(DBUSMENUPROXY) << "Global menu service is running, starting right away"; init(); } else { qCDebug(DBUSMENUPROXY) << "No global menu service available, waiting for it to start before doing anything"; // be sure when started to restore gtk menus when there's no dbus menu around in case we crashed enableGtkSettings(false); } // kde-gtk-config just deletes and re-creates the gtkrc-2.0, watch this and add our config to it again m_writeGtk2SettingsTimer->setSingleShot(true); m_writeGtk2SettingsTimer->setInterval(1000); connect(m_writeGtk2SettingsTimer, &QTimer::timeout, this, &MenuProxy::writeGtk2Settings); auto startGtk2SettingsTimer = [this] { if (!m_writeGtk2SettingsTimer->isActive()) { m_writeGtk2SettingsTimer->start(); } }; connect(m_gtk2RcWatch, &KDirWatch::created, this, startGtk2SettingsTimer); connect(m_gtk2RcWatch, &KDirWatch::dirty, this, startGtk2SettingsTimer); m_gtk2RcWatch->addFile(gtkRc2Path()); } MenuProxy::~MenuProxy() { teardown(); } bool MenuProxy::init() { if (!QDBusConnection::sessionBus().registerService(s_ourServiceName)) { qCWarning(DBUSMENUPROXY) << "Failed to register DBus service" << s_ourServiceName; return false; } enableGtkSettings(true); connect(KWindowSystem::self(), &KWindowSystem::windowAdded, this, &MenuProxy::onWindowAdded); connect(KWindowSystem::self(), &KWindowSystem::windowRemoved, this, &MenuProxy::onWindowRemoved); const auto windows = KWindowSystem::windows(); for (WId id : windows) { onWindowAdded(id); } if (m_windows.isEmpty()) { qCDebug(DBUSMENUPROXY) << "Up and running but no windows with menus in sight"; } return true; } void MenuProxy::teardown() { enableGtkSettings(false); QDBusConnection::sessionBus().unregisterService(s_ourServiceName); disconnect(KWindowSystem::self(), &KWindowSystem::windowAdded, this, &MenuProxy::onWindowAdded); disconnect(KWindowSystem::self(), &KWindowSystem::windowRemoved, this, &MenuProxy::onWindowRemoved); qDeleteAll(m_windows); m_windows.clear(); } void MenuProxy::enableGtkSettings(bool enable) { m_enabled = enable; writeGtk2Settings(); writeGtk3Settings(); // TODO use gconf/dconf directly or at least signal a change somehow? } QString MenuProxy::gtkRc2Path() { return QDir::homePath() + QLatin1String("/.gtkrc-2.0"); } QString MenuProxy::gtk3SettingsIniPath() { return QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation) + QLatin1String("/gtk-3.0/settings.ini"); } void MenuProxy::writeGtk2Settings() { QFile rcFile(gtkRc2Path()); if (!rcFile.exists()) { // Don't create it here, that would break writing default GTK-2.0 settings on first login, // as the gtkbreeze kconf_update script only does so if it does not exist return; } qCDebug(DBUSMENUPROXY) << "Writing gtkrc-2.0 to" << (m_enabled ? "enable" : "disable") << "global menu support"; if (!rcFile.open(QIODevice::ReadWrite | QIODevice::Text)) { return; } QByteArray content; QStringList gtkModules; while (!rcFile.atEnd()) { const QByteArray rawLine = rcFile.readLine(); const QString line = QString::fromUtf8(rawLine.trimmed()); if (!line.startsWith(s_gtkModules)) { // keep line as-is content += rawLine; continue; } const int equalSignIdx = line.indexOf(QLatin1Char('=')); if (equalSignIdx < 1) { continue; } gtkModules = line.mid(equalSignIdx + 1).split(QLatin1Char(':'), QString::SkipEmptyParts); break; } addOrRemoveAppMenuGtkModule(gtkModules); if (!gtkModules.isEmpty()) { content += QStringLiteral("%1=%2").arg(s_gtkModules, gtkModules.join(QLatin1Char(':'))).toUtf8(); } qCDebug(DBUSMENUPROXY) << " gtk-modules:" << gtkModules; m_gtk2RcWatch->stopScan(); // now write the new contents of the file rcFile.resize(0); rcFile.write(content); rcFile.close(); m_gtk2RcWatch->startScan(); } void MenuProxy::writeGtk3Settings() { qCDebug(DBUSMENUPROXY) << "Writing gtk-3.0/settings.ini" << (m_enabled ? "enable" : "disable") << "global menu support"; // mostly taken from kde-gtk-config auto cfg = KSharedConfig::openConfig(gtk3SettingsIniPath(), KConfig::NoGlobals); KConfigGroup group(cfg, "Settings"); QStringList gtkModules = group.readEntry("gtk-modules", QString()).split(QLatin1Char(':'), QString::SkipEmptyParts); addOrRemoveAppMenuGtkModule(gtkModules); if (!gtkModules.isEmpty()) { group.writeEntry("gtk-modules", gtkModules.join(QLatin1Char(':'))); } else { group.deleteEntry("gtk-modules"); } qCDebug(DBUSMENUPROXY) << " gtk-modules:" << gtkModules; if (m_enabled) { group.writeEntry("gtk-shell-shows-menubar", 1); } else { group.deleteEntry("gtk-shell-shows-menubar"); } qCDebug(DBUSMENUPROXY) << " gtk-shell-shows-menubar:" << (m_enabled ? 1 : 0); group.sync(); } void MenuProxy::addOrRemoveAppMenuGtkModule(QStringList &list) { if (m_enabled && !list.contains(s_appMenuGtkModule)) { list.append(s_appMenuGtkModule); } else if (!m_enabled) { list.removeAll(s_appMenuGtkModule); } } void MenuProxy::onWindowAdded(WId id) { if (m_windows.contains(id)) { return; } KWindowInfo info(id, NET::WMWindowType); NET::WindowType wType = info.windowType(NET::NormalMask | NET::DesktopMask | NET::DockMask | NET::ToolbarMask | NET::MenuMask | NET::DialogMask | NET::OverrideMask | NET::TopMenuMask | NET::UtilityMask | NET::SplashMask); // Only top level windows typically have a menu bar, dialogs, such as settings don't if (wType != NET::Normal) { - qCInfo(DBUSMENUPROXY) << "Ignoring window" << id << "of type" << wType; + qCDebug(DBUSMENUPROXY) << "Ignoring window" << id << "of type" << wType; return; } const QString serviceName = QString::fromUtf8(getWindowPropertyString(id, s_gtkUniqueBusName)); if (serviceName.isEmpty()) { return; } const QString applicationObjectPath = QString::fromUtf8(getWindowPropertyString(id, s_gtkApplicationObjectPath)); const QString unityObjectPath = QString::fromUtf8(getWindowPropertyString(id, s_unityObjectPath)); const QString windowObjectPath = QString::fromUtf8(getWindowPropertyString(id, s_gtkWindowObjectPath)); const QString applicationMenuObjectPath = QString::fromUtf8(getWindowPropertyString(id, s_gtkAppMenuObjectPath)); const QString menuBarObjectPath = QString::fromUtf8(getWindowPropertyString(id, s_gtkMenuBarObjectPath)); if (applicationMenuObjectPath.isEmpty() && menuBarObjectPath.isEmpty()) { return; } Window *window = new Window(serviceName); window->setWinId(id); window->setApplicationObjectPath(applicationObjectPath); window->setUnityObjectPath(unityObjectPath); window->setWindowObjectPath(windowObjectPath); window->setApplicationMenuObjectPath(applicationMenuObjectPath); window->setMenuBarObjectPath(menuBarObjectPath); m_windows.insert(id, window); connect(window, &Window::requestWriteWindowProperties, this, [this, window] { Q_ASSERT(!window->proxyObjectPath().isEmpty()); writeWindowProperty(window->winId(), s_kdeNetWmAppMenuServiceName, s_ourServiceName.toUtf8()); writeWindowProperty(window->winId(), s_kdeNetWmAppMenuObjectPath, window->proxyObjectPath().toUtf8()); }); connect(window, &Window::requestRemoveWindowProperties, this, [this, window] { writeWindowProperty(window->winId(), s_kdeNetWmAppMenuServiceName, QByteArray()); writeWindowProperty(window->winId(), s_kdeNetWmAppMenuObjectPath, QByteArray()); }); window->init(); } void MenuProxy::onWindowRemoved(WId id) { // no need to cleanup() (which removes window properties) when the window is gone, delete right away delete m_windows.take(id); } QByteArray MenuProxy::getWindowPropertyString(WId id, const QByteArray &name) { QByteArray value; auto atom = getAtom(name); if (atom == XCB_ATOM_NONE) { return value; } // GTK properties aren't XCB_ATOM_STRING but a custom one auto utf8StringAtom = getAtom(QByteArrayLiteral("UTF8_STRING")); static const long MAX_PROP_SIZE = 10000; auto propertyCookie = xcb_get_property(m_xConnection, false, id, atom, utf8StringAtom, 0, MAX_PROP_SIZE); QScopedPointer propertyReply(xcb_get_property_reply(m_xConnection, propertyCookie, nullptr)); if (propertyReply.isNull()) { qCWarning(DBUSMENUPROXY) << "XCB property reply for atom" << name << "on" << id << "was null"; return value; } if (propertyReply->type == utf8StringAtom && 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; } void MenuProxy::writeWindowProperty(WId id, const QByteArray &name, const QByteArray &value) { auto atom = getAtom(name); if (atom == XCB_ATOM_NONE) { return; } if (value.isEmpty()) { xcb_delete_property(m_xConnection, id, atom); } else { xcb_change_property(m_xConnection, XCB_PROP_MODE_REPLACE, id, atom, XCB_ATOM_STRING, 8, value.length(), value.constData()); } } xcb_atom_t MenuProxy::getAtom(const QByteArray &name) { static QHash s_atoms; auto atom = s_atoms.value(name, XCB_ATOM_NONE); if (atom == XCB_ATOM_NONE) { const xcb_intern_atom_cookie_t atomCookie = xcb_intern_atom(m_xConnection, false, name.length(), name.constData()); QScopedPointer atomReply(xcb_intern_atom_reply(m_xConnection, atomCookie, nullptr)); if (!atomReply.isNull()) { atom = atomReply->atom; if (atom != XCB_ATOM_NONE) { s_atoms.insert(name, atom); } } } return atom; } diff --git a/gmenu-dbusmenu-proxy/window.cpp b/gmenu-dbusmenu-proxy/window.cpp index 0aca30080..4ef77b641 100644 --- a/gmenu-dbusmenu-proxy/window.cpp +++ b/gmenu-dbusmenu-proxy/window.cpp @@ -1,677 +1,675 @@ /* * Copyright (C) 2018 Kai Uwe Broulik * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA * */ #include "window.h" #include "debug.h" #include #include #include #include #include #include #include #include #include #include "actions.h" #include "dbusmenuadaptor.h" #include "icons.h" #include "menu.h" #include "utils.h" #include "../libdbusmenuqt/dbusmenushortcut_p.h" static const QString s_orgGtkActions = QStringLiteral("org.gtk.Actions"); static const QString s_orgGtkMenus = QStringLiteral("org.gtk.Menus"); static const QString s_applicationActionsPrefix = QStringLiteral("app."); static const QString s_unityActionsPrefix = QStringLiteral("unity."); static const QString s_windowActionsPrefix = QStringLiteral("win."); Window::Window(const QString &serviceName) : QObject() , m_serviceName(serviceName) { qCDebug(DBUSMENUPROXY) << "Created menu on" << serviceName; Q_ASSERT(!serviceName.isEmpty()); GDBusMenuTypes_register(); DBusMenuTypes_register(); } Window::~Window() = default; void Window::init() { qCDebug(DBUSMENUPROXY) << "Inited window with menu for" << m_winId << "on" << m_serviceName << "at app" << m_applicationObjectPath << "win" << m_windowObjectPath << "unity" << m_unityObjectPath; if (!m_applicationMenuObjectPath.isEmpty()) { m_applicationMenu = new Menu(m_serviceName, m_applicationMenuObjectPath, this); connect(m_applicationMenu, &Menu::menuAppeared, this, &Window::updateWindowProperties); connect(m_applicationMenu, &Menu::menuDisappeared, this, &Window::updateWindowProperties); connect(m_applicationMenu, &Menu::subscribed, this, &Window::onMenuSubscribed); // basically so it replies on DBus no matter what connect(m_applicationMenu, &Menu::failedToSubscribe, this, &Window::onMenuSubscribed); connect(m_applicationMenu, &Menu::itemsChanged, this, &Window::menuItemsChanged); connect(m_applicationMenu, &Menu::menusChanged, this, &Window::menuChanged); } if (!m_menuBarObjectPath.isEmpty()) { m_menuBar = new Menu(m_serviceName, m_menuBarObjectPath, this); connect(m_menuBar, &Menu::menuAppeared, this, &Window::updateWindowProperties); connect(m_menuBar, &Menu::menuDisappeared, this, &Window::updateWindowProperties); connect(m_menuBar, &Menu::subscribed, this, &Window::onMenuSubscribed); connect(m_menuBar, &Menu::failedToSubscribe, this, &Window::onMenuSubscribed); connect(m_menuBar, &Menu::itemsChanged, this, &Window::menuItemsChanged); connect(m_menuBar, &Menu::menusChanged, this, &Window::menuChanged); } if (!m_applicationObjectPath.isEmpty()) { m_applicationActions = new Actions(m_serviceName, m_applicationObjectPath, this); connect(m_applicationActions, &Actions::actionsChanged, this, [this](const QStringList &dirtyActions) { onActionsChanged(dirtyActions, s_applicationActionsPrefix); }); connect(m_applicationActions, &Actions::loaded, this, [this] { if (m_menuInited) { onActionsChanged(m_applicationActions->getAll().keys(), s_applicationActionsPrefix); } else { initMenu(); } }); m_applicationActions->load(); } if (!m_unityObjectPath.isEmpty()) { m_unityActions = new Actions(m_serviceName, m_unityObjectPath, this); connect(m_unityActions, &Actions::actionsChanged, this, [this](const QStringList &dirtyActions) { onActionsChanged(dirtyActions, s_unityActionsPrefix); }); connect(m_unityActions, &Actions::loaded, this, [this] { if (m_menuInited) { onActionsChanged(m_unityActions->getAll().keys(), s_unityActionsPrefix); } else { initMenu(); } }); m_unityActions->load(); } if (!m_windowObjectPath.isEmpty()) { m_windowActions = new Actions(m_serviceName, m_windowObjectPath, this); connect(m_windowActions, &Actions::actionsChanged, this, [this](const QStringList &dirtyActions) { onActionsChanged(dirtyActions, s_windowActionsPrefix); }); connect(m_windowActions, &Actions::loaded, this, [this] { if (m_menuInited) { onActionsChanged(m_windowActions->getAll().keys(), s_windowActionsPrefix); } else { initMenu(); } }); m_windowActions->load(); } } WId Window::winId() const { return m_winId; } void Window::setWinId(WId winId) { m_winId = winId; } QString Window::serviceName() const { return m_serviceName; } QString Window::applicationObjectPath() const { return m_applicationObjectPath; } void Window::setApplicationObjectPath(const QString &applicationObjectPath) { m_applicationObjectPath = applicationObjectPath; } QString Window::unityObjectPath() const { return m_unityObjectPath; } void Window::setUnityObjectPath(const QString &unityObjectPath) { m_unityObjectPath = unityObjectPath; } QString Window::applicationMenuObjectPath() const { return m_applicationMenuObjectPath; } void Window::setApplicationMenuObjectPath(const QString &applicationMenuObjectPath) { m_applicationMenuObjectPath = applicationMenuObjectPath; } QString Window::menuBarObjectPath() const { return m_menuBarObjectPath; } void Window::setMenuBarObjectPath(const QString &menuBarObjectPath) { m_menuBarObjectPath = menuBarObjectPath; } QString Window::windowObjectPath() const { return m_windowObjectPath; } void Window::setWindowObjectPath(const QString &windowObjectPath) { m_windowObjectPath = windowObjectPath; } QString Window::currentMenuObjectPath() const { return m_currentMenuObjectPath; } QString Window::proxyObjectPath() const { return m_proxyObjectPath; } void Window::initMenu() { if (m_menuInited) { return; } if (!registerDBusObject()) { return; } // appmenu-gtk-module always announces a menu bar on every GTK window even if there is none // so we subscribe to the menu bar as soon as it shows up so we can figure out // if we have a menu bar, an app menu, or just nothing if (m_applicationMenu) { m_applicationMenu->start(0); } if (m_menuBar) { m_menuBar->start(0); } m_menuInited = true; } void Window::menuItemsChanged(const QVector &itemIds) { if (qobject_cast(sender()) != m_currentMenu) { return; } DBusMenuItemList items; for (uint id : itemIds) { const auto newItem = m_currentMenu->getItem(id); DBusMenuItem dBusItem{ // 0 is menu, items start at 1 static_cast(id), gMenuToDBusMenuProperties(newItem) }; items.append(dBusItem); } emit ItemsPropertiesUpdated(items, {}); } void Window::menuChanged(const QVector &menuIds) { if (qobject_cast(sender()) != m_currentMenu) { return; } for (uint menu : menuIds) { emit LayoutUpdated(3 /*revision*/, menu); } } void Window::onMenuSubscribed(uint id) { // When it was a delayed GetLayout request, send the reply now const auto pendingReplies = m_pendingGetLayouts.values(id); if (!pendingReplies.isEmpty()) { for (const auto &pendingReply : pendingReplies) { if (pendingReply.type() != QDBusMessage::InvalidMessage) { auto reply = pendingReply.createReply(); DBusMenuLayoutItem item; uint revision = GetLayout(Utils::treeStructureToInt(id, 0, 0), 0, {}, item); reply << revision << QVariant::fromValue(item); QDBusConnection::sessionBus().send(reply); } } m_pendingGetLayouts.remove(id); } else { emit LayoutUpdated(2 /*revision*/, id); } } bool Window::getAction(const QString &name, GMenuAction &action) const { QString lookupName; Actions *actions = getActionsForAction(name, lookupName); if (!actions) { return false; } return actions->get(lookupName, action); } -void Window::triggerAction(const QString &name, uint timestamp) +void Window::triggerAction(const QString &name, const QVariant &target, uint timestamp) { QString lookupName; Actions *actions = getActionsForAction(name, lookupName); - if (!actions) { return; } - actions->trigger(lookupName, timestamp); + actions->trigger(lookupName, target, timestamp); } Actions *Window::getActionsForAction(const QString &name, QString &lookupName) const { if (name.startsWith(QLatin1String("app."))) { lookupName = name.mid(4); return m_applicationActions; } else if (name.startsWith(QLatin1String("unity."))) { lookupName = name.mid(6); return m_unityActions; } else if (name.startsWith(QLatin1String("win."))) { lookupName = name.mid(4); return m_windowActions; } return nullptr; } void Window::onActionsChanged(const QStringList &dirty, const QString &prefix) { if (m_applicationMenu) { m_applicationMenu->actionsChanged(dirty, prefix); } if (m_menuBar) { m_menuBar->actionsChanged(dirty, prefix); } } bool Window::registerDBusObject() { Q_ASSERT(m_proxyObjectPath.isEmpty()); static int menus = 0; ++menus; new DbusmenuAdaptor(this); const QString objectPath = QStringLiteral("/MenuBar/%1").arg(QString::number(menus)); qCDebug(DBUSMENUPROXY) << "Registering DBus object path" << objectPath; if (!QDBusConnection::sessionBus().registerObject(objectPath, this)) { qCWarning(DBUSMENUPROXY) << "Failed to register object"; return false; } m_proxyObjectPath = objectPath; return true; } void Window::updateWindowProperties() { const bool hasMenu = ((m_applicationMenu && m_applicationMenu->hasMenu()) || (m_menuBar && m_menuBar->hasMenu())); if (!hasMenu) { emit requestRemoveWindowProperties(); return; } Menu *oldMenu = m_currentMenu; Menu *newMenu = qobject_cast(sender()); // set current menu as needed if (!m_currentMenu) { m_currentMenu = newMenu; // Menu Bar takes precedence over application menu } else if (m_currentMenu == m_applicationMenu && newMenu == m_menuBar) { qCDebug(DBUSMENUPROXY) << "Switching from application menu to menu bar"; m_currentMenu = newMenu; // TODO update layout } if (m_currentMenu != oldMenu) { // update entire menu now emit LayoutUpdated(4 /*revision*/, 0); } emit requestWriteWindowProperties(); } // DBus bool Window::AboutToShow(int id) { // We always request the first time GetLayout is called and keep up-to-date internally // No need to have us prepare anything here Q_UNUSED(id); return false; } void Window::Event(int id, const QString &eventId, const QDBusVariant &data, uint timestamp) { Q_UNUSED(data); if (!m_currentMenu) { return; } // GMenu dbus doesn't have any "opened" or "closed" signals, we'll only handle "clicked" if (eventId == QLatin1String("clicked")) { - const QString action = m_currentMenu->getItem(id).value(QStringLiteral("action")).toString(); + const QVariantMap item = m_currentMenu->getItem(id); + const QString action = item.value(QStringLiteral("action")).toString(); + const QVariant target = item.value(QStringLiteral("target")); if (!action.isEmpty()) { - triggerAction(action, timestamp); + triggerAction(action, target, timestamp); } } } DBusMenuItemList Window::GetGroupProperties(const QList &ids, const QStringList &propertyNames) { Q_UNUSED(ids); Q_UNUSED(propertyNames); return DBusMenuItemList(); } uint Window::GetLayout(int parentId, int recursionDepth, const QStringList &propertyNames, DBusMenuLayoutItem &dbusItem) { Q_UNUSED(recursionDepth); // TODO Q_UNUSED(propertyNames); int subscription; int sectionId; int index; Utils::intToTreeStructure(parentId, subscription, sectionId, index); if (!m_currentMenu) { return 1; } if (!m_currentMenu->hasSubscription(subscription)) { // let's serve multiple similar requests in one go once we've processed them m_pendingGetLayouts.insertMulti(subscription, message()); setDelayedReply(true); m_currentMenu->start(subscription); return 1; } bool ok; const GMenuItem section = m_currentMenu->getSection(subscription, sectionId, &ok); if (!ok) { qCDebug(DBUSMENUPROXY) << "There is no section on" << subscription << "at" << sectionId << "with" << parentId; return 1; } // If a particular entry is requested, see what it is and resolve as necessary // for example the "File" entry on root is 0,0,1 but is a menu reference to e.g. 1,0,0 // so resolve that and return the correct menu if (index > 0) { // non-zero index indicates item within a menu but the index in the list still starts at zero if (section.items.count() < index) { qCDebug(DBUSMENUPROXY) << "Requested index" << index << "on" << subscription << "at" << sectionId << "with" << parentId << "is out of bounds"; return 0; } const auto &requestedItem = section.items.at(index - 1); auto it = requestedItem.constFind(QStringLiteral(":submenu")); if (it != requestedItem.constEnd()) { const GMenuSection gmenuSection = qdbus_cast(it->value()); return GetLayout(Utils::treeStructureToInt(gmenuSection.subscription, gmenuSection.menu, 0), recursionDepth, propertyNames, dbusItem); } else { // TODO return 0; } } dbusItem.id = parentId; // TODO dbusItem.properties = { {QStringLiteral("children-display"), QStringLiteral("submenu")} }; int count = 0; const auto itemsToBeAdded = section.items; for (const auto &item : itemsToBeAdded) { DBusMenuLayoutItem child{ Utils::treeStructureToInt(section.id, sectionId, ++count), gMenuToDBusMenuProperties(item), {} // children }; dbusItem.children.append(child); // Now resolve section aliases auto it = item.constFind(QStringLiteral(":section")); if (it != item.constEnd()) { // references another place, add it instead GMenuSection gmenuSection = qdbus_cast(it->value()); // remember where the item came from and give it an appropriate ID // so updates signalled by the app will map to the right place int originalSubscription = gmenuSection.subscription; int originalMenu = gmenuSection.menu; // TODO start subscription if we don't have it auto items = m_currentMenu->getSection(gmenuSection.subscription, gmenuSection.menu).items; // Check whether it's an alias to an alias // FIXME make generic/recursive if (items.count() == 1) { const auto &aliasedItem = items.constFirst(); auto findIt = aliasedItem.constFind(QStringLiteral(":section")); if (findIt != aliasedItem.constEnd()) { GMenuSection gmenuSection2 = qdbus_cast(findIt->value()); items = m_currentMenu->getSection(gmenuSection2.subscription, gmenuSection2.menu).items; originalSubscription = gmenuSection2.subscription; originalMenu = gmenuSection2.menu; } } int aliasedCount = 0; for (const auto &aliasedItem : qAsConst(items)) { DBusMenuLayoutItem aliasedChild{ Utils::treeStructureToInt(originalSubscription, originalMenu, ++aliasedCount), gMenuToDBusMenuProperties(aliasedItem), {} // children }; dbusItem.children.append(aliasedChild); } } } // revision, unused in libdbusmenuqt return 1; } QDBusVariant Window::GetProperty(int id, const QString &property) { Q_UNUSED(id); Q_UNUSED(property); QDBusVariant value; return value; } QString Window::status() const { return QStringLiteral("normal"); } uint Window::version() const { return 4; } QVariantMap Window::gMenuToDBusMenuProperties(const QVariantMap &source) const { QVariantMap result; result.insert(QStringLiteral("label"), source.value(QStringLiteral("label")).toString()); if (source.contains(QLatin1String(":section"))) { result.insert(QStringLiteral("type"), QStringLiteral("separator")); } const bool isMenu = source.contains(QLatin1String(":submenu")); if (isMenu) { result.insert(QStringLiteral("children-display"), QStringLiteral("submenu")); } QString accel = source.value(QStringLiteral("accel")).toString(); if (!accel.isEmpty()) { QStringList shortcut; // TODO use regexp or something if (accel.contains(QLatin1String("")) || accel.contains(QLatin1String(""))) { shortcut.append(QStringLiteral("Control")); accel.remove(QLatin1String("")); accel.remove(QLatin1String("")); } if (accel.contains(QLatin1String(""))) { shortcut.append(QStringLiteral("Shift")); accel.remove(QLatin1String("")); } if (accel.contains(QLatin1String(""))) { shortcut.append(QStringLiteral("Alt")); accel.remove(QLatin1String("")); } if (accel.contains(QLatin1String(""))) { shortcut.append(QStringLiteral("Super")); accel.remove(QLatin1String("")); } if (!accel.isEmpty()) { // TODO replace "+" by "plus" and "-" by "minus" shortcut.append(accel); // TODO does gmenu support multiple? DBusMenuShortcut dbusShortcut; dbusShortcut.append(shortcut); // don't let it unwrap the list we append result.insert(QStringLiteral("shortcut"), QVariant::fromValue(dbusShortcut)); } } bool enabled = true; const QString actionName = Utils::itemActionName(source); GMenuAction action; // if no action is specified this is fine but if there is an action we don't have // disable the menu entry bool actionOk = true; if (!actionName.isEmpty()) { actionOk = getAction(actionName, action); enabled = actionOk && action.enabled; } // we used to only send this if not enabled but then dbusmenuimporter does not // update the enabled state when it changes from disabled to enabled result.insert(QStringLiteral("enabled"), enabled); bool visible = true; const QString hiddenWhen = source.value(QStringLiteral("hidden-when")).toString(); if (hiddenWhen == QLatin1String("action-disabled") && (!actionOk || !enabled)) { visible = false; } else if (hiddenWhen == QLatin1String("action-missing") && !actionOk) { visible = false; // While we have Global Menu we don't have macOS menu (where Quit, Help, etc is separate) } else if (hiddenWhen == QLatin1String("macos-menubar")) { visible = true; } result.insert(QStringLiteral("visible"), visible); QString icon = source.value(QStringLiteral("icon")).toString(); if (icon.isEmpty()) { icon = source.value(QStringLiteral("verb-icon")).toString(); } icon = Icons::actionIcon(actionName); if (!icon.isEmpty()) { result.insert(QStringLiteral("icon-name"), icon); } + const QVariant target = source.value(QStringLiteral("target")); + if (actionOk) { - const auto args = action.state; - if (args.count() == 1) { - const auto &firstArg = args.first(); + const auto actionStates = action.state; + if (actionStates.count() == 1) { + const auto &actionState = actionStates.first(); // assume this is a checkbox if (!isMenu) { - if (firstArg.type() == QVariant::Bool) { + if (actionState.type() == QVariant::Bool) { result.insert(QStringLiteral("toggle-type"), QStringLiteral("checkbox")); - result.insert(QStringLiteral("toggle-state"), firstArg.toBool() ? 1 : 0); - } else if (firstArg.type() == QVariant::String) { + result.insert(QStringLiteral("toggle-state"), actionState.toBool() ? 1 : 0); + } else if (actionState.type() == QVariant::String) { result.insert(QStringLiteral("toggle-type"), QStringLiteral("radio")); - const QString checkedAction = firstArg.toString(); - if (!checkedAction.isEmpty() && actionName.endsWith(checkedAction)) { - result.insert(QStringLiteral("toggle-state"), 1); - } else { - result.insert(QStringLiteral("toggle-state"), 0); - } + result.insert(QStringLiteral("toggle-state"), actionState == target ? 1 : 0); } } } } return result; } diff --git a/gmenu-dbusmenu-proxy/window.h b/gmenu-dbusmenu-proxy/window.h index 035ea6a00..a3025c955 100644 --- a/gmenu-dbusmenu-proxy/window.h +++ b/gmenu-dbusmenu-proxy/window.h @@ -1,140 +1,140 @@ /* * Copyright (C) 2018 Kai Uwe Broulik * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA * */ #pragma once #include #include #include #include #include // for WId #include #include "gdbusmenutypes_p.h" #include "../libdbusmenuqt/dbusmenutypes_p.h" class QDBusVariant; class Actions; class Menu; class Window : public QObject, protected QDBusContext { Q_OBJECT // DBus Q_PROPERTY(QString Status READ status) Q_PROPERTY(uint Version READ version) public: Window(const QString &serviceName); ~Window() override; void init(); WId winId() const; void setWinId(WId winId); QString serviceName() const; QString applicationObjectPath() const; void setApplicationObjectPath(const QString &applicationObjectPath); QString unityObjectPath() const; void setUnityObjectPath(const QString &unityObjectPath); QString windowObjectPath() const; void setWindowObjectPath(const QString &windowObjectPath); QString applicationMenuObjectPath() const; void setApplicationMenuObjectPath(const QString &applicationMenuObjectPath); QString menuBarObjectPath() const; void setMenuBarObjectPath(const QString &menuBarObjectPath); QString currentMenuObjectPath() const; QString proxyObjectPath() const; // DBus bool AboutToShow(int id); void Event(int id, const QString &eventId, const QDBusVariant &data, uint timestamp); DBusMenuItemList GetGroupProperties(const QList &ids, const QStringList &propertyNames); uint GetLayout(int parentId, int recursionDepth, const QStringList &propertyNames, DBusMenuLayoutItem &dbusItem); QDBusVariant GetProperty(int id, const QString &property); QString status() const; uint version() const; signals: // don't want to pollute X stuff into Menu, let all of that be in MenuProxy void requestWriteWindowProperties(); void requestRemoveWindowProperties(); // DBus void ItemActivationRequested(int id, uint timestamp); void ItemsPropertiesUpdated(const DBusMenuItemList &updatedProps, const DBusMenuItemKeysList &removedProps); void LayoutUpdated(uint revision, int parent); private: void initMenu(); bool registerDBusObject(); void updateWindowProperties(); bool getAction(const QString &name, GMenuAction &action) const; - void triggerAction(const QString &name, uint timestamp = 0); + void triggerAction(const QString &name, const QVariant &target, uint timestamp = 0); Actions *getActionsForAction(const QString &name, QString &lookupName) const; void menuChanged(const QVector &menuIds); void menuItemsChanged(const QVector &itemIds); void onActionsChanged(const QStringList &dirty, const QString &prefix); void onMenuSubscribed(uint id); QVariantMap gMenuToDBusMenuProperties(const QVariantMap &source) const; WId m_winId = 0; QString m_serviceName; // original GMenu service (the gtk app) QString m_applicationObjectPath; QString m_unityObjectPath; QString m_windowObjectPath; QString m_applicationMenuObjectPath; QString m_menuBarObjectPath; QString m_currentMenuObjectPath; QString m_proxyObjectPath; // our object path on this proxy app QHash m_pendingGetLayouts; Menu *m_applicationMenu = nullptr; Menu *m_menuBar = nullptr; Menu *m_currentMenu = nullptr; Actions *m_applicationActions = nullptr; Actions *m_unityActions = nullptr; Actions *m_windowActions = nullptr; bool m_menuInited = false; }; diff --git a/runners/services/servicerunner.cpp b/runners/services/servicerunner.cpp index 5b63f75eb..6b391b92b 100644 --- a/runners/services/servicerunner.cpp +++ b/runners/services/servicerunner.cpp @@ -1,468 +1,472 @@ /* * Copyright (C) 2006 Aaron Seigo * Copyright (C) 2014 Vishesh Handa * Copyright (C) 2016 Harald Sitter * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Library General Public License version 2 as * published by the Free Software Foundation * * 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 Library General Public * License along with this program; if not, write to the * Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "servicerunner.h" #include #include #include #include #include #include #include #include #include #include #include #include #include "debug.h" namespace { int weightedLength(const QString &query) { return KStringHandler::logicalLength(query); } } // namespace /** * @brief Finds all KServices for a given runner query */ class ServiceFinder { public: ServiceFinder(ServiceRunner *runner) : m_runner(runner) {} void match(Plasma::RunnerContext &context) { if (!context.isValid()) { return; } term = context.query(); weightedTermLength = weightedLength(term); matchExectuables(); matchNameKeywordAndGenericName(); matchCategories(); matchJumpListActions(); context.addMatches(matches); } private: void seen(const KService::Ptr &service) { m_seen.insert(service->storageId()); m_seen.insert(service->exec()); } void seen(const KServiceAction &action) { m_seen.insert(action.exec()); } bool hasSeen(const KService::Ptr &service) { return m_seen.contains(service->storageId()) && m_seen.contains(service->exec()); } bool hasSeen(const KServiceAction &action) { return m_seen.contains(action.exec()); } bool disqualify(const KService::Ptr &service) { auto ret = hasSeen(service) || service->noDisplay(); qCDebug(RUNNER_SERVICES) << service->name() << "disqualified?" << ret; seen(service); return ret; } qreal increaseMatchRelavance(const KService::Ptr &service, QVector &strList, QString category) { //Increment the relevance based on all the words (other than the first) of the query list qreal relevanceIncrement = 0; for(int i=1; iname().contains(strList[i], Qt::CaseInsensitive)) { relevanceIncrement += 0.01; } } else if (category == QLatin1String("GenericName")) { if (service->genericName().contains(strList[i], Qt::CaseInsensitive)) { relevanceIncrement += 0.01; } } else if (category == QLatin1String("Exec")) { if (service->exec().contains(strList[i], Qt::CaseInsensitive)) { relevanceIncrement += 0.01; } } else if (category == QLatin1String("Comment")) { if (service->comment().contains(strList[i], Qt::CaseInsensitive)) { relevanceIncrement += 0.01; } } } return relevanceIncrement; } QString generateQuery(const QVector &strList) { QString keywordTemplate = QStringLiteral("exist Keywords"); QString genericNameTemplate = QStringLiteral("exist GenericName"); QString nameTemplate = QStringLiteral("exist Name"); QString commentTemplate = QStringLiteral("exist Comment"); // Search for applications which are executable and the term case-insensitive matches any of // * a substring of one of the keywords // * a substring of the GenericName field // * a substring of the Name field // Note that before asking for the content of e.g. Keywords and GenericName we need to ask if // they exist to prevent a tree evaluation error if they are not defined. for (const QStringRef &str : strList) { keywordTemplate += QStringLiteral(" and '%1' ~subin Keywords").arg(str.toString()); genericNameTemplate += QStringLiteral(" and '%1' ~~ GenericName").arg(str.toString()); nameTemplate += QStringLiteral(" and '%1' ~~ Name").arg(str.toString()); commentTemplate += QStringLiteral(" and '%1' ~~ Comment").arg(str.toString()); } QString finalQuery = QStringLiteral("exist Exec and ( (%1) or (%2) or (%3) or ('%4' ~~ Exec) or (%5) )") .arg(keywordTemplate, genericNameTemplate, nameTemplate, strList[0].toString(), commentTemplate); qCDebug(RUNNER_SERVICES) << "Final query : " << finalQuery; return finalQuery; } void setupMatch(const KService::Ptr &service, Plasma::QueryMatch &match) { const QString name = service->name(); match.setText(name); match.setData(service->storageId()); if (!service->genericName().isEmpty() && service->genericName() != name) { match.setSubtext(service->genericName()); } else if (!service->comment().isEmpty()) { match.setSubtext(service->comment()); } if (!service->icon().isEmpty()) { match.setIconName(service->icon()); } } void matchExectuables() { if (weightedTermLength < 2) { return; } // Search for applications which are executable and case-insensitively match the search term // See https://techbase.kde.org/Development/Tutorials/Services/Traders#The_KTrader_Query_Language // if the following is unclear to you. query = QStringLiteral("exist Exec and ('%1' =~ Name)").arg(term); const KService::List services = KServiceTypeTrader::self()->query(QStringLiteral("Application"), query); if (services.isEmpty()) { return; } for (const KService::Ptr &service : services) { qCDebug(RUNNER_SERVICES) << service->name() << "is an exact match!" << service->storageId() << service->exec(); if (disqualify(service)) { continue; } Plasma::QueryMatch match(m_runner); match.setType(Plasma::QueryMatch::ExactMatch); setupMatch(service, match); match.setRelevance(1); matches << match; } } void matchNameKeywordAndGenericName() { //Splitting the query term to match using subsequences QVector queryList = term.splitRef(QLatin1Char(' ')); // If the term length is < 3, no real point searching the Keywords and GenericName if (weightedTermLength < 3) { query = QStringLiteral("exist Exec and ( (exist Name and '%1' ~~ Name) or ('%1' ~~ Exec) )").arg(term); } else { //Match using subsequences (Bug: 262837) query = generateQuery(queryList); } KService::List services = KServiceTypeTrader::self()->query(QStringLiteral("Application"), query); services += KServiceTypeTrader::self()->query(QStringLiteral("KCModule"), query); qCDebug(RUNNER_SERVICES) << "got " << services.count() << " services from " << query; for (const KService::Ptr &service : qAsConst(services)) { if (disqualify(service)) { continue; } const QString id = service->storageId(); const QString name = service->desktopEntryName(); const QString exec = service->exec(); Plasma::QueryMatch match(m_runner); match.setType(Plasma::QueryMatch::PossibleMatch); setupMatch(service, match); qreal relevance(0.6); // If the term was < 3 chars and NOT at the beginning of the App's name or Exec, then // chances are the user doesn't want that app. if (weightedTermLength < 3) { if (name.startsWith(term, Qt::CaseInsensitive) || exec.startsWith(term, Qt::CaseInsensitive)) { relevance = 0.9; } else { continue; } } else if (service->name().contains(queryList[0], Qt::CaseInsensitive)) { relevance = 0.8; relevance += increaseMatchRelavance(service, queryList, QStringLiteral("Name")); if (service->name().startsWith(queryList[0], Qt::CaseInsensitive)) { relevance += 0.1; } } else if (service->genericName().contains(queryList[0], Qt::CaseInsensitive)) { relevance = 0.65; relevance += increaseMatchRelavance(service, queryList, QStringLiteral("GenericName")); if (service->genericName().startsWith(queryList[0], Qt::CaseInsensitive)) { relevance += 0.05; } } else if (service->exec().contains(queryList[0], Qt::CaseInsensitive)) { relevance = 0.7; relevance += increaseMatchRelavance(service, queryList, QStringLiteral("Exec")); if (service->exec().startsWith(queryList[0], Qt::CaseInsensitive)) { relevance += 0.05; } } else if (service->comment().contains(queryList[0], Qt::CaseInsensitive)) { relevance = 0.5; relevance += increaseMatchRelavance(service, queryList, QStringLiteral("Comment")); if (service->comment().startsWith(queryList[0], Qt::CaseInsensitive)) { relevance += 0.05; } } if (service->categories().contains(QLatin1String("KDE")) || service->serviceTypes().contains(QLatin1String("KCModule"))) { qCDebug(RUNNER_SERVICES) << "found a kde thing" << id << match.subtext() << relevance; relevance += .09; } qCDebug(RUNNER_SERVICES) << service->name() << "is this relevant:" << relevance; match.setRelevance(relevance); if (service->serviceTypes().contains(QLatin1String("KCModule"))) { match.setMatchCategory(i18n("System Settings")); } matches << match; } } void matchCategories() { //search for applications whose categories contains the query query = QStringLiteral("exist Exec and (exist Categories and '%1' ~subin Categories)").arg(term); const auto services = KServiceTypeTrader::self()->query(QStringLiteral("Application"), query); for (const KService::Ptr &service : services) { qCDebug(RUNNER_SERVICES) << service->name() << "is an exact match!" << service->storageId() << service->exec(); if (disqualify(service)) { continue; } Plasma::QueryMatch match(m_runner); match.setType(Plasma::QueryMatch::PossibleMatch); setupMatch(service, match); qreal relevance = 0.6; if (service->categories().contains(QLatin1String("X-KDE-More")) || !service->showInCurrentDesktop()) { relevance = 0.5; } if (service->isApplication()) { relevance += .04; } match.setRelevance(relevance); matches << match; } } void matchJumpListActions() { if (weightedTermLength < 3) { return; } query = QStringLiteral("exist Actions"); // doesn't work const auto services = KServiceTypeTrader::self()->query(QStringLiteral("Application"));//, query); for (const KService::Ptr &service : services) { if (service->noDisplay()) { continue; } for (const KServiceAction &action : service->actions()) { if (action.text().isEmpty() || action.exec().isEmpty() || hasSeen(action)) { continue; } seen(action); const int matchIndex = action.text().indexOf(term, 0, Qt::CaseInsensitive); if (matchIndex < 0) { continue; } Plasma::QueryMatch match(m_runner); - match.setType(Plasma::QueryMatch::HelperMatch); + match.setType(Plasma::QueryMatch::PossibleMatch); if (!action.icon().isEmpty()) { match.setIconName(action.icon()); } else { match.setIconName(service->icon()); } match.setText(i18nc("Jump list search result, %1 is action (eg. open new tab), %2 is application (eg. browser)", "%1 - %2", action.text(), service->name())); - match.setData(action.exec()); + match.setData(QStringLiteral("exec::") + action.exec()); qreal relevance = 0.5; if (matchIndex == 0) { relevance += 0.05; } match.setRelevance(relevance); matches << match; } } } ServiceRunner *m_runner; QSet m_seen; QList matches; QString query; QString term; int weightedTermLength; }; ServiceRunner::ServiceRunner(QObject *parent, const QVariantList &args) : Plasma::AbstractRunner(parent, args) { Q_UNUSED(args) setObjectName( QStringLiteral("Application" )); setPriority(AbstractRunner::HighestPriority); addSyntax(Plasma::RunnerSyntax(QStringLiteral(":q:"), i18n("Finds applications whose name or description match :q:"))); } ServiceRunner::~ServiceRunner() { } QStringList ServiceRunner::categories() const { QStringList cat; cat << i18n("Applications") << i18n("System Settings"); return cat; } QIcon ServiceRunner::categoryIcon(const QString& category) const { if (category == i18n("Applications")) { return QIcon::fromTheme(QStringLiteral("applications-other")); } else if (category == i18n("System Settings")) { return QIcon::fromTheme(QStringLiteral("preferences-system")); } return Plasma::AbstractRunner::categoryIcon(category); } void ServiceRunner::match(Plasma::RunnerContext &context) { // This helper class aids in keeping state across numerous // different queries that together form the matches set. ServiceFinder finder(this); finder.match(context); } void ServiceRunner::run(const Plasma::RunnerContext &context, const Plasma::QueryMatch &match) { Q_UNUSED(context); - if (match.type() == Plasma::QueryMatch::HelperMatch) { // Jump List Action - KRun::run(match.data().toString(), {}, nullptr); + + const QString dataString = match.data().toString(); + + const QString execPrefix = QStringLiteral("exec::"); + if (dataString.startsWith(execPrefix)) { + KRun::run(dataString.mid(execPrefix.length()), {}, nullptr); return; } - KService::Ptr service = KService::serviceByStorageId(match.data().toString()); + KService::Ptr service = KService::serviceByStorageId(dataString); if (service) { KActivities::ResourceInstance::notifyAccessed( QUrl(QStringLiteral("applications:") + service->storageId()), QStringLiteral("org.kde.krunner") ); KRun::runService(*service, {}, nullptr, true); } } QMimeData * ServiceRunner::mimeDataForMatch(const Plasma::QueryMatch &match) { KService::Ptr service = KService::serviceByStorageId(match.data().toString()); if (!service) { return nullptr; } QString path = service->entryPath(); if (!QDir::isAbsolutePath(path)) { path = QStandardPaths::locate(QStandardPaths::GenericDataLocation, QLatin1String("kservices5/") + path); } if (path.isEmpty()) { return nullptr; } QMimeData *data = new QMimeData(); data->setUrls(QList{QUrl::fromLocalFile(path)}); return data; } K_EXPORT_PLASMA_RUNNER(services, ServiceRunner) #include "servicerunner.moc"