core/qsmenu!: improve menu layout change UX

Exposes QsMenuOpener.children as an ObjectModel instead of a list to
allow smoother layout change handling in custom menu renderers.

Fixes QsMenuAnchor/platform menus closing whenever menu content changes.
This commit is contained in:
outfoxxed 2024-12-13 01:30:11 -08:00
parent 3fc1c914c7
commit a053373d57
Signed by: outfoxxed
GPG key ID: 4C88A185FB89301E
7 changed files with 61 additions and 67 deletions

View file

@ -71,6 +71,22 @@ bool UntypedObjectModel::removeObject(const QObject* object) {
return true; return true;
} }
void UntypedObjectModel::diffUpdate(const QVector<QObject*>& newValues) {
for (qsizetype i = 0; i < this->valuesList.length();) {
if (newValues.contains(this->valuesList.at(i))) i++;
else this->removeAt(i);
}
qsizetype oi = 0;
for (auto* object: newValues) {
if (this->valuesList.length() == oi || this->valuesList.at(oi) != object) {
this->insertObject(object, oi);
}
oi++;
}
}
qsizetype UntypedObjectModel::indexOf(QObject* object) { return this->valuesList.indexOf(object); } qsizetype UntypedObjectModel::indexOf(QObject* object) { return this->valuesList.indexOf(object); }
UntypedObjectModel* UntypedObjectModel::emptyInstance() { UntypedObjectModel* UntypedObjectModel::emptyInstance() {

View file

@ -73,6 +73,9 @@ protected:
void insertObject(QObject* object, qsizetype index = -1); void insertObject(QObject* object, qsizetype index = -1);
bool removeObject(const QObject* object); bool removeObject(const QObject* object);
// Assumes only one instance of a specific value
void diffUpdate(const QVector<QObject*>& newValues);
QVector<QObject*> valuesList; QVector<QObject*> valuesList;
private: private:
@ -97,6 +100,11 @@ public:
void removeObject(const T* object) { this->UntypedObjectModel::removeObject(object); } void removeObject(const T* object) { this->UntypedObjectModel::removeObject(object); }
// Assumes only one instance of a specific value
void diffUpdate(const QVector<T*>& newValues) {
this->UntypedObjectModel::diffUpdate(*std::bit_cast<const QVector<QObject*>*>(&newValues));
}
static ObjectModel<T>* emptyInstance() { static ObjectModel<T>* emptyInstance() {
return static_cast<ObjectModel<T>*>(UntypedObjectModel::emptyInstance()); return static_cast<ObjectModel<T>*>(UntypedObjectModel::emptyInstance());
} }

View file

@ -20,6 +20,7 @@
#include "../window/proxywindow.hpp" #include "../window/proxywindow.hpp"
#include "../window/windowinterface.hpp" #include "../window/windowinterface.hpp"
#include "iconprovider.hpp" #include "iconprovider.hpp"
#include "model.hpp"
#include "platformmenu_p.hpp" #include "platformmenu_p.hpp"
#include "popupanchor.hpp" #include "popupanchor.hpp"
#include "qsmenu.hpp" #include "qsmenu.hpp"
@ -61,6 +62,7 @@ PlatformMenuEntry::PlatformMenuEntry(QsMenuEntry* menu): QObject(menu), menu(men
QObject::connect(menu, &QsMenuEntry::buttonTypeChanged, this, &PlatformMenuEntry::onButtonTypeChanged); QObject::connect(menu, &QsMenuEntry::buttonTypeChanged, this, &PlatformMenuEntry::onButtonTypeChanged);
QObject::connect(menu, &QsMenuEntry::checkStateChanged, this, &PlatformMenuEntry::onCheckStateChanged); QObject::connect(menu, &QsMenuEntry::checkStateChanged, this, &PlatformMenuEntry::onCheckStateChanged);
QObject::connect(menu, &QsMenuEntry::hasChildrenChanged, this, &PlatformMenuEntry::relayoutParent); QObject::connect(menu, &QsMenuEntry::hasChildrenChanged, this, &PlatformMenuEntry::relayoutParent);
QObject::connect(menu->children(), &UntypedObjectModel::valuesChanged, this, &PlatformMenuEntry::relayout);
// clang-format on // clang-format on
} }
@ -178,10 +180,10 @@ void PlatformMenuEntry::relayout() {
this->qmenu->setIcon(getCurrentEngineImageAsIcon(icon)); this->qmenu->setIcon(getCurrentEngineImageAsIcon(icon));
} }
auto children = this->menu->children(); const auto& children = this->menu->children()->valueList();
auto len = children.count(&children); auto len = children.count();
for (auto i = 0; i < len; i++) { for (auto i = 0; i < len; i++) {
auto* child = children.at(&children, i); auto* child = children.at(i);
auto* instance = new PlatformMenuEntry(child); auto* instance = new PlatformMenuEntry(child);
QObject::connect(instance, &QObject::destroyed, this, &PlatformMenuEntry::onChildDestroyed); QObject::connect(instance, &QObject::destroyed, this, &PlatformMenuEntry::onChildDestroyed);

View file

@ -4,8 +4,8 @@
#include <qobject.h> #include <qobject.h>
#include <qqmllist.h> #include <qqmllist.h>
#include <qtmetamacros.h> #include <qtmetamacros.h>
#include <qtypes.h>
#include "model.hpp"
#include "platformmenu.hpp" #include "platformmenu.hpp"
using namespace qs::menu::platform; using namespace qs::menu::platform;
@ -34,15 +34,6 @@ void QsMenuEntry::display(QObject* parentWindow, int relativeX, int relativeY) {
if (!success) delete platform; if (!success) delete platform;
} }
QQmlListProperty<QsMenuEntry> QsMenuEntry::emptyChildren(QObject* parent) {
return QQmlListProperty<QsMenuEntry>(
parent,
nullptr,
&QsMenuEntry::childCount,
&QsMenuEntry::childAt
);
}
void QsMenuEntry::ref() { void QsMenuEntry::ref() {
this->refcount++; this->refcount++;
if (this->refcount == 1) emit this->opened(); if (this->refcount == 1) emit this->opened();
@ -53,7 +44,9 @@ void QsMenuEntry::unref() {
if (this->refcount == 0) emit this->closed(); if (this->refcount == 0) emit this->closed();
} }
QQmlListProperty<QsMenuEntry> QsMenuEntry::children() { return QsMenuEntry::emptyChildren(this); } ObjectModel<QsMenuEntry>* QsMenuEntry::children() {
return ObjectModel<QsMenuEntry>::emptyInstance();
}
QsMenuOpener::~QsMenuOpener() { QsMenuOpener::~QsMenuOpener() {
if (this->mMenu) { if (this->mMenu) {
@ -83,13 +76,6 @@ void QsMenuOpener::setMenu(QsMenuHandle* menu) {
if (menu != nullptr) { if (menu != nullptr) {
auto onMenuChanged = [this, menu]() { auto onMenuChanged = [this, menu]() {
if (menu->menu()) { if (menu->menu()) {
QObject::connect(
menu->menu(),
&QsMenuEntry::childrenChanged,
this,
&QsMenuOpener::childrenChanged
);
menu->menu()->ref(); menu->menu()->ref();
} }
@ -113,19 +99,12 @@ void QsMenuOpener::onMenuDestroyed() {
emit this->childrenChanged(); emit this->childrenChanged();
} }
QQmlListProperty<QsMenuEntry> QsMenuOpener::children() { ObjectModel<QsMenuEntry>* QsMenuOpener::children() {
if (this->mMenu && this->mMenu->menu()) { if (this->mMenu && this->mMenu->menu()) {
return this->mMenu->menu()->children(); return this->mMenu->menu()->children();
} else { } else {
return QsMenuEntry::emptyChildren(this); return ObjectModel<QsMenuEntry>::emptyInstance();
} }
} }
qsizetype QsMenuEntry::childCount(QQmlListProperty<QsMenuEntry>* /*property*/) { return 0; }
QsMenuEntry*
QsMenuEntry::childAt(QQmlListProperty<QsMenuEntry>* /*property*/, qsizetype /*index*/) {
return nullptr;
}
} // namespace qs::menu } // namespace qs::menu

View file

@ -9,6 +9,7 @@
#include <qtypes.h> #include <qtypes.h>
#include "doc.hpp" #include "doc.hpp"
#include "model.hpp"
namespace qs::menu { namespace qs::menu {
@ -107,9 +108,7 @@ public:
void ref(); void ref();
void unref(); void unref();
[[nodiscard]] virtual QQmlListProperty<QsMenuEntry> children(); [[nodiscard]] virtual ObjectModel<QsMenuEntry>* children();
static QQmlListProperty<QsMenuEntry> emptyChildren(QObject* parent);
signals: signals:
/// Send a trigger/click signal to the menu entry. /// Send a trigger/click signal to the menu entry.
@ -125,12 +124,8 @@ signals:
void buttonTypeChanged(); void buttonTypeChanged();
void checkStateChanged(); void checkStateChanged();
void hasChildrenChanged(); void hasChildrenChanged();
QSDOC_HIDE void childrenChanged();
private: private:
static qsizetype childCount(QQmlListProperty<QsMenuEntry>* property);
static QsMenuEntry* childAt(QQmlListProperty<QsMenuEntry>* property, qsizetype index);
qsizetype refcount = 0; qsizetype refcount = 0;
}; };
@ -140,7 +135,8 @@ class QsMenuOpener: public QObject {
/// The menu to retrieve children from. /// The menu to retrieve children from.
Q_PROPERTY(qs::menu::QsMenuHandle* menu READ menu WRITE setMenu NOTIFY menuChanged); Q_PROPERTY(qs::menu::QsMenuHandle* menu READ menu WRITE setMenu NOTIFY menuChanged);
/// The children of the given menu. /// The children of the given menu.
Q_PROPERTY(QQmlListProperty<qs::menu::QsMenuEntry> children READ children NOTIFY childrenChanged); QSDOC_TYPE_OVERRIDE(ObjectModel<qs::menu::QsMenuEntry>*);
Q_PROPERTY(UntypedObjectModel* children READ children NOTIFY childrenChanged);
QML_ELEMENT; QML_ELEMENT;
public: public:
@ -151,7 +147,7 @@ public:
[[nodiscard]] QsMenuHandle* menu() const; [[nodiscard]] QsMenuHandle* menu() const;
void setMenu(QsMenuHandle* menu); void setMenu(QsMenuHandle* menu);
[[nodiscard]] QQmlListProperty<QsMenuEntry> children(); [[nodiscard]] ObjectModel<QsMenuEntry>* children();
signals: signals:
void menuChanged(); void menuChanged();

View file

@ -21,6 +21,7 @@
#include <qvariant.h> #include <qvariant.h>
#include "../../core/iconimageprovider.hpp" #include "../../core/iconimageprovider.hpp"
#include "../../core/model.hpp"
#include "../../core/qsmenu.hpp" #include "../../core/qsmenu.hpp"
#include "../../dbus/properties.hpp" #include "../../dbus/properties.hpp"
#include "dbus_menu.h" #include "dbus_menu.h"
@ -95,22 +96,8 @@ void DBusMenuItem::updateLayout() const {
bool DBusMenuItem::hasChildren() const { return this->displayChildren || this->id == 0; } bool DBusMenuItem::hasChildren() const { return this->displayChildren || this->id == 0; }
QQmlListProperty<QsMenuEntry> DBusMenuItem::children() { ObjectModel<QsMenuEntry>* DBusMenuItem::children() {
return QQmlListProperty<QsMenuEntry>( return reinterpret_cast<ObjectModel<QsMenuEntry>*>(&this->enabledChildren);
this,
nullptr,
&DBusMenuItem::childrenCount,
&DBusMenuItem::childAt
);
}
qsizetype DBusMenuItem::childrenCount(QQmlListProperty<QsMenuEntry>* property) {
return reinterpret_cast<DBusMenuItem*>(property->object)->enabledChildren.count();
}
QsMenuEntry* DBusMenuItem::childAt(QQmlListProperty<QsMenuEntry>* property, qsizetype index) {
auto* item = reinterpret_cast<DBusMenuItem*>(property->object);
return item->menu->items.value(item->enabledChildren.at(index));
} }
void DBusMenuItem::updateProperties(const QVariantMap& properties, const QStringList& removed) { void DBusMenuItem::updateProperties(const QVariantMap& properties, const QStringList& removed) {
@ -270,14 +257,13 @@ void DBusMenuItem::updateProperties(const QVariantMap& properties, const QString
} }
void DBusMenuItem::onChildrenUpdated() { void DBusMenuItem::onChildrenUpdated() {
this->enabledChildren.clear(); QVector<DBusMenuItem*> children;
for (auto child: this->mChildren) { for (auto child: this->mChildren) {
auto* item = this->menu->items.value(child); auto* item = this->menu->items.value(child);
if (item->visible) this->enabledChildren.push_back(child); if (item->visible) children.append(item);
} }
emit this->childrenChanged(); this->enabledChildren.diffUpdate(children);
} }
QDebug operator<<(QDebug debug, DBusMenuItem* item) { QDebug operator<<(QDebug debug, DBusMenuItem* item) {
@ -388,7 +374,7 @@ void DBusMenu::updateLayoutRecursive(
[&](const DBusMenuLayout& layout) { return layout.id == *iter; } [&](const DBusMenuLayout& layout) { return layout.id == *iter; }
); );
if (existing == layout.children.end()) { if (!item->mShowChildren || existing == layout.children.end()) {
qCDebug(logDbusMenu) << "Removing missing layout item" << this->items.value(*iter) << "from" qCDebug(logDbusMenu) << "Removing missing layout item" << this->items.value(*iter) << "from"
<< item; << item;
this->removeRecursive(*iter); this->removeRecursive(*iter);
@ -402,7 +388,7 @@ void DBusMenu::updateLayoutRecursive(
for (const auto& child: layout.children) { for (const auto& child: layout.children) {
if (item->mShowChildren && !item->mChildren.contains(child.id)) { if (item->mShowChildren && !item->mChildren.contains(child.id)) {
qCDebug(logDbusMenu) << "Creating new layout item" << child.id << "in" << item; qCDebug(logDbusMenu) << "Creating new layout item" << child.id << "in" << item;
item->mChildren.push_back(child.id); // item->mChildren.push_back(child.id);
this->items.insert(child.id, nullptr); this->items.insert(child.id, nullptr);
childrenChanged = true; childrenChanged = true;
} }
@ -410,7 +396,15 @@ void DBusMenu::updateLayoutRecursive(
this->updateLayoutRecursive(child, item, depth - 1); this->updateLayoutRecursive(child, item, depth - 1);
} }
if (childrenChanged) item->onChildrenUpdated(); if (childrenChanged) {
// reset to preserve order
item->mChildren.clear();
for (const auto& child: layout.children) {
item->mChildren.push_back(child.id);
}
item->onChildrenUpdated();
}
} }
if (item->mShowChildren && !item->childrenLoaded) { if (item->mShowChildren && !item->childrenLoaded) {
@ -554,6 +548,7 @@ void DBusMenuHandle::onMenuPathChanged() {
this->mMenu->setParent(this); this->mMenu->setParent(this);
QObject::connect(&this->mMenu->rootItem, &DBusMenuItem::layoutUpdated, this, [this]() { QObject::connect(&this->mMenu->rootItem, &DBusMenuItem::layoutUpdated, this, [this]() {
QObject::disconnect(&this->mMenu->rootItem, &DBusMenuItem::layoutUpdated, this, nullptr);
this->loaded = true; this->loaded = true;
emit this->menuChanged(); emit this->menuChanged();
}); });

View file

@ -15,6 +15,7 @@
#include "../../core/doc.hpp" #include "../../core/doc.hpp"
#include "../../core/imageprovider.hpp" #include "../../core/imageprovider.hpp"
#include "../../core/model.hpp"
#include "../../core/qsmenu.hpp" #include "../../core/qsmenu.hpp"
#include "../properties.hpp" #include "../properties.hpp"
#include "dbus_menu_types.hpp" #include "dbus_menu_types.hpp"
@ -65,7 +66,7 @@ public:
[[nodiscard]] bool isShowingChildren() const; [[nodiscard]] bool isShowingChildren() const;
void setShowChildrenRecursive(bool showChildren); void setShowChildrenRecursive(bool showChildren);
[[nodiscard]] QQmlListProperty<menu::QsMenuEntry> children() override; [[nodiscard]] ObjectModel<QsMenuEntry>* children() override;
void updateProperties(const QVariantMap& properties, const QStringList& removed = {}); void updateProperties(const QVariantMap& properties, const QStringList& removed = {});
void onChildrenUpdated(); void onChildrenUpdated();
@ -96,11 +97,8 @@ private:
menu::QsMenuButtonType::Enum mButtonType = menu::QsMenuButtonType::None; menu::QsMenuButtonType::Enum mButtonType = menu::QsMenuButtonType::None;
Qt::CheckState mCheckState = Qt::Unchecked; Qt::CheckState mCheckState = Qt::Unchecked;
bool displayChildren = false; bool displayChildren = false;
QVector<qint32> enabledChildren; ObjectModel<DBusMenuItem> enabledChildren {this};
DBusMenuItem* parentMenu = nullptr; DBusMenuItem* parentMenu = nullptr;
static qsizetype childrenCount(QQmlListProperty<menu::QsMenuEntry>* property);
static menu::QsMenuEntry* childAt(QQmlListProperty<menu::QsMenuEntry>* property, qsizetype index);
}; };
QDebug operator<<(QDebug debug, DBusMenuItem* item); QDebug operator<<(QDebug debug, DBusMenuItem* item);