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.
pull/1854/head
Nicolas Werner 4 weeks ago
parent 7db1711b09
commit cd21ff6993
No known key found for this signature in database
GPG Key ID: C8D75E610773F2D9
  1. 2
      CMakeLists.txt
  2. 2
      resources/qml/MatrixText.qml
  3. 339
      resources/qml/MessageView.qml
  4. 127
      resources/qml/components/SpaceMenuLevel.qml
  5. 136
      src/ui/NhekoMenuVisibilityFilter.cpp
  6. 53
      src/ui/NhekoMenuVisibilityFilter.h

@ -408,6 +408,8 @@ set(SRC_FILES
src/ui/NhekoDropArea.h src/ui/NhekoDropArea.h
src/ui/NhekoGlobalObject.cpp src/ui/NhekoGlobalObject.cpp
src/ui/NhekoGlobalObject.h src/ui/NhekoGlobalObject.h
src/ui/NhekoMenuVisibilityFilter.h
src/ui/NhekoMenuVisibilityFilter.cpp
src/ui/RoomSettings.cpp src/ui/RoomSettings.cpp
src/ui/RoomSettings.h src/ui/RoomSettings.h
src/ui/RoomSummary.cpp src/ui/RoomSummary.cpp

@ -35,7 +35,7 @@ TextArea {
Component.onCompleted: { Component.onCompleted: {
TimelineManager.fixImageRendering(r.textDocument, r); TimelineManager.fixImageRendering(r.textDocument, r);
} }
onLinkActivated: Nheko.openLink(link) onLinkActivated: (link) => Nheko.openLink(link)
// propagate events up // propagate events up
onPressAndHold: event => event.accepted = false onPressAndHold: event => event.accepted = false

@ -419,6 +419,9 @@ Item {
link = link_; link = link_;
else else
link = ""; link = "";
messageActionsCFilter.updateTarget();
if (showAt_) if (showAt_)
popup(showAt_); popup(showAt_);
else else
@ -453,149 +456,189 @@ Item {
} }
} }
MenuItem { NhekoMenuVisibilityFilter on contentData {
enabled: visible id: messageActionsCFilter
text: qsTr("Go to &message")
visible: filteredTimeline.filterByContent
onTriggered: function () { Component {
topBar.searchString = ""; MenuItem {
room.showEvent(messageContextMenuC.eventId); enabled: visible
} text: qsTr("Go to &message")
} visible: filteredTimeline.filterByContent
MenuItem {
enabled: visible
text: qsTr("&Copy")
visible: messageContextMenuC.text
onTriggered: Clipboard.text = messageContextMenuC.text onTriggered: function () {
} topBar.searchString = "";
MenuItem { room.showEvent(messageContextMenuC.eventId);
enabled: visible }
text: qsTr("Copy &link location") }
visible: messageContextMenuC.link }
Component {
onTriggered: Clipboard.text = messageContextMenuC.link MenuItem {
} enabled: visible
MenuItem { text: qsTr("&Copy")
enabled: visible visible: messageContextMenuC.text
id: reactionOption
text: qsTr("Re&act") onTriggered: Clipboard.text = messageContextMenuC.text
visible: room ? room.permissions.canSend(MtxEvent.Reaction) : false }
}
Component {
MenuItem {
enabled: visible
text: qsTr("Copy &link location")
visible: messageContextMenuC.link
onTriggered: emojiPopup.visible ? emojiPopup.close() : emojiPopup.show(null, room.roomId, function (plaintext, markdown) { onTriggered: Clipboard.text = messageContextMenuC.link
room.input.reaction(messageContextMenuC.eventId, plaintext); }
TimelineManager.focusMessageInput(); }
}) Component {
} MenuItem {
MenuItem { enabled: visible
enabled: visible id: reactionOption
text: qsTr("Repl&y")
visible: room ? room.permissions.canSend(MtxEvent.TextMessage) : false
onTriggered: room.reply = (messageContextMenuC.eventId) text: qsTr("Re&act")
} visible: room ? room.permissions.canSend(MtxEvent.Reaction) : false
MenuItem {
enabled: visible
text: qsTr("&Edit")
visible: messageContextMenuC.isEditable && (room ? room.permissions.canSend(MtxEvent.TextMessage) : false)
onTriggered: room.edit = (messageContextMenuC.eventId) onTriggered: emojiPopup.visible ? emojiPopup.close() : emojiPopup.show(null, room.roomId, function (plaintext, markdown) {
} room.input.reaction(messageContextMenuC.eventId, plaintext);
MenuItem { TimelineManager.focusMessageInput();
enabled: visible })
text: qsTr("&Thread") }
visible: (room ? room.permissions.canSend(MtxEvent.TextMessage) : false) }
Component {
MenuItem {
enabled: visible
text: qsTr("Repl&y")
visible: room ? room.permissions.canSend(MtxEvent.TextMessage) : false
onTriggered: room.thread = (messageContextMenuC.threadId || messageContextMenuC.eventId) onTriggered: room.reply = (messageContextMenuC.eventId)
} }
MenuItem { }
enabled: visible Component {
text: visible && room.pinnedMessages.includes(messageContextMenuC.eventId) ? qsTr("Un&pin") : qsTr("&Pin") MenuItem {
visible: (room ? room.permissions.canChange(MtxEvent.PinnedEvents) : false) enabled: visible
text: qsTr("&Edit")
visible: messageContextMenuC.isEditable && (room ? room.permissions.canSend(MtxEvent.TextMessage) : false)
onTriggered: visible && room.pinnedMessages.includes(messageContextMenuC.eventId) ? room.unpin(messageContextMenuC.eventId) : room.pin(messageContextMenuC.eventId) onTriggered: room.edit = (messageContextMenuC.eventId)
} }
MenuItem { }
enabled: visible Component {
text: qsTr("&Read receipts") MenuItem {
enabled: visible
text: qsTr("&Thread")
visible: (room ? room.permissions.canSend(MtxEvent.TextMessage) : false)
onTriggered: room.showReadReceipts(messageContextMenuC.eventId) onTriggered: room.thread = (messageContextMenuC.threadId || messageContextMenuC.eventId)
} }
MenuItem { }
enabled: visible Component {
text: qsTr("&Forward") MenuItem {
visible: messageContextMenuC.eventType == MtxEvent.ImageMessage || messageContextMenuC.eventType == MtxEvent.VideoMessage || messageContextMenuC.eventType == MtxEvent.AudioMessage || messageContextMenuC.eventType == MtxEvent.FileMessage || messageContextMenuC.eventType == MtxEvent.Sticker || messageContextMenuC.eventType == MtxEvent.TextMessage || messageContextMenuC.eventType == MtxEvent.LocationMessage || messageContextMenuC.eventType == MtxEvent.EmoteMessage || messageContextMenuC.eventType == MtxEvent.NoticeMessage enabled: visible
text: visible && room.pinnedMessages.includes(messageContextMenuC.eventId) ? qsTr("Un&pin") : qsTr("&Pin")
visible: (room ? room.permissions.canChange(MtxEvent.PinnedEvents) : false)
onTriggered: { onTriggered: visible && room.pinnedMessages.includes(messageContextMenuC.eventId) ? room.unpin(messageContextMenuC.eventId) : room.pin(messageContextMenuC.eventId)
var forwardMess = forwardCompleterComponent.createObject(timelineRoot); }
forwardMess.setMessageEventId(messageContextMenuC.eventId);
forwardMess.open();
timelineRoot.destroyOnClose(forwardMess);
} }
} Component {
MenuItem { MenuItem {
enabled: visible enabled: visible
text: qsTr("&Mark as read") text: qsTr("&Read receipts")
}
MenuItem {
enabled: visible
text: qsTr("View raw message")
onTriggered: room.viewRawMessage(messageContextMenuC.eventId) onTriggered: room.showReadReceipts(messageContextMenuC.eventId)
} }
MenuItem { }
enabled: visible Component {
text: qsTr("View decrypted raw message") MenuItem {
// TODO(Nico): Fix this still being iterated over, when using keyboard to select options enabled: visible
visible: messageContextMenuC.isEncrypted text: qsTr("&Forward")
visible: messageContextMenuC.eventType == MtxEvent.ImageMessage || messageContextMenuC.eventType == MtxEvent.VideoMessage || messageContextMenuC.eventType == MtxEvent.AudioMessage || messageContextMenuC.eventType == MtxEvent.FileMessage || messageContextMenuC.eventType == MtxEvent.Sticker || messageContextMenuC.eventType == MtxEvent.TextMessage || messageContextMenuC.eventType == MtxEvent.LocationMessage || messageContextMenuC.eventType == MtxEvent.EmoteMessage || messageContextMenuC.eventType == MtxEvent.NoticeMessage
onTriggered: {
var forwardMess = forwardCompleterComponent.createObject(timelineRoot);
forwardMess.setMessageEventId(messageContextMenuC.eventId);
forwardMess.open();
timelineRoot.destroyOnClose(forwardMess);
}
}
}
Component {
MenuItem {
enabled: visible
text: qsTr("&Mark as read")
}
}
Component {
MenuItem {
enabled: visible
text: qsTr("View raw message")
onTriggered: room.viewDecryptedRawMessage(messageContextMenuC.eventId) onTriggered: room.viewRawMessage(messageContextMenuC.eventId)
} }
MenuItem {
enabled: visible
text: qsTr("Remo&ve message")
visible: (room ? room.permissions.canRedact() : false) || messageContextMenuC.isSender
onTriggered: function () {
var dialog = removeReason.createObject(timelineRoot);
dialog.eventId = messageContextMenuC.eventId;
dialog.show();
dialog.forceActiveFocus();
timelineRoot.destroyOnClose(dialog);
} }
} Component {
MenuItem { MenuItem {
text: qsTr("Report message") enabled: visible
enabled: visible text: qsTr("View decrypted raw message")
onTriggered: function () { // TODO(Nico): Fix this still being iterated over, when using keyboard to select options
var dialog = reportDialog.createObject(timelineRoot, {"eventId": messageContextMenuC.eventId}); visible: messageContextMenuC.isEncrypted
dialog.show();
dialog.forceActiveFocus(); onTriggered: room.viewDecryptedRawMessage(messageContextMenuC.eventId)
timelineRoot.destroyOnClose(dialog); }
} }
} Component {
MenuItem { MenuItem {
enabled: visible enabled: visible
text: qsTr("&Save as") text: qsTr("Remo&ve message")
visible: messageContextMenuC.eventType == MtxEvent.ImageMessage || messageContextMenuC.eventType == MtxEvent.VideoMessage || messageContextMenuC.eventType == MtxEvent.AudioMessage || messageContextMenuC.eventType == MtxEvent.FileMessage || messageContextMenuC.eventType == MtxEvent.Sticker visible: (room ? room.permissions.canRedact() : false) || messageContextMenuC.isSender
onTriggered: function () {
var dialog = removeReason.createObject(timelineRoot);
dialog.eventId = messageContextMenuC.eventId;
dialog.show();
dialog.forceActiveFocus();
timelineRoot.destroyOnClose(dialog);
}
}
}
Component {
MenuItem {
text: qsTr("Report message")
enabled: visible
onTriggered: function () {
var dialog = reportDialog.createObject(timelineRoot, {"eventId": messageContextMenuC.eventId});
dialog.show();
dialog.forceActiveFocus();
timelineRoot.destroyOnClose(dialog);
}
}
}
Component {
MenuItem {
enabled: visible
text: qsTr("&Save as")
visible: messageContextMenuC.eventType == MtxEvent.ImageMessage || messageContextMenuC.eventType == MtxEvent.VideoMessage || messageContextMenuC.eventType == MtxEvent.AudioMessage || messageContextMenuC.eventType == MtxEvent.FileMessage || messageContextMenuC.eventType == MtxEvent.Sticker
onTriggered: room.saveMedia(messageContextMenuC.eventId) onTriggered: room.saveMedia(messageContextMenuC.eventId)
} }
MenuItem { }
enabled: visible Component {
text: qsTr("&Open in external program") MenuItem {
visible: messageContextMenuC.eventType == MtxEvent.ImageMessage || messageContextMenuC.eventType == MtxEvent.VideoMessage || messageContextMenuC.eventType == MtxEvent.AudioMessage || messageContextMenuC.eventType == MtxEvent.FileMessage || messageContextMenuC.eventType == MtxEvent.Sticker enabled: visible
text: qsTr("&Open in external program")
visible: messageContextMenuC.eventType == MtxEvent.ImageMessage || messageContextMenuC.eventType == MtxEvent.VideoMessage || messageContextMenuC.eventType == MtxEvent.AudioMessage || messageContextMenuC.eventType == MtxEvent.FileMessage || messageContextMenuC.eventType == MtxEvent.Sticker
onTriggered: room.openMedia(messageContextMenuC.eventId) onTriggered: room.openMedia(messageContextMenuC.eventId)
} }
MenuItem { }
enabled: visible Component {
text: qsTr("Copy link to eve&nt") MenuItem {
visible: messageContextMenuC.eventId enabled: visible
text: qsTr("Copy link to eve&nt")
visible: messageContextMenuC.eventId
onTriggered: room.copyLinkToEvent(messageContextMenuC.eventId) onTriggered: room.copyLinkToEvent(messageContextMenuC.eventId)
}
}
} }
} }
Component { Component {
@ -615,6 +658,8 @@ Item {
text = text_; text = text_;
link = link_; link = link_;
eventId = eventId_; eventId = eventId_;
replyContextMenuCFilter.updateTarget();
open(); open();
} }
@ -625,26 +670,36 @@ Item {
} }
MenuItem { NhekoMenuVisibilityFilter on contentData {
enabled: visible id: replyContextMenuCFilter
text: qsTr("&Copy")
visible: replyContextMenuC.text
onTriggered: Clipboard.text = replyContextMenuC.text Component {
} MenuItem {
MenuItem { enabled: visible
enabled: visible text: qsTr("&Copy")
text: qsTr("Copy &link location") visible: replyContextMenuC.text
visible: replyContextMenuC.link
onTriggered: Clipboard.text = replyContextMenuC.link onTriggered: Clipboard.text = replyContextMenuC.text
} }
MenuItem { }
enabled: visible Component {
text: qsTr("&Go to quoted message") MenuItem {
visible: true enabled: visible
text: qsTr("Copy &link location")
visible: replyContextMenuC.link
onTriggered: room.showEvent(replyContextMenuC.eventId) onTriggered: Clipboard.text = replyContextMenuC.link
}
}
Component {
MenuItem {
enabled: visible
text: qsTr("&Go to quoted message")
visible: true
onTriggered: room.showEvent(replyContextMenuC.eventId)
}
}
} }
} }
RoundButton { RoundButton {

@ -12,77 +12,88 @@ Menu {
property string roomid property string roomid
property Component childMenu property Component childMenu
property var modelData: undefined
property int position: modelData == undefined ? -2 : modelData.treeIndex property int position: modelData == undefined ? -2 : modelData.treeIndex
title: modelData != undefined ? modelData.name : qsTr("Add or remove from community") title: modelData != undefined ? modelData.name : qsTr("Add or remove from community")
property bool loadChildren: false property bool loadChildren: false
onAboutToShow: loadChildren = true onAboutToShow: {
//onAboutToHide: loadChildren = false loadChildren = true;
menuFilter.updateTarget();
}
ButtonGroup { ButtonGroup {
id: modificationGroup id: modificationGroup
//visible: position != -1 //visible: position != -1
} }
MenuItem { NhekoMenuVisibilityFilter on contentData {
text: qsTr("Official community for this room") id: menuFilter
ButtonGroup.group: modificationGroup
visible: position != -1
checkable: true
checked: spacesMenu.position >= 0 && (modelData.childValid && modelData.parentValid && modelData.canonical)
enabled: spacesMenu.position >= 0 && (modelData.canEditChild && modelData.canEditParent)
onTriggered: if (checked) Communities.updateSpaceStatus(modelData.roomid, spacesMenu.roomid, true, true, true)
}
MenuItem {
text: qsTr("Affiliated community for this room")
ButtonGroup.group: modificationGroup
visible: position != -1
checkable: true
checked: spacesMenu.position >= 0 && (modelData.childValid && modelData.parentValid && !modelData.canonical)
enabled: spacesMenu.position >= 0 && (modelData.canEditChild && modelData.canEditParent)
onTriggered: if (checked) Communities.updateSpaceStatus(modelData.roomid, spacesMenu.roomid, true, true, false)
}
MenuItem {
text: qsTr("Listed only for community members")
ButtonGroup.group: modificationGroup
visible: position != -1
checkable: true
checked: spacesMenu.position >= 0 && (modelData.childValid && !modelData.parentValid)
enabled: spacesMenu.position >= 0 && ((modelData.canEditChild || modelData.childValid) && (!modelData.parentValid || modelData.canEditParent))
onTriggered: if (checked) Communities.updateSpaceStatus(modelData.roomid, spacesMenu.roomid, false, true, false)
}
MenuItem {
text: qsTr("Listed only for room members")
ButtonGroup.group: modificationGroup
visible: position != -1
checkable: true
checked: spacesMenu.position >= 0 && (!modelData.childValid && modelData.parentValid)
enabled: spacesMenu.position >= 0 && ((modelData.canEditChild) && (modelData.parentValid || modelData.canEditParent))
onTriggered: if (checked) Communities.updateSpaceStatus(modelData.roomid, spacesMenu.roomid, true, false, false)
}
MenuItem {
text: qsTr("Not related")
ButtonGroup.group: modificationGroup
visible: position != -1
checkable: true
checked: spacesMenu.position >= 0 && (!modelData.childValid && !modelData.parentValid)
enabled: spacesMenu.position >= 0 && ((modelData.canEditChild || !modelData.childValid) && (!modelData.parentValid || modelData.canEditParent))
onTriggered: if (checked) Communities.updateSpaceStatus(modelData.roomid, spacesMenu.roomid, false, false, false)
}
MenuSeparator { Component {
//text: qsTr("Subcommunities") MenuItem {
ButtonGroup.group: modificationGroup text: qsTr("Official community for this room")
visible: position != -1 && inst.model != undefined ButtonGroup.group: modificationGroup
} visible: position != -1
checkable: true
checked: spacesMenu.position >= 0 && (modelData.childValid && modelData.parentValid && modelData.canonical)
enabled: spacesMenu.position >= 0 && (modelData.canEditChild && modelData.canEditParent)
onTriggered: if (checked) Communities.updateSpaceStatus(modelData.roomid, spacesMenu.roomid, true, true, true)
}
}
Component {
MenuItem {
text: qsTr("Affiliated community for this room")
ButtonGroup.group: modificationGroup
visible: position != -1
checkable: true
checked: spacesMenu.position >= 0 && (modelData.childValid && modelData.parentValid && !modelData.canonical)
enabled: spacesMenu.position >= 0 && (modelData.canEditChild && modelData.canEditParent)
onTriggered: if (checked) Communities.updateSpaceStatus(modelData.roomid, spacesMenu.roomid, true, true, false)
}
}
Component {
MenuItem {
text: qsTr("Listed only for community members")
ButtonGroup.group: modificationGroup
visible: position != -1
checkable: true
checked: spacesMenu.position >= 0 && (modelData.childValid && !modelData.parentValid)
enabled: spacesMenu.position >= 0 && ((modelData.canEditChild || modelData.childValid) && (!modelData.parentValid || modelData.canEditParent))
onTriggered: if (checked) Communities.updateSpaceStatus(modelData.roomid, spacesMenu.roomid, false, true, false)
}
}
Component {
MenuItem {
text: qsTr("Listed only for room members")
ButtonGroup.group: modificationGroup
visible: position != -1
checkable: true
checked: spacesMenu.position >= 0 && (!modelData.childValid && modelData.parentValid)
enabled: spacesMenu.position >= 0 && ((modelData.canEditChild) && (modelData.parentValid || modelData.canEditParent))
onTriggered: if (checked) Communities.updateSpaceStatus(modelData.roomid, spacesMenu.roomid, true, false, false)
}
}
Component {
MenuItem {
text: qsTr("Not related")
ButtonGroup.group: modificationGroup
visible: position != -1
checkable: true
checked: spacesMenu.position >= 0 && (!modelData.childValid && !modelData.parentValid)
enabled: spacesMenu.position >= 0 && ((modelData.canEditChild || !modelData.childValid) && (!modelData.parentValid || modelData.canEditParent))
onTriggered: if (checked) Communities.updateSpaceStatus(modelData.roomid, spacesMenu.roomid, false, false, false)
}
Instantiator {
id: inst
model: spacesMenu.loadChildren ? Communities.spaceChildrenListFromIndex(spacesMenu.roomid, spacesMenu.position) : undefined
onObjectAdded: (idx, o) => {
spacesMenu.insertMenu(idx + (spacesMenu.position != -1 ? 6 : 0), o)
} }
onObjectRemoved: (index, object) => spacesMenu.removeMenu(object) Component {
MenuSeparator {
//text: qsTr("Subcommunities")
visible: position != -1// && menuFilter.model != undefined
}
}
model: spacesMenu.loadChildren ? Communities.spaceChildrenListFromIndex(spacesMenu.roomid, spacesMenu.position) : []
delegate: childMenu delegate: childMenu
} }

@ -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"

@ -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();
};
Loading…
Cancel
Save