service/tray!: redesign menus / dbusmenu and add native menu support

Reworks dbusmenu menus to be displayable with a system context menu.

Breaks the entire DBusMenu api.
This commit is contained in:
outfoxxed 2024-07-01 20:50:30 -07:00
parent c31bbea837
commit ec362637b8
Signed by untrusted user: outfoxxed
GPG key ID: 4C88A185FB89301E
18 changed files with 898 additions and 191 deletions

View file

@ -21,19 +21,26 @@
#include <qvariant.h>
#include "../../core/iconimageprovider.hpp"
#include "../../core/qsmenu.hpp"
#include "../../dbus/properties.hpp"
#include "dbus_menu.h"
#include "dbus_menu_types.hpp"
Q_LOGGING_CATEGORY(logDbusMenu, "quickshell.dbus.dbusmenu", QtWarningMsg);
using namespace qs::menu;
namespace qs::dbus::dbusmenu {
DBusMenuItem::DBusMenuItem(qint32 id, DBusMenu* menu, DBusMenuItem* parentMenu)
: QObject(menu)
: QsMenuEntry(menu)
, id(id)
, menu(menu)
, parentMenu(parentMenu) {
QObject::connect(this, &QsMenuEntry::opened, this, &DBusMenuItem::sendOpened);
QObject::connect(this, &QsMenuEntry::closed, this, &DBusMenuItem::sendClosed);
QObject::connect(this, &QsMenuEntry::triggered, this, &DBusMenuItem::sendTriggered);
QObject::connect(
&this->menu->iconThemePath,
&AbstractDBusProperty::changed,
@ -42,20 +49,13 @@ DBusMenuItem::DBusMenuItem(qint32 id, DBusMenu* menu, DBusMenuItem* parentMenu)
);
}
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"); }
void DBusMenuItem::sendOpened() const { this->menu->sendEvent(this->id, "opened"); }
void DBusMenuItem::sendClosed() const { this->menu->sendEvent(this->id, "closed"); }
void DBusMenuItem::sendTriggered() const { this->menu->sendEvent(this->id, "clicked"); }
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::text() const { return this->mCleanLabel; }
QString DBusMenuItem::icon() const {
if (!this->iconName.isEmpty()) {
@ -68,23 +68,20 @@ QString DBusMenuItem::icon() const {
} else return nullptr;
}
ToggleButtonType::Enum DBusMenuItem::toggleType() const { return this->mToggleType; };
QsMenuButtonType::Enum DBusMenuItem::buttonType() const { return this->mButtonType; };
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) {
void DBusMenuItem::setShowChildrenRecursive(bool showChildren) {
if (showChildren == this->mShowChildren) return;
this->mShowChildren = showChildren;
this->childrenLoaded = false;
if (showChildren) {
this->menu->prepareToShow(this->id, true);
this->menu->prepareToShow(this->id, -1);
} else {
this->menu->sendEvent(this->id, "closed");
emit this->showingChildrenChanged();
if (!this->mChildren.isEmpty()) {
for (auto child: this->mChildren) {
this->menu->removeRecursive(child);
@ -96,10 +93,15 @@ void DBusMenuItem::setShowChildren(bool showChildren) {
}
}
void DBusMenuItem::updateLayout() const {
if (!this->isShowingChildren()) return;
this->menu->updateLayout(this->id, -1);
}
bool DBusMenuItem::hasChildren() const { return this->displayChildren; }
QQmlListProperty<DBusMenuItem> DBusMenuItem::children() {
return QQmlListProperty<DBusMenuItem>(
QQmlListProperty<QsMenuEntry> DBusMenuItem::children() {
return QQmlListProperty<QsMenuEntry>(
this,
nullptr,
&DBusMenuItem::childrenCount,
@ -107,11 +109,11 @@ QQmlListProperty<DBusMenuItem> DBusMenuItem::children() {
);
}
qsizetype DBusMenuItem::childrenCount(QQmlListProperty<DBusMenuItem>* property) {
qsizetype DBusMenuItem::childrenCount(QQmlListProperty<QsMenuEntry>* property) {
return reinterpret_cast<DBusMenuItem*>(property->object)->enabledChildren.count(); // NOLINT
}
DBusMenuItem* DBusMenuItem::childAt(QQmlListProperty<DBusMenuItem>* property, qsizetype index) {
QsMenuEntry* DBusMenuItem::childAt(QQmlListProperty<QsMenuEntry>* property, qsizetype index) {
auto* item = reinterpret_cast<DBusMenuItem*>(property->object); // NOLINT
return item->menu->items.value(item->enabledChildren.at(index));
}
@ -124,30 +126,30 @@ void DBusMenuItem::updateProperties(const QVariantMap& properties, const QString
return;
}
auto originalLabel = this->mLabel;
auto originalText = this->mText;
//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 originalButtonType = this->mButtonType;
auto originalToggleState = this->mCheckState;
auto originalDisplayChildren = this->displayChildren;
auto label = properties.value("label");
if (label.canConvert<QString>()) {
auto text = label.value<QString>();
this->mLabel = text;
this->mText = text;
this->mCleanLabel = text;
//this->mnemonic = QChar();
for (auto i = 0; i < this->mLabel.length() - 1;) {
if (this->mLabel.at(i) == '_') {
for (auto i = 0; i < this->mText.length() - 1;) {
if (this->mText.at(i) == '_') {
//if (this->mnemonic == QChar()) this->mnemonic = this->mLabel.at(i + 1);
this->mLabel.remove(i, 1);
this->mLabel.insert(i + 1, "</u>");
this->mLabel.insert(i, "<u>");
this->mText.remove(i, 1);
this->mText.insert(i + 1, "</u>");
this->mText.insert(i, "<u>");
i += 8;
} else {
i++;
@ -160,7 +162,7 @@ void DBusMenuItem::updateProperties(const QVariantMap& properties, const QString
}
}
} else if (removed.isEmpty() || removed.contains("label")) {
this->mLabel = "";
this->mText = "";
//this->mnemonic = QChar();
}
@ -208,15 +210,15 @@ void DBusMenuItem::updateProperties(const QVariantMap& properties, const QString
if (toggleType.canConvert<QString>()) {
auto toggleTypeStr = toggleType.value<QString>();
if (toggleTypeStr == "") this->mToggleType = ToggleButtonType::None;
else if (toggleTypeStr == "checkmark") this->mToggleType = ToggleButtonType::CheckBox;
else if (toggleTypeStr == "radio") this->mToggleType = ToggleButtonType::RadioButton;
if (toggleTypeStr == "") this->mButtonType = QsMenuButtonType::None;
else if (toggleTypeStr == "checkmark") this->mButtonType = QsMenuButtonType::CheckBox;
else if (toggleTypeStr == "radio") this->mButtonType = QsMenuButtonType::RadioButton;
else {
qCWarning(logDbusMenu) << "Unrecognized toggle type" << toggleTypeStr << "for" << this;
this->mToggleType = ToggleButtonType::None;
this->mButtonType = QsMenuButtonType::None;
}
} else if (removed.isEmpty() || removed.contains("toggle-type")) {
this->mToggleType = ToggleButtonType::None;
this->mButtonType = QsMenuButtonType::None;
}
auto toggleState = properties.value("toggle-state");
@ -227,7 +229,7 @@ void DBusMenuItem::updateProperties(const QVariantMap& properties, const QString
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;
this->mCheckState = Qt::Unchecked;
}
auto childrenDisplay = properties.value("children-display");
@ -245,14 +247,14 @@ void DBusMenuItem::updateProperties(const QVariantMap& properties, const QString
this->displayChildren = false;
}
if (this->mLabel != originalLabel) emit this->labelChanged();
if (this->mText != originalText) emit this->textChanged();
//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->mButtonType != originalButtonType) emit this->buttonTypeChanged();
if (this->mCheckState != originalToggleState) emit this->checkStateChanged();
if (this->mSeparator != originalIsSeparator) emit this->separatorChanged();
if (this->mSeparator != originalIsSeparator) emit this->isSeparatorChanged();
if (this->displayChildren != originalDisplayChildren) emit this->hasChildrenChanged();
if (this->iconName != originalIconName || this->image != originalImage) {
@ -263,11 +265,11 @@ void DBusMenuItem::updateProperties(const QVariantMap& properties, const QString
emit this->iconChanged();
}
qCDebug(logDbusMenu).nospace() << "Updated properties of " << this << " { label=" << this->mLabel
qCDebug(logDbusMenu).nospace() << "Updated properties of " << this << " { label=" << this->mText
<< ", enabled=" << this->mEnabled << ", visible=" << this->visible
<< ", iconName=" << this->iconName << ", iconData=" << this->image
<< ", separator=" << this->mSeparator
<< ", toggleType=" << this->mToggleType
<< ", toggleType=" << this->mButtonType
<< ", toggleState=" << this->mCheckState
<< ", displayChildren=" << this->displayChildren << " }";
}
@ -291,20 +293,7 @@ QDebug operator<<(QDebug debug, DBusMenuItem* item) {
auto saver = QDebugStateSaver(debug);
debug.nospace() << "DBusMenuItem(" << static_cast<void*>(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;
}
<< ", label=" << item->mText << ", menu=" << item->menu << ")";
return debug;
}
@ -334,19 +323,18 @@ DBusMenu::DBusMenu(const QString& service, const QString& path, QObject* parent)
this->properties.updateAllViaGetAll();
}
void DBusMenu::prepareToShow(qint32 item, bool sendOpened) {
void DBusMenu::prepareToShow(qint32 item, qint32 depth) {
auto pending = this->interface->AboutToShow(item);
auto* call = new QDBusPendingCallWatcher(pending, this);
auto responseCallback = [this, item, sendOpened](QDBusPendingCallWatcher* call) {
auto responseCallback = [this, item, depth](QDBusPendingCallWatcher* call) {
const QDBusPendingReply<bool> 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");
this->updateLayout(item, depth);
delete call;
};
@ -385,6 +373,7 @@ void DBusMenu::updateLayoutRecursive(
// 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);
item->mShowChildren = parent != nullptr && parent->mShowChildren;
this->items.insert(layout.id, item);
}
}
@ -431,8 +420,9 @@ void DBusMenu::updateLayoutRecursive(
if (item->mShowChildren && !item->childrenLoaded) {
item->childrenLoaded = true;
emit item->showingChildrenChanged();
}
emit item->layoutUpdated();
}
void DBusMenu::removeRecursive(qint32 id) {

View file

@ -14,32 +14,18 @@
#include <qtypes.h>
#include "../../core/imageprovider.hpp"
#include "../../core/qsmenu.hpp"
#include "../properties.hpp"
#include "dbus_menu_types.hpp"
Q_DECLARE_LOGGING_CATEGORY(logDbusMenu);
namespace ToggleButtonType { // NOLINT
Q_NAMESPACE;
QML_ELEMENT;
enum Enum {
/// This menu item does not have a checkbox or a radiobutton associated with it.
None = 0,
/// This menu item should draw a checkbox.
CheckBox = 1,
/// This menu item should draw a radiobutton.
RadioButton = 2,
};
Q_ENUM_NS(Enum);
} // namespace ToggleButtonType
class DBusMenuInterface;
namespace qs::dbus::dbusmenu {
QDebug operator<<(QDebug debug, const ToggleButtonType::Enum& toggleType);
// hack because docgen can't take namespaces in superclasses
using menu::QsMenuEntry;
class DBusMenu;
class DBusMenuPngImage;
@ -47,113 +33,56 @@ class DBusMenuPngImage;
///! Menu item shared by an external program.
/// Menu item shared by an external program via the
/// [DBusMenu specification](https://github.com/AyatanaIndicators/libdbusmenu/blob/master/libdbusmenu-glib/dbus-menu.xml).
class DBusMenuItem: public QObject {
class DBusMenuItem: public QsMenuEntry {
Q_OBJECT;
// clang-format off
/// Handle to the root of this menu.
Q_PROPERTY(DBusMenu* menuHandle READ menuHandle CONSTANT);
/// Text of the menu item, including hotkey markup.
Q_PROPERTY(QString label READ label NOTIFY labelChanged);
/// Text of the menu item without hotkey markup.
Q_PROPERTY(QString cleanLabel READ cleanLabel NOTIFY labelChanged);
/// If the menu item should be shown as enabled.
///
/// > [!INFO] Disabled menu items are often used as headers in addition
/// > to actual disabled entries.
Q_PROPERTY(bool enabled READ enabled NOTIFY enabledChanged);
/// Url of the menu item's icon or `""` if it doesn't have one.
///
/// This can be passed to [Image.source](https://doc.qt.io/qt-6/qml-qtquick-image.html#source-prop)
/// as shown below.
///
/// ```qml
/// Image {
/// source: menuItem.icon
/// // To get the best image quality, set the image source size to the same size
/// // as the rendered image.
/// sourceSize.width: width
/// sourceSize.height: height
/// }
/// ```
Q_PROPERTY(QString icon READ icon NOTIFY iconChanged);
/// If this menu item has an associated checkbox or radiobutton.
///
/// > [!INFO] It is the responsibility of the remote application to update the state of
/// > checkboxes and radiobuttons via [checkState](#prop.checkState).
Q_PROPERTY(ToggleButtonType::Enum toggleType READ toggleType NOTIFY toggleTypeChanged);
/// The check state of the checkbox or radiobutton if applicable, as a
/// [Qt.CheckState](https://doc.qt.io/qt-6/qt.html#CheckState-enum).
Q_PROPERTY(Qt::CheckState checkState READ checkState NOTIFY checkStateChanged);
/// If this menu item should be rendered as a separator between other items.
///
/// No other properties have a meaningful value when `isSeparator` is true.
Q_PROPERTY(bool isSeparator READ isSeparator NOTIFY separatorChanged);
/// If this menu item reveals a submenu containing more items.
///
/// Any submenu items must be requested by setting [showChildren](#prop.showChildren).
Q_PROPERTY(bool hasChildren READ hasChildren NOTIFY hasChildrenChanged);
/// If submenu entries of this item should be shown.
///
/// When true, children of this menu item will be exposed via [children](#prop.children).
/// Setting this property will additionally send the `opened` and `closed` events to the
/// process that provided the menu.
Q_PROPERTY(bool showChildren READ isShowingChildren WRITE setShowChildren NOTIFY showingChildrenChanged);
/// Children of this menu item. Only populated when [showChildren](#prop.showChildren) is true.
///
/// > [!INFO] Use [hasChildren](#prop.hasChildren) to check if this item should reveal a submenu
/// > instead of checking if `children` is empty.
Q_PROPERTY(QQmlListProperty<DBusMenuItem> children READ children NOTIFY childrenChanged);
// clang-format on
QML_ELEMENT;
QML_UNCREATABLE("DBusMenus can only be acquired from a DBusMenuHandle");
public:
explicit DBusMenuItem(qint32 id, DBusMenu* menu, DBusMenuItem* parentMenu);
/// Send a `clicked` event to the remote application for this menu item.
Q_INVOKABLE void click();
/// Send a `hovered` event to the remote application for this menu item.
/// Refreshes the menu contents.
///
/// Note: we are not aware of any programs that use this in any meaningful way.
Q_INVOKABLE void hover() const;
/// Usually you shouldn't need to call this manually but some applications providing
/// menus do not update them correctly. Call this if menus don't update their state.
///
/// The `layoutUpdated` signal will be sent when a response is received.
Q_INVOKABLE void updateLayout() 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 isSeparator() const override;
[[nodiscard]] bool enabled() const override;
[[nodiscard]] QString text() const override;
[[nodiscard]] QString icon() const override;
[[nodiscard]] menu::QsMenuButtonType::Enum buttonType() const override;
[[nodiscard]] Qt::CheckState checkState() const override;
[[nodiscard]] bool hasChildren() const override;
[[nodiscard]] bool isShowingChildren() const;
void setShowChildren(bool showChildren);
void setShowChildrenRecursive(bool showChildren);
[[nodiscard]] QQmlListProperty<DBusMenuItem> children();
[[nodiscard]] QQmlListProperty<menu::QsMenuEntry> children() override;
void updateProperties(const QVariantMap& properties, const QStringList& removed = {});
void onChildrenUpdated();
qint32 id = 0;
QString mLabel;
QString mText;
QVector<qint32> 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();
void layoutUpdated();
private slots:
void sendOpened() const;
void sendClosed() const;
void sendTriggered() const;
private:
QString mCleanLabel;
@ -163,14 +92,14 @@ private:
bool mSeparator = false;
QString iconName;
DBusMenuPngImage* image = nullptr;
ToggleButtonType::Enum mToggleType = ToggleButtonType::None;
Qt::CheckState mCheckState = Qt::Checked;
menu::QsMenuButtonType::Enum mButtonType = menu::QsMenuButtonType::None;
Qt::CheckState mCheckState = Qt::Unchecked;
bool displayChildren = false;
QVector<qint32> enabledChildren;
DBusMenuItem* parentMenu = nullptr;
static qsizetype childrenCount(QQmlListProperty<DBusMenuItem>* property);
static DBusMenuItem* childAt(QQmlListProperty<DBusMenuItem>* property, qsizetype index);
static qsizetype childrenCount(QQmlListProperty<menu::QsMenuEntry>* property);
static menu::QsMenuEntry* childAt(QQmlListProperty<menu::QsMenuEntry>* property, qsizetype index);
};
QDebug operator<<(QDebug debug, DBusMenuItem* item);
@ -192,7 +121,7 @@ public:
dbus::DBusProperty<QString> status {this->properties, "Status"};
dbus::DBusProperty<QStringList> iconThemePath {this->properties, "IconThemePath", {}, false};
void prepareToShow(qint32 item, bool sendOpened);
void prepareToShow(qint32 item, qint32 depth);
void updateLayout(qint32 parent, qint32 depth);
void removeRecursive(qint32 id);
void sendEvent(qint32 item, const QString& event);