Horrific hack to hide menu entries when invisible

Workaround for https://bugreports.qt.io/browse/QTBUG-54767 or
https://bugreports.qt.io/browse/QTBUG-130996.

This is probably the worst code I have written in a while, but basically
we add a value interceptor to filter out any invisible menu entry. This
is pretty dangerous because one false step crashes the whole menu. Menu
entries are actually Cpp owned and need to be manually deleted unless
they are removed via removeItem. Care needs to be taken to not mess up
the contentData list.

I expect this to break soon.
This commit is contained in:
Nicolas Werner 2025-01-06 16:06:19 +01:00
parent 7db1711b09
commit cd21ff6993
No known key found for this signature in database
GPG key ID: C8D75E610773F2D9
6 changed files with 467 additions and 210 deletions

View file

@ -0,0 +1,136 @@
// SPDX-FileCopyrightText: Nheko Contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later
#include "NhekoMenuVisibilityFilter.h"
#include <QQmlListReference>
#include <QQuickItem>
#include "Logging.h"
QQmlListProperty<QQmlComponent>
NhekoMenuVisibilityFilter::items()
{
return QQmlListProperty<QQmlComponent>(this,
this,
&NhekoMenuVisibilityFilter::appendItem,
&NhekoMenuVisibilityFilter::itemCount,
&NhekoMenuVisibilityFilter::getItem,
&NhekoMenuVisibilityFilter::clearItems,
&NhekoMenuVisibilityFilter::replaceItem,
&NhekoMenuVisibilityFilter::removeLast);
}
void
NhekoMenuVisibilityFilter::appendItem(QQmlListProperty<QQmlComponent> *p, QQmlComponent *c)
{
NhekoMenuVisibilityFilter *dc = static_cast<NhekoMenuVisibilityFilter *>(p->object);
dc->items_.append(c);
// dc->updateTarget();
// QQmlProperty prop(c, "visible");
// prop.connectNotifySignal(dc, SLOT(updateTarget()));
}
qsizetype
NhekoMenuVisibilityFilter::itemCount(QQmlListProperty<QQmlComponent> *p)
{
return static_cast<NhekoMenuVisibilityFilter *>(p->object)->items_.count();
}
QQmlComponent *
NhekoMenuVisibilityFilter::getItem(QQmlListProperty<QQmlComponent> *p, qsizetype index)
{
return static_cast<NhekoMenuVisibilityFilter *>(p->object)->items_.at(index);
}
void
NhekoMenuVisibilityFilter::clearItems(QQmlListProperty<QQmlComponent> *p)
{
static_cast<NhekoMenuVisibilityFilter *>(p->object)->items_.clear();
// static_cast<NhekoMenuVisibilityFilter *>(p->object)->updateTarget();
}
void
NhekoMenuVisibilityFilter::replaceItem(QQmlListProperty<QQmlComponent> *p,
qsizetype index,
QQmlComponent *c)
{
static_cast<NhekoMenuVisibilityFilter *>(p->object)->items_.assign(index, c);
// static_cast<NhekoMenuVisibilityFilter *>(p->object)->updateTarget();
}
void
NhekoMenuVisibilityFilter::removeLast(QQmlListProperty<QQmlComponent> *p)
{
static_cast<NhekoMenuVisibilityFilter *>(p->object)->items_.pop_back();
// static_cast<NhekoMenuVisibilityFilter *>(p->object)->updateTarget();
}
void
NhekoMenuVisibilityFilter::setTarget(const QQmlProperty &prop)
{
if (prop.propertyTypeCategory() != QQmlProperty::List) {
nhlog::ui()->warn("Target prop of NhekoMenuVisibilityFilter set to non list property");
return;
}
targetProperty = prop;
// updateTarget();
}
void
NhekoMenuVisibilityFilter::updateTarget()
{
if (!targetProperty.isValid())
return;
auto newItems = qvariant_cast<QQmlListReference>(targetProperty.read());
// newItems.clear(); <- does not remove the visual items
for (qsizetype i = newItems.size(); i > 0; i--) {
// only remove items, not other random stuff in there!
if (auto item = qobject_cast<QQuickItem *>(newItems.at(i - 1))) {
// emit removeItem(item); <- easier to "automagic" this by using invokeMethod
QMetaObject::invokeMethod(
targetProperty.object(), "removeItem", Qt::DirectConnection, item);
}
}
for (const auto &item : std::as_const(items_)) {
auto newItem = item->create(QQmlEngine::contextForObject(this));
if (auto prop = newItem->property("visible"); !prop.isValid() || prop.toBool()) {
// targetProperty.write(QVariant::fromValue(newItem)); <- appends but breaks removal
newItems.append(newItem);
// You might think this should be JS Ownership, but no! The menu deletes stuff
// explicitly!
// Inb4 this causes a leak...
//
// QQmlEngine::setObjectOwnership(newItem, QQmlEngine::JavaScriptOwnership);
// emit addItem(newItem); <- would work, but manual code, ew
} else {
newItem->deleteLater();
}
}
if (delegate) {
for (const auto &modelData : std::as_const(model)) {
QVariantMap initial;
initial["modelData"] = modelData;
auto newItem =
delegate->createWithInitialProperties(initial, QQmlEngine::contextForObject(this));
// targetProperty.write(QVariant::fromValue(newItem)); <- appends but breaks
// removal
newItems.append(newItem);
// You might think this should be JS Ownership, but no! The menu deletes stuff
// explicitly!
// Inb4 this causes a leak...
//
// QQmlEngine::setObjectOwnership(newItem, QQmlEngine::JavaScriptOwnership);
// emit addItem(newItem); <- would work, but manual code, ew
}
}
// targetProperty.write(QVariant::fromValue(std::move(newItems)));
}
#include "moc_NhekoMenuVisibilityFilter.cpp"

View file

@ -0,0 +1,53 @@
// SPDX-FileCopyrightText: Nheko Contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <QQmlComponent>
#include <QQmlEngine>
#include <QQmlListProperty>
#include <QQmlProperty>
#include <QQmlPropertyValueSource>
#include <QVariantList>
class NhekoMenuVisibilityFilter
: public QObject
, public QQmlPropertyValueSource
{
Q_OBJECT
Q_INTERFACES(QQmlPropertyValueSource)
Q_CLASSINFO("DefaultProperty", "items")
Q_PROPERTY(QQmlListProperty<QQmlComponent> items READ items CONSTANT FINAL)
Q_PROPERTY(QQmlComponent *delegate MEMBER delegate FINAL)
Q_PROPERTY(QVariantList model MEMBER model FINAL)
QML_ELEMENT
public:
NhekoMenuVisibilityFilter(QObject *parent = nullptr)
: QObject(parent)
{
}
QQmlListProperty<QQmlComponent> items();
void setTarget(const QQmlProperty &prop) override;
private:
QQmlProperty targetProperty;
QList<QQmlComponent *> items_;
QQmlComponent *delegate = nullptr;
QVariantList model;
static void appendItem(QQmlListProperty<QQmlComponent> *, QQmlComponent *);
static qsizetype itemCount(QQmlListProperty<QQmlComponent> *);
static QQmlComponent *getItem(QQmlListProperty<QQmlComponent> *, qsizetype index);
static void clearItems(QQmlListProperty<QQmlComponent> *);
static void replaceItem(QQmlListProperty<QQmlComponent> *, qsizetype index, QQmlComponent *);
static void removeLast(QQmlListProperty<QQmlComponent> *);
public slots:
// call this before showing the menu. We don't want to update elsewhere to prevent jumping menus
// and useless work
void updateTarget();
};