Skip to content

Commit a053373

Browse files
committed
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.
1 parent 3fc1c91 commit a053373

File tree

7 files changed

+61
-67
lines changed

7 files changed

+61
-67
lines changed

src/core/model.cpp

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,22 @@ bool UntypedObjectModel::removeObject(const QObject* object) {
7171
return true;
7272
}
7373

74+
void UntypedObjectModel::diffUpdate(const QVector<QObject*>& newValues) {
75+
for (qsizetype i = 0; i < this->valuesList.length();) {
76+
if (newValues.contains(this->valuesList.at(i))) i++;
77+
else this->removeAt(i);
78+
}
79+
80+
qsizetype oi = 0;
81+
for (auto* object: newValues) {
82+
if (this->valuesList.length() == oi || this->valuesList.at(oi) != object) {
83+
this->insertObject(object, oi);
84+
}
85+
86+
oi++;
87+
}
88+
}
89+
7490
qsizetype UntypedObjectModel::indexOf(QObject* object) { return this->valuesList.indexOf(object); }
7591

7692
UntypedObjectModel* UntypedObjectModel::emptyInstance() {

src/core/model.hpp

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,9 @@ class UntypedObjectModel: public QAbstractListModel {
7373
void insertObject(QObject* object, qsizetype index = -1);
7474
bool removeObject(const QObject* object);
7575

76+
// Assumes only one instance of a specific value
77+
void diffUpdate(const QVector<QObject*>& newValues);
78+
7679
QVector<QObject*> valuesList;
7780

7881
private:
@@ -97,6 +100,11 @@ class ObjectModel: public UntypedObjectModel {
97100

98101
void removeObject(const T* object) { this->UntypedObjectModel::removeObject(object); }
99102

103+
// Assumes only one instance of a specific value
104+
void diffUpdate(const QVector<T*>& newValues) {
105+
this->UntypedObjectModel::diffUpdate(*std::bit_cast<const QVector<QObject*>*>(&newValues));
106+
}
107+
100108
static ObjectModel<T>* emptyInstance() {
101109
return static_cast<ObjectModel<T>*>(UntypedObjectModel::emptyInstance());
102110
}

src/core/platformmenu.cpp

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
#include "../window/proxywindow.hpp"
2121
#include "../window/windowinterface.hpp"
2222
#include "iconprovider.hpp"
23+
#include "model.hpp"
2324
#include "platformmenu_p.hpp"
2425
#include "popupanchor.hpp"
2526
#include "qsmenu.hpp"
@@ -61,6 +62,7 @@ PlatformMenuEntry::PlatformMenuEntry(QsMenuEntry* menu): QObject(menu), menu(men
6162
QObject::connect(menu, &QsMenuEntry::buttonTypeChanged, this, &PlatformMenuEntry::onButtonTypeChanged);
6263
QObject::connect(menu, &QsMenuEntry::checkStateChanged, this, &PlatformMenuEntry::onCheckStateChanged);
6364
QObject::connect(menu, &QsMenuEntry::hasChildrenChanged, this, &PlatformMenuEntry::relayoutParent);
65+
QObject::connect(menu->children(), &UntypedObjectModel::valuesChanged, this, &PlatformMenuEntry::relayout);
6466
// clang-format on
6567
}
6668

@@ -178,10 +180,10 @@ void PlatformMenuEntry::relayout() {
178180
this->qmenu->setIcon(getCurrentEngineImageAsIcon(icon));
179181
}
180182

181-
auto children = this->menu->children();
182-
auto len = children.count(&children);
183+
const auto& children = this->menu->children()->valueList();
184+
auto len = children.count();
183185
for (auto i = 0; i < len; i++) {
184-
auto* child = children.at(&children, i);
186+
auto* child = children.at(i);
185187

186188
auto* instance = new PlatformMenuEntry(child);
187189
QObject::connect(instance, &QObject::destroyed, this, &PlatformMenuEntry::onChildDestroyed);

src/core/qsmenu.cpp

Lines changed: 6 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@
44
#include <qobject.h>
55
#include <qqmllist.h>
66
#include <qtmetamacros.h>
7-
#include <qtypes.h>
87

8+
#include "model.hpp"
99
#include "platformmenu.hpp"
1010

1111
using namespace qs::menu::platform;
@@ -34,15 +34,6 @@ void QsMenuEntry::display(QObject* parentWindow, int relativeX, int relativeY) {
3434
if (!success) delete platform;
3535
}
3636

37-
QQmlListProperty<QsMenuEntry> QsMenuEntry::emptyChildren(QObject* parent) {
38-
return QQmlListProperty<QsMenuEntry>(
39-
parent,
40-
nullptr,
41-
&QsMenuEntry::childCount,
42-
&QsMenuEntry::childAt
43-
);
44-
}
45-
4637
void QsMenuEntry::ref() {
4738
this->refcount++;
4839
if (this->refcount == 1) emit this->opened();
@@ -53,7 +44,9 @@ void QsMenuEntry::unref() {
5344
if (this->refcount == 0) emit this->closed();
5445
}
5546

56-
QQmlListProperty<QsMenuEntry> QsMenuEntry::children() { return QsMenuEntry::emptyChildren(this); }
47+
ObjectModel<QsMenuEntry>* QsMenuEntry::children() {
48+
return ObjectModel<QsMenuEntry>::emptyInstance();
49+
}
5750

5851
QsMenuOpener::~QsMenuOpener() {
5952
if (this->mMenu) {
@@ -83,13 +76,6 @@ void QsMenuOpener::setMenu(QsMenuHandle* menu) {
8376
if (menu != nullptr) {
8477
auto onMenuChanged = [this, menu]() {
8578
if (menu->menu()) {
86-
QObject::connect(
87-
menu->menu(),
88-
&QsMenuEntry::childrenChanged,
89-
this,
90-
&QsMenuOpener::childrenChanged
91-
);
92-
9379
menu->menu()->ref();
9480
}
9581

@@ -113,19 +99,12 @@ void QsMenuOpener::onMenuDestroyed() {
11399
emit this->childrenChanged();
114100
}
115101

116-
QQmlListProperty<QsMenuEntry> QsMenuOpener::children() {
102+
ObjectModel<QsMenuEntry>* QsMenuOpener::children() {
117103
if (this->mMenu && this->mMenu->menu()) {
118104
return this->mMenu->menu()->children();
119105
} else {
120-
return QsMenuEntry::emptyChildren(this);
106+
return ObjectModel<QsMenuEntry>::emptyInstance();
121107
}
122108
}
123109

124-
qsizetype QsMenuEntry::childCount(QQmlListProperty<QsMenuEntry>* /*property*/) { return 0; }
125-
126-
QsMenuEntry*
127-
QsMenuEntry::childAt(QQmlListProperty<QsMenuEntry>* /*property*/, qsizetype /*index*/) {
128-
return nullptr;
129-
}
130-
131110
} // namespace qs::menu

src/core/qsmenu.hpp

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
#include <qtypes.h>
1010

1111
#include "doc.hpp"
12+
#include "model.hpp"
1213

1314
namespace qs::menu {
1415

@@ -107,9 +108,7 @@ class QsMenuEntry: public QsMenuHandle {
107108
void ref();
108109
void unref();
109110

110-
[[nodiscard]] virtual QQmlListProperty<QsMenuEntry> children();
111-
112-
static QQmlListProperty<QsMenuEntry> emptyChildren(QObject* parent);
111+
[[nodiscard]] virtual ObjectModel<QsMenuEntry>* children();
113112

114113
signals:
115114
/// Send a trigger/click signal to the menu entry.
@@ -125,12 +124,8 @@ class QsMenuEntry: public QsMenuHandle {
125124
void buttonTypeChanged();
126125
void checkStateChanged();
127126
void hasChildrenChanged();
128-
QSDOC_HIDE void childrenChanged();
129127

130128
private:
131-
static qsizetype childCount(QQmlListProperty<QsMenuEntry>* property);
132-
static QsMenuEntry* childAt(QQmlListProperty<QsMenuEntry>* property, qsizetype index);
133-
134129
qsizetype refcount = 0;
135130
};
136131

@@ -140,7 +135,8 @@ class QsMenuOpener: public QObject {
140135
/// The menu to retrieve children from.
141136
Q_PROPERTY(qs::menu::QsMenuHandle* menu READ menu WRITE setMenu NOTIFY menuChanged);
142137
/// The children of the given menu.
143-
Q_PROPERTY(QQmlListProperty<qs::menu::QsMenuEntry> children READ children NOTIFY childrenChanged);
138+
QSDOC_TYPE_OVERRIDE(ObjectModel<qs::menu::QsMenuEntry>*);
139+
Q_PROPERTY(UntypedObjectModel* children READ children NOTIFY childrenChanged);
144140
QML_ELEMENT;
145141

146142
public:
@@ -151,7 +147,7 @@ class QsMenuOpener: public QObject {
151147
[[nodiscard]] QsMenuHandle* menu() const;
152148
void setMenu(QsMenuHandle* menu);
153149

154-
[[nodiscard]] QQmlListProperty<QsMenuEntry> children();
150+
[[nodiscard]] ObjectModel<QsMenuEntry>* children();
155151

156152
signals:
157153
void menuChanged();

src/dbus/dbusmenu/dbusmenu.cpp

Lines changed: 18 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
#include <qvariant.h>
2222

2323
#include "../../core/iconimageprovider.hpp"
24+
#include "../../core/model.hpp"
2425
#include "../../core/qsmenu.hpp"
2526
#include "../../dbus/properties.hpp"
2627
#include "dbus_menu.h"
@@ -95,22 +96,8 @@ void DBusMenuItem::updateLayout() const {
9596

9697
bool DBusMenuItem::hasChildren() const { return this->displayChildren || this->id == 0; }
9798

98-
QQmlListProperty<QsMenuEntry> DBusMenuItem::children() {
99-
return QQmlListProperty<QsMenuEntry>(
100-
this,
101-
nullptr,
102-
&DBusMenuItem::childrenCount,
103-
&DBusMenuItem::childAt
104-
);
105-
}
106-
107-
qsizetype DBusMenuItem::childrenCount(QQmlListProperty<QsMenuEntry>* property) {
108-
return reinterpret_cast<DBusMenuItem*>(property->object)->enabledChildren.count();
109-
}
110-
111-
QsMenuEntry* DBusMenuItem::childAt(QQmlListProperty<QsMenuEntry>* property, qsizetype index) {
112-
auto* item = reinterpret_cast<DBusMenuItem*>(property->object);
113-
return item->menu->items.value(item->enabledChildren.at(index));
99+
ObjectModel<QsMenuEntry>* DBusMenuItem::children() {
100+
return reinterpret_cast<ObjectModel<QsMenuEntry>*>(&this->enabledChildren);
114101
}
115102

116103
void DBusMenuItem::updateProperties(const QVariantMap& properties, const QStringList& removed) {
@@ -270,14 +257,13 @@ void DBusMenuItem::updateProperties(const QVariantMap& properties, const QString
270257
}
271258

272259
void DBusMenuItem::onChildrenUpdated() {
273-
this->enabledChildren.clear();
274-
260+
QVector<DBusMenuItem*> children;
275261
for (auto child: this->mChildren) {
276262
auto* item = this->menu->items.value(child);
277-
if (item->visible) this->enabledChildren.push_back(child);
263+
if (item->visible) children.append(item);
278264
}
279265

280-
emit this->childrenChanged();
266+
this->enabledChildren.diffUpdate(children);
281267
}
282268

283269
QDebug operator<<(QDebug debug, DBusMenuItem* item) {
@@ -388,7 +374,7 @@ void DBusMenu::updateLayoutRecursive(
388374
[&](const DBusMenuLayout& layout) { return layout.id == *iter; }
389375
);
390376

391-
if (existing == layout.children.end()) {
377+
if (!item->mShowChildren || existing == layout.children.end()) {
392378
qCDebug(logDbusMenu) << "Removing missing layout item" << this->items.value(*iter) << "from"
393379
<< item;
394380
this->removeRecursive(*iter);
@@ -402,15 +388,23 @@ void DBusMenu::updateLayoutRecursive(
402388
for (const auto& child: layout.children) {
403389
if (item->mShowChildren && !item->mChildren.contains(child.id)) {
404390
qCDebug(logDbusMenu) << "Creating new layout item" << child.id << "in" << item;
405-
item->mChildren.push_back(child.id);
391+
// item->mChildren.push_back(child.id);
406392
this->items.insert(child.id, nullptr);
407393
childrenChanged = true;
408394
}
409395

410396
this->updateLayoutRecursive(child, item, depth - 1);
411397
}
412398

413-
if (childrenChanged) item->onChildrenUpdated();
399+
if (childrenChanged) {
400+
// reset to preserve order
401+
item->mChildren.clear();
402+
for (const auto& child: layout.children) {
403+
item->mChildren.push_back(child.id);
404+
}
405+
406+
item->onChildrenUpdated();
407+
}
414408
}
415409

416410
if (item->mShowChildren && !item->childrenLoaded) {
@@ -554,6 +548,7 @@ void DBusMenuHandle::onMenuPathChanged() {
554548
this->mMenu->setParent(this);
555549

556550
QObject::connect(&this->mMenu->rootItem, &DBusMenuItem::layoutUpdated, this, [this]() {
551+
QObject::disconnect(&this->mMenu->rootItem, &DBusMenuItem::layoutUpdated, this, nullptr);
557552
this->loaded = true;
558553
emit this->menuChanged();
559554
});

src/dbus/dbusmenu/dbusmenu.hpp

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
#include "../../core/doc.hpp"
1717
#include "../../core/imageprovider.hpp"
18+
#include "../../core/model.hpp"
1819
#include "../../core/qsmenu.hpp"
1920
#include "../properties.hpp"
2021
#include "dbus_menu_types.hpp"
@@ -65,7 +66,7 @@ class DBusMenuItem: public QsMenuEntry {
6566
[[nodiscard]] bool isShowingChildren() const;
6667
void setShowChildrenRecursive(bool showChildren);
6768

68-
[[nodiscard]] QQmlListProperty<menu::QsMenuEntry> children() override;
69+
[[nodiscard]] ObjectModel<QsMenuEntry>* children() override;
6970

7071
void updateProperties(const QVariantMap& properties, const QStringList& removed = {});
7172
void onChildrenUpdated();
@@ -96,11 +97,8 @@ private slots:
9697
menu::QsMenuButtonType::Enum mButtonType = menu::QsMenuButtonType::None;
9798
Qt::CheckState mCheckState = Qt::Unchecked;
9899
bool displayChildren = false;
99-
QVector<qint32> enabledChildren;
100+
ObjectModel<DBusMenuItem> enabledChildren {this};
100101
DBusMenuItem* parentMenu = nullptr;
101-
102-
static qsizetype childrenCount(QQmlListProperty<menu::QsMenuEntry>* property);
103-
static menu::QsMenuEntry* childAt(QQmlListProperty<menu::QsMenuEntry>* property, qsizetype index);
104102
};
105103

106104
QDebug operator<<(QDebug debug, DBusMenuItem* item);

0 commit comments

Comments
 (0)