From 61061644a5419b0adacc23bcd62114d62b8cc31b Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Mon, 29 Apr 2024 23:57:26 -0700 Subject: [PATCH] dbus/dbusmenu: add DBusMenu support --- src/core/imageprovider.cpp | 2 +- src/dbus/CMakeLists.txt | 3 +- src/dbus/dbusmenu/CMakeLists.txt | 25 + src/dbus/dbusmenu/com.canonical.dbusmenu.xml | 68 +++ src/dbus/dbusmenu/dbus_menu_types.cpp | 92 ++++ src/dbus/dbusmenu/dbus_menu_types.hpp | 47 ++ src/dbus/dbusmenu/dbusmenu.cpp | 525 +++++++++++++++++++ src/dbus/dbusmenu/dbusmenu.hpp | 176 +++++++ src/services/status_notifier/CMakeLists.txt | 2 +- src/services/status_notifier/item.cpp | 20 +- src/services/status_notifier/item.hpp | 3 + src/services/status_notifier/qml.cpp | 19 + src/services/status_notifier/qml.hpp | 13 +- 13 files changed, 984 insertions(+), 11 deletions(-) create mode 100644 src/dbus/dbusmenu/CMakeLists.txt create mode 100644 src/dbus/dbusmenu/com.canonical.dbusmenu.xml create mode 100644 src/dbus/dbusmenu/dbus_menu_types.cpp create mode 100644 src/dbus/dbusmenu/dbus_menu_types.hpp create mode 100644 src/dbus/dbusmenu/dbusmenu.cpp create mode 100644 src/dbus/dbusmenu/dbusmenu.hpp diff --git a/src/core/imageprovider.cpp b/src/core/imageprovider.cpp index e33f6c1a..cc81c47f 100644 --- a/src/core/imageprovider.cpp +++ b/src/core/imageprovider.cpp @@ -8,7 +8,7 @@ #include #include -static QMap liveImages; +static QMap liveImages; // NOLINT QsImageHandle::QsImageHandle(QQmlImageProviderBase::ImageType type, QObject* parent) : QObject(parent) diff --git a/src/dbus/CMakeLists.txt b/src/dbus/CMakeLists.txt index a468c6b2..ee6df30a 100644 --- a/src/dbus/CMakeLists.txt +++ b/src/dbus/CMakeLists.txt @@ -18,4 +18,5 @@ target_include_directories(quickshell-dbus PRIVATE ${CMAKE_CURRENT_BINARY_DIR}) target_link_libraries(quickshell-dbus PRIVATE ${QT_DEPS}) qs_pch(quickshell-dbus) -#qs_pch(quickshell-dbusplugin) + +add_subdirectory(dbusmenu) diff --git a/src/dbus/dbusmenu/CMakeLists.txt b/src/dbus/dbusmenu/CMakeLists.txt new file mode 100644 index 00000000..ab222e5a --- /dev/null +++ b/src/dbus/dbusmenu/CMakeLists.txt @@ -0,0 +1,25 @@ +set_source_files_properties(com.canonical.dbusmenu.xml PROPERTIES + CLASSNAME DBusMenuInterface + INCLUDE ${CMAKE_CURRENT_SOURCE_DIR}/dbus_menu_types.hpp +) + +qt_add_dbus_interface(DBUS_INTERFACES + com.canonical.dbusmenu.xml + dbus_menu +) + +qt_add_library(quickshell-dbusmenu STATIC + dbus_menu_types.cpp + dbusmenu.cpp + ${DBUS_INTERFACES} +) + +qt_add_qml_module(quickshell-dbusmenu URI Quickshell.DBusMenu VERSION 0.1) + +# dbus headers +target_include_directories(quickshell-dbusmenu PRIVATE ${CMAKE_CURRENT_BINARY_DIR}) + +target_link_libraries(quickshell-dbusmenu PRIVATE ${QT_DEPS}) + +qs_pch(quickshell-dbusmenu) +qs_pch(quickshell-dbusmenuplugin) diff --git a/src/dbus/dbusmenu/com.canonical.dbusmenu.xml b/src/dbus/dbusmenu/com.canonical.dbusmenu.xml new file mode 100644 index 00000000..12f021bc --- /dev/null +++ b/src/dbus/dbusmenu/com.canonical.dbusmenu.xml @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/dbus/dbusmenu/dbus_menu_types.cpp b/src/dbus/dbusmenu/dbus_menu_types.cpp new file mode 100644 index 00000000..36ae41fe --- /dev/null +++ b/src/dbus/dbusmenu/dbus_menu_types.cpp @@ -0,0 +1,92 @@ +#include "dbus_menu_types.hpp" + +#include +#include +#include +#include +#include + +const QDBusArgument& operator>>(const QDBusArgument& argument, DBusMenuLayout& layout) { + layout.children.clear(); + + argument.beginStructure(); + argument >> layout.id; + argument >> layout.properties; + + argument.beginArray(); + while (!argument.atEnd()) { + auto childArgument = qdbus_cast(argument).variant().value(); + auto child = qdbus_cast(childArgument); + layout.children.append(child); + } + argument.endArray(); + + argument.endStructure(); + return argument; +} + +const QDBusArgument& operator<<(QDBusArgument& argument, const DBusMenuLayout& layout) { + argument.beginStructure(); + argument << layout.id; + argument << layout.properties; + + argument.beginArray(qMetaTypeId()); + for (const auto& child: layout.children) { + argument << QDBusVariant(QVariant::fromValue(child)); + } + argument.endArray(); + + argument.endStructure(); + return argument; +} + +const QDBusArgument& operator>>(const QDBusArgument& argument, DBusMenuItemProperties& item) { + argument.beginStructure(); + argument >> item.id; + argument >> item.properties; + argument.endStructure(); + return argument; +} + +const QDBusArgument& operator<<(QDBusArgument& argument, const DBusMenuItemProperties& item) { + argument.beginStructure(); + argument << item.id; + argument << item.properties; + argument.endStructure(); + return argument; +} + +const QDBusArgument& operator>>(const QDBusArgument& argument, DBusMenuItemPropertyNames& names) { + argument.beginStructure(); + argument >> names.id; + argument >> names.properties; + argument.endStructure(); + return argument; +} + +const QDBusArgument& operator<<(QDBusArgument& argument, const DBusMenuItemPropertyNames& names) { + argument.beginStructure(); + argument << names.id; + argument << names.properties; + argument.endStructure(); + return argument; +} + +QDebug operator<<(QDebug debug, const DBusMenuLayout& layout) { + debug.nospace() << "DBusMenuLayout(id=" << layout.id << ", properties=" << layout.properties + << ", children=" << layout.children << ")"; + + return debug; +} + +QDebug operator<<(QDebug debug, const DBusMenuItemProperties& item) { + debug.nospace() << "DBusMenuItemProperties(id=" << item.id << ", properties=" << item.properties + << ")"; + return debug; +} + +QDebug operator<<(QDebug debug, const DBusMenuItemPropertyNames& names) { + debug.nospace() << "DBusMenuItemPropertyNames(id=" << names.id + << ", properties=" << names.properties << ")"; + return debug; +} diff --git a/src/dbus/dbusmenu/dbus_menu_types.hpp b/src/dbus/dbusmenu/dbus_menu_types.hpp new file mode 100644 index 00000000..29659497 --- /dev/null +++ b/src/dbus/dbusmenu/dbus_menu_types.hpp @@ -0,0 +1,47 @@ +#pragma once + +#include +#include +#include +#include +#include + +struct DBusMenuLayout { + qint32 id = 0; + QVariantMap properties; + QList children; +}; + +using DBusMenuIdList = QList; + +struct DBusMenuItemProperties { + qint32 id = 0; + QVariantMap properties; +}; + +using DBusMenuItemPropertiesList = QList; + +struct DBusMenuItemPropertyNames { + qint32 id = 0; + QStringList properties; +}; + +using DBusMenuItemPropertyNamesList = QList; + +const QDBusArgument& operator>>(const QDBusArgument& argument, DBusMenuLayout& layout); +const QDBusArgument& operator<<(QDBusArgument& argument, const DBusMenuLayout& layout); +const QDBusArgument& operator>>(const QDBusArgument& argument, DBusMenuItemProperties& item); +const QDBusArgument& operator<<(QDBusArgument& argument, const DBusMenuItemProperties& item); +const QDBusArgument& operator>>(const QDBusArgument& argument, DBusMenuItemPropertyNames& names); +const QDBusArgument& operator<<(QDBusArgument& argument, const DBusMenuItemPropertyNames& names); + +QDebug operator<<(QDebug debug, const DBusMenuLayout& layout); +QDebug operator<<(QDebug debug, const DBusMenuItemProperties& item); +QDebug operator<<(QDebug debug, const DBusMenuItemPropertyNames& names); + +Q_DECLARE_METATYPE(DBusMenuLayout); +Q_DECLARE_METATYPE(DBusMenuIdList); +Q_DECLARE_METATYPE(DBusMenuItemProperties); +Q_DECLARE_METATYPE(DBusMenuItemPropertiesList); +Q_DECLARE_METATYPE(DBusMenuItemPropertyNames); +Q_DECLARE_METATYPE(DBusMenuItemPropertyNamesList); diff --git a/src/dbus/dbusmenu/dbusmenu.cpp b/src/dbus/dbusmenu/dbusmenu.cpp new file mode 100644 index 00000000..7484d849 --- /dev/null +++ b/src/dbus/dbusmenu/dbusmenu.cpp @@ -0,0 +1,525 @@ +#include "dbusmenu.hpp" +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../../core/iconimageprovider.hpp" +#include "../../dbus/properties.hpp" +#include "dbus_menu.h" +#include "dbus_menu_types.hpp" + +Q_LOGGING_CATEGORY(logDbusMenu, "quickshell.dbus.dbusmenu", QtWarningMsg); + +namespace qs::dbus::dbusmenu { + +DBusMenuItem::DBusMenuItem(qint32 id, DBusMenu* menu, DBusMenuItem* parentMenu) + : QObject(menu) + , id(id) + , menu(menu) + , parentMenu(parentMenu) { + QObject::connect( + &this->menu->iconThemePath, + &AbstractDBusProperty::changed, + this, + &DBusMenuItem::iconChanged + ); +} + +void DBusMenuItem::click() { + if (this->displayChildren) { + this->setShowChildren(!this->mShowChildren); + } else { + this->menu->sendEvent(this->id, "clicked"); + } +} + +void DBusMenuItem::hover() const { this->menu->sendEvent(this->id, "hovered"); } + +DBusMenu* DBusMenuItem::menuHandle() const { return this->menu; } +QString DBusMenuItem::label() const { return this->mLabel; } +QString DBusMenuItem::cleanLabel() const { return this->mCleanLabel; } +bool DBusMenuItem::enabled() const { return this->mEnabled; } + +QString DBusMenuItem::icon() const { + if (!this->iconName.isEmpty()) { + return IconImageProvider::requestString( + this->iconName, + this->menu->iconThemePath.get().join(':') + ); + } else if (this->image != nullptr) { + return this->image->url(); + } else return nullptr; +} + +ToggleButtonType::Enum DBusMenuItem::toggleType() const { return this->mToggleType; }; +Qt::CheckState DBusMenuItem::checkState() const { return this->mCheckState; } +bool DBusMenuItem::isSeparator() const { return this->mSeparator; } + +bool DBusMenuItem::isShowingChildren() const { return this->mShowChildren && this->childrenLoaded; } + +void DBusMenuItem::setShowChildren(bool showChildren) { + if (showChildren == this->mShowChildren) return; + this->mShowChildren = showChildren; + this->childrenLoaded = false; + + if (showChildren) { + this->menu->prepareToShow(this->id, true); + } else { + this->menu->sendEvent(this->id, "closed"); + emit this->showingChildrenChanged(); + + if (!this->mChildren.isEmpty()) { + for (auto child: this->mChildren) { + this->menu->removeRecursive(child); + } + + this->mChildren.clear(); + this->onChildrenUpdated(); + } + } +} + +bool DBusMenuItem::hasChildren() const { return this->displayChildren; } + +QQmlListProperty DBusMenuItem::children() { + return QQmlListProperty( + this, + nullptr, + &DBusMenuItem::childrenCount, + &DBusMenuItem::childAt + ); +} + +qsizetype DBusMenuItem::childrenCount(QQmlListProperty* property) { + return reinterpret_cast(property->object)->enabledChildren.count(); // NOLINT +} + +DBusMenuItem* DBusMenuItem::childAt(QQmlListProperty* property, qsizetype index) { + auto* item = reinterpret_cast(property->object); // NOLINT + return item->menu->items.value(item->enabledChildren.at(index)); +} + +void DBusMenuItem::updateProperties(const QVariantMap& properties, const QStringList& removed) { + // Some programs appear to think sending an empty map does not mean "reset everything" + // and instead means "do nothing". oh well... + if (properties.isEmpty() && removed.isEmpty()) { + qCDebug(logDbusMenu) << "Ignoring empty property update for" << this; + return; + } + + auto originalLabel = this->mLabel; + //auto originalMnemonic = this->mnemonic; + auto originalEnabled = this->mEnabled; + auto originalVisible = this->visible; + auto originalIconName = this->iconName; + auto* originalImage = this->image; + auto originalIsSeparator = this->mSeparator; + auto originalToggleType = this->mToggleType; + auto originalToggleState = this->mCheckState; + auto originalDisplayChildren = this->displayChildren; + + auto label = properties.value("label"); + if (label.canConvert()) { + auto text = label.value(); + this->mLabel = text; + this->mCleanLabel = text; + //this->mnemonic = QChar(); + + for (auto i = 0; i < this->mLabel.length() - 1;) { + if (this->mLabel.at(i) == '_') { + //if (this->mnemonic == QChar()) this->mnemonic = this->mLabel.at(i + 1); + this->mLabel.remove(i, 1); + this->mLabel.insert(i + 1, ""); + this->mLabel.insert(i, ""); + i += 8; + } else { + i++; + } + } + + for (auto i = 0; i < this->mCleanLabel.length() - 1; i++) { + if (this->mCleanLabel.at(i) == '_') { + this->mCleanLabel.remove(i, 1); + } + } + } else if (removed.isEmpty() || removed.contains("label")) { + this->mLabel = ""; + //this->mnemonic = QChar(); + } + + auto enabled = properties.value("enabled"); + if (enabled.canConvert()) { + this->mEnabled = enabled.value(); + } else if (removed.isEmpty() || removed.contains("enabled")) { + this->mEnabled = true; + } + + auto visible = properties.value("visible"); + if (visible.canConvert()) { + this->visible = visible.value(); + } else if (removed.isEmpty() || removed.contains("visible")) { + this->visible = true; + } + + auto iconName = properties.value("icon-name"); + if (iconName.canConvert()) { + this->iconName = iconName.value(); + } else if (removed.isEmpty() || removed.contains("icon-name")) { + this->iconName = ""; + } + + auto iconData = properties.value("icon-data"); + if (iconData.canConvert()) { + auto data = iconData.value(); + if (data.isEmpty()) { + this->image = nullptr; + } else if (this->image == nullptr || this->image->data != data) { + this->image = new DBusMenuPngImage(data, this); + } + } else if (removed.isEmpty() || removed.contains("icon-data")) { + this->image = nullptr; + } + + auto type = properties.value("type"); + if (type.canConvert()) { + this->mSeparator = type.value() == "separator"; + } else if (removed.isEmpty() || removed.contains("type")) { + this->mSeparator = false; + } + + auto toggleType = properties.value("toggle-type"); + if (toggleType.canConvert()) { + auto toggleTypeStr = toggleType.value(); + + if (toggleTypeStr == "") this->mToggleType = ToggleButtonType::None; + else if (toggleTypeStr == "checkmark") this->mToggleType = ToggleButtonType::CheckBox; + else if (toggleTypeStr == "radio") this->mToggleType = ToggleButtonType::RadioButton; + else { + qCWarning(logDbusMenu) << "Unrecognized toggle type" << toggleTypeStr << "for" << this; + this->mToggleType = ToggleButtonType::None; + } + } else if (removed.isEmpty() || removed.contains("toggle-type")) { + this->mToggleType = ToggleButtonType::None; + } + + auto toggleState = properties.value("toggle-state"); + if (toggleState.canConvert()) { + auto toggleStateInt = toggleState.value(); + + if (toggleStateInt == 0) this->mCheckState = Qt::Unchecked; + else if (toggleStateInt == 1) this->mCheckState = Qt::Checked; + else this->mCheckState = Qt::PartiallyChecked; + } else if (removed.isEmpty() || removed.contains("toggle-state")) { + this->mCheckState = Qt::PartiallyChecked; + } + + auto childrenDisplay = properties.value("children-display"); + if (childrenDisplay.canConvert()) { + auto childrenDisplayStr = childrenDisplay.value(); + + if (childrenDisplayStr == "") this->displayChildren = false; + else if (childrenDisplayStr == "submenu") this->displayChildren = true; + else { + qCWarning(logDbusMenu) << "Unrecognized children-display mode" << childrenDisplayStr << "for" + << this; + this->displayChildren = false; + } + } else if (removed.isEmpty() || removed.contains("children-display")) { + this->displayChildren = false; + } + + if (this->mLabel != originalLabel) emit this->labelChanged(); + //if (this->mnemonic != originalMnemonic) emit this->labelChanged(); + if (this->mEnabled != originalEnabled) emit this->enabledChanged(); + if (this->visible != originalVisible && this->parentMenu != nullptr) + this->parentMenu->onChildrenUpdated(); + if (this->mToggleType != originalToggleType) emit this->toggleTypeChanged(); + if (this->mCheckState != originalToggleState) emit this->checkStateChanged(); + if (this->mSeparator != originalIsSeparator) emit this->separatorChanged(); + if (this->displayChildren != originalDisplayChildren) emit this->hasChildrenChanged(); + + if (this->iconName != originalIconName || this->image != originalImage) { + if (this->image != originalImage) { + delete originalImage; + } + + emit this->iconChanged(); + } + + qCDebug(logDbusMenu).nospace() << "Updated properties of " << this << " { label=" << this->mLabel + << ", enabled=" << this->mEnabled << ", visible=" << this->visible + << ", iconName=" << this->iconName << ", iconData=" << this->image + << ", separator=" << this->mSeparator + << ", toggleType=" << this->mToggleType + << ", toggleState=" << this->mCheckState + << ", displayChildren=" << this->displayChildren << " }"; +} + +void DBusMenuItem::onChildrenUpdated() { + this->enabledChildren.clear(); + + for (auto child: this->mChildren) { + auto* item = this->menu->items.value(child); + if (item->visible) this->enabledChildren.push_back(child); + } + + emit this->childrenChanged(); +} + +QDebug operator<<(QDebug debug, DBusMenuItem* item) { + if (item == nullptr) { + debug << "DBusMenuItem(nullptr)"; + return debug; + } + + auto saver = QDebugStateSaver(debug); + debug.nospace() << "DBusMenuItem(" << static_cast(item) << ", id=" << item->id + << ", label=" << item->mLabel << ", menu=" << item->menu << ")"; + return debug; +} + +QDebug operator<<(QDebug debug, const ToggleButtonType::Enum& toggleType) { + auto saver = QDebugStateSaver(debug); + debug.nospace() << "ToggleType::"; + + switch (toggleType) { + case ToggleButtonType::None: debug << "None"; break; + case ToggleButtonType::CheckBox: debug << "Checkbox"; break; + case ToggleButtonType::RadioButton: debug << "Radiobutton"; break; + } + + return debug; +} + +DBusMenu::DBusMenu(const QString& service, const QString& path, QObject* parent): QObject(parent) { + qDBusRegisterMetaType(); + qDBusRegisterMetaType(); + qDBusRegisterMetaType(); + qDBusRegisterMetaType(); + qDBusRegisterMetaType(); + qDBusRegisterMetaType(); + + this->interface = new DBusMenuInterface(service, path, QDBusConnection::sessionBus(), this); + + if (!this->interface->isValid()) { + qCWarning(logDbusMenu).noquote() << "Cannot create DBusMenu for" << service << "at" << path; + return; + } + + QObject::connect( + this->interface, + &DBusMenuInterface::LayoutUpdated, + this, + &DBusMenu::onLayoutUpdated + ); + + this->properties.setInterface(this->interface); + this->properties.updateAllViaGetAll(); +} + +void DBusMenu::prepareToShow(qint32 item, bool sendOpened) { + auto pending = this->interface->AboutToShow(item); + auto* call = new QDBusPendingCallWatcher(pending, this); + + auto responseCallback = [this, item, sendOpened](QDBusPendingCallWatcher* call) { + const QDBusPendingReply reply = *call; + if (reply.isError()) { + qCWarning(logDbusMenu) << "Error in AboutToShow, but showing anyway for menu" << item << "of" + << this << reply.error(); + } + + this->updateLayout(item, 1); + if (sendOpened) this->sendEvent(item, "opened"); + + delete call; + }; + + QObject::connect(call, &QDBusPendingCallWatcher::finished, this, responseCallback); +} + +void DBusMenu::updateLayout(qint32 parent, qint32 depth) { + auto pending = this->interface->GetLayout(parent, depth, QStringList()); + auto* call = new QDBusPendingCallWatcher(pending, this); + + auto responseCallback = [this, parent, depth](QDBusPendingCallWatcher* call) { + const QDBusPendingReply reply = *call; + + if (reply.isError()) { + qCWarning(logDbusMenu) << "Error updating layout for menu" << parent << "of" << this + << reply.error(); + } else { + auto layout = reply.argumentAt<1>(); + this->updateLayoutRecursive(layout, this->items.value(parent), depth); + } + + delete call; + }; + + QObject::connect(call, &QDBusPendingCallWatcher::finished, this, responseCallback); +} + +void DBusMenu::updateLayoutRecursive( + const DBusMenuLayout& layout, + DBusMenuItem* parent, + qint32 depth +) { + auto* item = this->items.value(layout.id); + if (item == nullptr) { + // there is an actual nullptr in the map and not no entry + if (this->items.contains(layout.id)) { + item = new DBusMenuItem(layout.id, this, parent); + this->items.insert(layout.id, item); + } + } + + if (item == nullptr) return; + + qCDebug(logDbusMenu) << "Updating layout recursively for" << this << "menu" << layout.id; + item->updateProperties(layout.properties); + + if (depth != 0) { + auto childrenChanged = false; + auto iter = item->mChildren.begin(); + while (iter != item->mChildren.end()) { + auto existing = std::find_if( + layout.children.begin(), + layout.children.end(), + [&](const DBusMenuLayout& layout) { return layout.id == *iter; } + ); + + if (existing == layout.children.end()) { + qCDebug(logDbusMenu) << "Removing missing layout item" << this->items.value(*iter) << "from" + << item; + this->removeRecursive(*iter); + iter = item->mChildren.erase(iter); + childrenChanged = true; + } else { + iter++; + } + } + + for (const auto& child: layout.children) { + if (item->mShowChildren && !item->mChildren.contains(child.id)) { + qCDebug(logDbusMenu) << "Creating new layout item" << child.id << "in" << item; + item->mChildren.push_back(child.id); + this->items.insert(child.id, nullptr); + childrenChanged = true; + } + + this->updateLayoutRecursive(child, item, depth - 1); + } + + if (childrenChanged) item->onChildrenUpdated(); + } + + if (item->mShowChildren && !item->childrenLoaded) { + item->childrenLoaded = true; + emit item->showingChildrenChanged(); + } +} + +void DBusMenu::removeRecursive(qint32 id) { + auto* item = this->items.value(id); + + if (item != nullptr) { + for (auto child: item->mChildren) { + this->removeRecursive(child); + } + } + + this->items.remove(id); + + if (item != nullptr) { + item->deleteLater(); + } +} + +void DBusMenu::sendEvent(qint32 item, const QString& event) { + qCDebug(logDbusMenu) << "Sending event" << event << "to menu" << item << "of" << this; + + auto pending = + this->interface->Event(item, event, QDBusVariant(0), QDateTime::currentSecsSinceEpoch()); + auto* call = new QDBusPendingCallWatcher(pending, this); + + auto responseCallback = [this, item, event](QDBusPendingCallWatcher* call) { + const QDBusPendingReply<> reply = *call; + + if (reply.isError()) { + qCWarning(logDbusMenu) << "Error sending event" << event << "to" << item << "of" << this + << reply.error(); + } + + delete call; + }; + + QObject::connect(call, &QDBusPendingCallWatcher::finished, this, responseCallback); +} + +DBusMenuItem* DBusMenu::menu() { return &this->rootItem; } + +void DBusMenu::onLayoutUpdated(quint32 /*unused*/, qint32 parent) { + // note: spec says this is recursive + this->updateLayout(parent, -1); +} + +void DBusMenu::onItemPropertiesUpdated( // NOLINT + const DBusMenuItemPropertiesList& updatedProps, + const DBusMenuItemPropertyNamesList& removedProps +) { + for (const auto& propset: updatedProps) { + auto* item = this->items.value(propset.id); + if (item != nullptr) { + item->updateProperties(propset.properties); + } + } + + for (const auto& propset: removedProps) { + auto* item = this->items.value(propset.id); + if (item != nullptr) { + item->updateProperties({}, propset.properties); + } + } +} + +QDebug operator<<(QDebug debug, DBusMenu* menu) { + if (menu == nullptr) { + debug << "DBusMenu(nullptr)"; + return debug; + } + + auto saver = QDebugStateSaver(debug); + debug.nospace() << "DBusMenu(" << static_cast(menu) << ", " << menu->properties.toString() + << ")"; + return debug; +} + +QImage +DBusMenuPngImage::requestImage(const QString& /*unused*/, QSize* size, const QSize& /*unused*/) { + auto image = QImage(); + + if (!image.loadFromData(this->data, "PNG")) { + qCWarning(logDbusMenu) << "Failed to load dbusmenu item png"; + } + + if (size != nullptr) *size = image.size(); + return image; +} + +} // namespace qs::dbus::dbusmenu diff --git a/src/dbus/dbusmenu/dbusmenu.hpp b/src/dbus/dbusmenu/dbusmenu.hpp new file mode 100644 index 00000000..dcf4d688 --- /dev/null +++ b/src/dbus/dbusmenu/dbusmenu.hpp @@ -0,0 +1,176 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../../core/imageprovider.hpp" +#include "../properties.hpp" +#include "dbus_menu_types.hpp" + +Q_DECLARE_LOGGING_CATEGORY(logDbusMenu); + +namespace ToggleButtonType { // NOLINT +Q_NAMESPACE; +QML_ELEMENT; + +enum Enum { + None = 0, + CheckBox = 1, + RadioButton = 2, +}; +Q_ENUM_NS(Enum); + +} // namespace ToggleButtonType + +class DBusMenuInterface; + +namespace qs::dbus::dbusmenu { + +QDebug operator<<(QDebug debug, const ToggleButtonType::Enum& toggleType); + +class DBusMenu; +class DBusMenuPngImage; + +class DBusMenuItem: public QObject { + Q_OBJECT; + // clang-format off + Q_PROPERTY(DBusMenu* menuHandle READ menuHandle CONSTANT); + Q_PROPERTY(QString label READ label NOTIFY labelChanged); + Q_PROPERTY(QString cleanLabel READ cleanLabel NOTIFY labelChanged); + Q_PROPERTY(bool enabled READ enabled NOTIFY enabledChanged); + Q_PROPERTY(QString icon READ icon NOTIFY iconChanged); + Q_PROPERTY(ToggleButtonType::Enum toggleType READ toggleType NOTIFY toggleTypeChanged); + Q_PROPERTY(Qt::CheckState checkState READ checkState NOTIFY checkStateChanged); + Q_PROPERTY(bool isSeparator READ isSeparator NOTIFY separatorChanged); + Q_PROPERTY(bool hasChildren READ hasChildren NOTIFY hasChildrenChanged); + Q_PROPERTY(bool showChildren READ isShowingChildren WRITE setShowChildren NOTIFY showingChildrenChanged); + Q_PROPERTY(QQmlListProperty children READ children NOTIFY childrenChanged); + // clang-format on + QML_NAMED_ELEMENT(DBusMenu); + QML_UNCREATABLE("DBusMenus can only be acquired from a DBusMenuHandle"); + +public: + explicit DBusMenuItem(qint32 id, DBusMenu* menu, DBusMenuItem* parentMenu); + + Q_INVOKABLE void click(); + Q_INVOKABLE void hover() const; + + [[nodiscard]] DBusMenu* menuHandle() const; + [[nodiscard]] QString label() const; + [[nodiscard]] QString cleanLabel() const; + [[nodiscard]] bool enabled() const; + [[nodiscard]] QString icon() const; + [[nodiscard]] ToggleButtonType::Enum toggleType() const; + [[nodiscard]] Qt::CheckState checkState() const; + [[nodiscard]] bool isSeparator() const; + [[nodiscard]] bool hasChildren() const; + + [[nodiscard]] bool isShowingChildren() const; + void setShowChildren(bool showChildren); + + [[nodiscard]] QQmlListProperty children(); + + void updateProperties(const QVariantMap& properties, const QStringList& removed = {}); + void onChildrenUpdated(); + + qint32 id = 0; + QString mLabel; + QVector mChildren; + bool mShowChildren = false; + bool childrenLoaded = false; + DBusMenu* menu = nullptr; + +signals: + void labelChanged(); + //void mnemonicChanged(); + void enabledChanged(); + void iconChanged(); + void separatorChanged(); + void toggleTypeChanged(); + void checkStateChanged(); + void hasChildrenChanged(); + void showingChildrenChanged(); + void childrenChanged(); + +private: + QString mCleanLabel; + //QChar mnemonic; + bool mEnabled = true; + bool visible = true; + bool mSeparator = false; + QString iconName; + DBusMenuPngImage* image = nullptr; + ToggleButtonType::Enum mToggleType = ToggleButtonType::None; + Qt::CheckState mCheckState = Qt::Checked; + bool displayChildren = false; + QVector enabledChildren; + DBusMenuItem* parentMenu = nullptr; + + static qsizetype childrenCount(QQmlListProperty* property); + static DBusMenuItem* childAt(QQmlListProperty* property, qsizetype index); +}; + +QDebug operator<<(QDebug debug, DBusMenuItem* item); + +class DBusMenu: public QObject { + Q_OBJECT; + Q_PROPERTY(DBusMenuItem* menu READ menu CONSTANT); + QML_NAMED_ELEMENT(DBusMenuHandle); + QML_UNCREATABLE("Menu handles cannot be directly created"); + +public: + explicit DBusMenu(const QString& service, const QString& path, QObject* parent = nullptr); + + dbus::DBusPropertyGroup properties; + dbus::DBusProperty version {this->properties, "Version"}; + dbus::DBusProperty textDirection {this->properties, "TextDirection"}; + dbus::DBusProperty status {this->properties, "Status"}; + dbus::DBusProperty iconThemePath {this->properties, "IconThemePath"}; + + void prepareToShow(qint32 item, bool sendOpened); + void updateLayout(qint32 parent, qint32 depth); + void removeRecursive(qint32 id); + void sendEvent(qint32 item, const QString& event); + + DBusMenuItem rootItem {0, this, nullptr}; + QHash items {std::make_pair(0, &this->rootItem)}; + + [[nodiscard]] DBusMenuItem* menu(); + +private slots: + void onLayoutUpdated(quint32 revision, qint32 parent); + void onItemPropertiesUpdated( + const DBusMenuItemPropertiesList& updatedProps, + const DBusMenuItemPropertyNamesList& removedProps + ); + +private: + void updateLayoutRecursive(const DBusMenuLayout& layout, DBusMenuItem* parent, qint32 depth); + + DBusMenuInterface* interface = nullptr; +}; + +QDebug operator<<(QDebug debug, DBusMenu* menu); + +class DBusMenuPngImage: public QsImageHandle { +public: + explicit DBusMenuPngImage(QByteArray data, DBusMenuItem* parent) + : QsImageHandle(QQuickImageProvider::Image, parent) + , data(std::move(data)) {} + + QImage requestImage(const QString& id, QSize* size, const QSize& requestedSize) override; + + QByteArray data; +}; + +} // namespace qs::dbus::dbusmenu diff --git a/src/services/status_notifier/CMakeLists.txt b/src/services/status_notifier/CMakeLists.txt index c92b6a55..79026836 100644 --- a/src/services/status_notifier/CMakeLists.txt +++ b/src/services/status_notifier/CMakeLists.txt @@ -43,7 +43,7 @@ qt_add_qml_module(quickshell-service-statusnotifier VERSION 0.1 ) -target_link_libraries(quickshell-service-statusnotifier PRIVATE ${QT_DEPS} quickshell-dbus) +target_link_libraries(quickshell-service-statusnotifier PRIVATE ${QT_DEPS} quickshell-dbus quickshell-dbusmenuplugin) target_link_libraries(quickshell PRIVATE quickshell-service-statusnotifierplugin) qs_pch(quickshell-service-statusnotifier) diff --git a/src/services/status_notifier/item.cpp b/src/services/status_notifier/item.cpp index b3cceecd..b8359ee8 100644 --- a/src/services/status_notifier/item.cpp +++ b/src/services/status_notifier/item.cpp @@ -1,6 +1,7 @@ #include "item.hpp" #include +#include #include #include #include @@ -20,12 +21,14 @@ #include "../../core/iconimageprovider.hpp" #include "../../core/imageprovider.hpp" +#include "../../dbus/dbusmenu/dbusmenu.hpp" #include "../../dbus/properties.hpp" #include "dbus_item.h" #include "dbus_item_types.hpp" #include "host.hpp" using namespace qs::dbus; +using namespace qs::dbus::dbusmenu; Q_LOGGING_CATEGORY(logStatusNotifierItem, "quickshell.service.sni.item", QtWarningMsg); @@ -34,6 +37,10 @@ namespace qs::service::sni { StatusNotifierItem::StatusNotifierItem(const QString& address, QObject* parent) : QObject(parent) , watcherId(address) { + qDBusRegisterMetaType(); + qDBusRegisterMetaType(); + qDBusRegisterMetaType(); + // spec is unclear about what exactly an item address is, so account for both combinations auto splitIdx = address.indexOf('/'); auto conn = splitIdx == -1 ? address : address.sliced(0, splitIdx); @@ -46,10 +53,6 @@ StatusNotifierItem::StatusNotifierItem(const QString& address, QObject* parent) return; } - qDBusRegisterMetaType(); - qDBusRegisterMetaType(); - qDBusRegisterMetaType(); - // clang-format off QObject::connect(this->item, &DBusStatusNotifierItem::NewTitle, &this->title, &AbstractDBusProperty::update); QObject::connect(this->item, &DBusStatusNotifierItem::NewIcon, &this->iconName, &AbstractDBusProperty::update); @@ -227,6 +230,15 @@ void StatusNotifierItem::updateIcon() { emit this->iconChanged(); } +DBusMenu* StatusNotifierItem::createMenu() const { + auto path = this->menuPath.get().path(); + if (!path.isEmpty()) { + return new DBusMenu(this->item->service(), this->menuPath.get().path()); + } + + return nullptr; +} + void StatusNotifierItem::onGetAllFinished() { if (this->mReady) return; this->mReady = true; diff --git a/src/services/status_notifier/item.hpp b/src/services/status_notifier/item.hpp index 4e8c308a..9dbf02fd 100644 --- a/src/services/status_notifier/item.hpp +++ b/src/services/status_notifier/item.hpp @@ -10,6 +10,7 @@ #include #include "../../core/imageprovider.hpp" +#include "../../dbus/dbusmenu/dbusmenu.hpp" #include "../../dbus/properties.hpp" #include "dbus_item.h" #include "dbus_item_types.hpp" @@ -40,6 +41,7 @@ public: [[nodiscard]] bool isReady() const; [[nodiscard]] QString iconId() const; [[nodiscard]] QPixmap createPixmap(const QSize& size) const; + [[nodiscard]] qs::dbus::dbusmenu::DBusMenu* createMenu() const; void activate(); void secondaryActivate(); @@ -77,6 +79,7 @@ private: DBusStatusNotifierItem* item = nullptr; TrayImageHandle imageHandle {this}; bool mReady = false; + dbus::dbusmenu::DBusMenu* mMenu = nullptr; // bumped to inhibit caching quint32 iconIndex = 0; diff --git a/src/services/status_notifier/qml.cpp b/src/services/status_notifier/qml.cpp index f661c04e..f3a6c7f9 100644 --- a/src/services/status_notifier/qml.cpp +++ b/src/services/status_notifier/qml.cpp @@ -9,11 +9,13 @@ #include #include +#include "../../dbus/dbusmenu/dbusmenu.hpp" #include "../../dbus/properties.hpp" #include "host.hpp" #include "item.hpp" using namespace qs::dbus; +using namespace qs::dbus::dbusmenu; using namespace qs::service::sni; SystemTrayItem::SystemTrayItem(qs::service::sni::StatusNotifierItem* item, QObject* parent) @@ -27,8 +29,11 @@ SystemTrayItem::SystemTrayItem(qs::service::sni::StatusNotifierItem* item, QObje QObject::connect(this->item, &StatusNotifierItem::iconChanged, this, &SystemTrayItem::iconChanged); QObject::connect(&this->item->tooltip, &AbstractDBusProperty::changed, this, &SystemTrayItem::tooltipTitleChanged); QObject::connect(&this->item->tooltip, &AbstractDBusProperty::changed, this, &SystemTrayItem::tooltipDescriptionChanged); + QObject::connect(&this->item->menuPath, &AbstractDBusProperty::changed, this, &SystemTrayItem::onMenuPathChanged); QObject::connect(&this->item->isMenu, &AbstractDBusProperty::changed, this, &SystemTrayItem::onlyMenuChanged); // clang-format on + + if (!this->item->menuPath.get().path().isEmpty()) this->onMenuPathChanged(); } QString SystemTrayItem::id() const { @@ -84,11 +89,25 @@ QString SystemTrayItem::tooltipDescription() const { return this->item->tooltip.get().description; } +DBusMenuItem* SystemTrayItem::menu() const { + if (this->mMenu == nullptr) return nullptr; + return &this->mMenu->rootItem; +} + bool SystemTrayItem::onlyMenu() const { if (this->item == nullptr) return false; return this->item->isMenu.get(); } +void SystemTrayItem::onMenuPathChanged() { + if (this->mMenu != nullptr) { + this->mMenu->deleteLater(); + } + + this->mMenu = this->item->createMenu(); + emit this->menuChanged(); +} + void SystemTrayItem::activate() { this->item->activate(); } void SystemTrayItem::secondaryActivate() { this->item->secondaryActivate(); } diff --git a/src/services/status_notifier/qml.hpp b/src/services/status_notifier/qml.hpp index ffac6638..26b252a0 100644 --- a/src/services/status_notifier/qml.hpp +++ b/src/services/status_notifier/qml.hpp @@ -58,15 +58,14 @@ class SystemTrayItem: public QObject { Q_PROPERTY(QString icon READ icon NOTIFY iconChanged); Q_PROPERTY(QString tooltipTitle READ tooltipTitle NOTIFY tooltipTitleChanged); Q_PROPERTY(QString tooltipDescription READ tooltipDescription NOTIFY tooltipDescriptionChanged); + Q_PROPERTY(qs::dbus::dbusmenu::DBusMenuItem* menu READ menu NOTIFY menuChanged); // If this tray item only offers a menu and no activation action. Q_PROPERTY(bool onlyMenu READ onlyMenu NOTIFY onlyMenuChanged); QML_ELEMENT; + QML_UNCREATABLE("SystemTrayItems can only be acquired from SystemTray"); public: - explicit SystemTrayItem( - qs::service::sni::StatusNotifierItem* item = nullptr, - QObject* parent = nullptr - ); + explicit SystemTrayItem(qs::service::sni::StatusNotifierItem* item, QObject* parent = nullptr); // Primary activation action, generally triggered via a left click. Q_INVOKABLE void activate(); @@ -84,6 +83,7 @@ public: [[nodiscard]] QString icon() const; [[nodiscard]] QString tooltipTitle() const; [[nodiscard]] QString tooltipDescription() const; + [[nodiscard]] qs::dbus::dbusmenu::DBusMenuItem* menu() const; [[nodiscard]] bool onlyMenu() const; signals: @@ -94,10 +94,15 @@ signals: void iconChanged(); void tooltipTitleChanged(); void tooltipDescriptionChanged(); + void menuChanged(); void onlyMenuChanged(); +private slots: + void onMenuPathChanged(); + private: qs::service::sni::StatusNotifierItem* item = nullptr; + qs::dbus::dbusmenu::DBusMenu* mMenu = nullptr; friend class SystemTray; };