From cd21ff699339fb8959cabd4be5c4e8767e1721ae Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Mon, 6 Jan 2025 16:06:19 +0100 Subject: [PATCH] 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. --- CMakeLists.txt | 2 + resources/qml/MatrixText.qml | 2 +- resources/qml/MessageView.qml | 339 ++++++++++++-------- resources/qml/components/SpaceMenuLevel.qml | 127 ++++---- src/ui/NhekoMenuVisibilityFilter.cpp | 136 ++++++++ src/ui/NhekoMenuVisibilityFilter.h | 53 +++ 6 files changed, 458 insertions(+), 201 deletions(-) create mode 100644 src/ui/NhekoMenuVisibilityFilter.cpp create mode 100644 src/ui/NhekoMenuVisibilityFilter.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 1832968e..7988f0cd 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -408,6 +408,8 @@ set(SRC_FILES src/ui/NhekoDropArea.h src/ui/NhekoGlobalObject.cpp src/ui/NhekoGlobalObject.h + src/ui/NhekoMenuVisibilityFilter.h + src/ui/NhekoMenuVisibilityFilter.cpp src/ui/RoomSettings.cpp src/ui/RoomSettings.h src/ui/RoomSummary.cpp diff --git a/resources/qml/MatrixText.qml b/resources/qml/MatrixText.qml index bdc0cb6b..f886016e 100644 --- a/resources/qml/MatrixText.qml +++ b/resources/qml/MatrixText.qml @@ -35,7 +35,7 @@ TextArea { Component.onCompleted: { TimelineManager.fixImageRendering(r.textDocument, r); } - onLinkActivated: Nheko.openLink(link) + onLinkActivated: (link) => Nheko.openLink(link) // propagate events up onPressAndHold: event => event.accepted = false diff --git a/resources/qml/MessageView.qml b/resources/qml/MessageView.qml index b8315bca..d6c0fbcd 100644 --- a/resources/qml/MessageView.qml +++ b/resources/qml/MessageView.qml @@ -419,6 +419,9 @@ Item { link = link_; else link = ""; + + messageActionsCFilter.updateTarget(); + if (showAt_) popup(showAt_); else @@ -453,149 +456,189 @@ Item { } } - MenuItem { - enabled: visible - text: qsTr("Go to &message") - visible: filteredTimeline.filterByContent + NhekoMenuVisibilityFilter on contentData { + id: messageActionsCFilter - onTriggered: function () { - topBar.searchString = ""; - room.showEvent(messageContextMenuC.eventId); - } - } - MenuItem { - enabled: visible - text: qsTr("&Copy") - visible: messageContextMenuC.text + Component { + MenuItem { + enabled: visible + text: qsTr("Go to &message") + visible: filteredTimeline.filterByContent - onTriggered: Clipboard.text = messageContextMenuC.text - } - MenuItem { - enabled: visible - text: qsTr("Copy &link location") - visible: messageContextMenuC.link - - onTriggered: Clipboard.text = messageContextMenuC.link - } - MenuItem { - enabled: visible - id: reactionOption + onTriggered: function () { + topBar.searchString = ""; + room.showEvent(messageContextMenuC.eventId); + } + } + } + Component { + MenuItem { + enabled: visible + text: qsTr("&Copy") + visible: messageContextMenuC.text - text: qsTr("Re&act") - visible: room ? room.permissions.canSend(MtxEvent.Reaction) : false + onTriggered: Clipboard.text = messageContextMenuC.text + } + } + 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) { - room.input.reaction(messageContextMenuC.eventId, plaintext); - TimelineManager.focusMessageInput(); - }) - } - MenuItem { - enabled: visible - text: qsTr("Repl&y") - visible: room ? room.permissions.canSend(MtxEvent.TextMessage) : false + onTriggered: Clipboard.text = messageContextMenuC.link + } + } + Component { + MenuItem { + enabled: visible + id: reactionOption - onTriggered: room.reply = (messageContextMenuC.eventId) - } - MenuItem { - enabled: visible - text: qsTr("&Edit") - visible: messageContextMenuC.isEditable && (room ? room.permissions.canSend(MtxEvent.TextMessage) : false) + text: qsTr("Re&act") + visible: room ? room.permissions.canSend(MtxEvent.Reaction) : false - onTriggered: room.edit = (messageContextMenuC.eventId) - } - MenuItem { - enabled: visible - text: qsTr("&Thread") - visible: (room ? room.permissions.canSend(MtxEvent.TextMessage) : false) + onTriggered: emojiPopup.visible ? emojiPopup.close() : emojiPopup.show(null, room.roomId, function (plaintext, markdown) { + room.input.reaction(messageContextMenuC.eventId, plaintext); + TimelineManager.focusMessageInput(); + }) + } + } + Component { + MenuItem { + enabled: visible + text: qsTr("Repl&y") + visible: room ? room.permissions.canSend(MtxEvent.TextMessage) : false - onTriggered: room.thread = (messageContextMenuC.threadId || messageContextMenuC.eventId) - } - MenuItem { - enabled: visible - text: visible && room.pinnedMessages.includes(messageContextMenuC.eventId) ? qsTr("Un&pin") : qsTr("&Pin") - visible: (room ? room.permissions.canChange(MtxEvent.PinnedEvents) : false) + onTriggered: room.reply = (messageContextMenuC.eventId) + } + } + Component { + MenuItem { + 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) - } - MenuItem { - enabled: visible - text: qsTr("&Read receipts") + onTriggered: room.edit = (messageContextMenuC.eventId) + } + } + Component { + MenuItem { + enabled: visible + text: qsTr("&Thread") + visible: (room ? room.permissions.canSend(MtxEvent.TextMessage) : false) - onTriggered: room.showReadReceipts(messageContextMenuC.eventId) - } - MenuItem { - enabled: visible - 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: room.thread = (messageContextMenuC.threadId || messageContextMenuC.eventId) + } + } + Component { + MenuItem { + enabled: visible + text: visible && room.pinnedMessages.includes(messageContextMenuC.eventId) ? qsTr("Un&pin") : qsTr("&Pin") + visible: (room ? room.permissions.canChange(MtxEvent.PinnedEvents) : false) - onTriggered: { - var forwardMess = forwardCompleterComponent.createObject(timelineRoot); - forwardMess.setMessageEventId(messageContextMenuC.eventId); - forwardMess.open(); - timelineRoot.destroyOnClose(forwardMess); + onTriggered: visible && room.pinnedMessages.includes(messageContextMenuC.eventId) ? room.unpin(messageContextMenuC.eventId) : room.pin(messageContextMenuC.eventId) + } } - } - MenuItem { - enabled: visible - text: qsTr("&Mark as read") - } - MenuItem { - enabled: visible - text: qsTr("View raw message") + Component { + MenuItem { + enabled: visible + text: qsTr("&Read receipts") - onTriggered: room.viewRawMessage(messageContextMenuC.eventId) - } - MenuItem { - enabled: visible - text: qsTr("View decrypted raw message") - // TODO(Nico): Fix this still being iterated over, when using keyboard to select options - visible: messageContextMenuC.isEncrypted + onTriggered: room.showReadReceipts(messageContextMenuC.eventId) + } + } + Component { + MenuItem { + enabled: visible + 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) - } - 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); + onTriggered: room.viewRawMessage(messageContextMenuC.eventId) + } } - } - 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("View decrypted raw message") + // TODO(Nico): Fix this still being iterated over, when using keyboard to select options + visible: messageContextMenuC.isEncrypted + + onTriggered: room.viewDecryptedRawMessage(messageContextMenuC.eventId) + } } - } - 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 + Component { + 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 { + 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) - } - MenuItem { - 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.saveMedia(messageContextMenuC.eventId) + } + } + Component { + MenuItem { + 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) - } - MenuItem { - enabled: visible - text: qsTr("Copy link to eve&nt") - visible: messageContextMenuC.eventId + onTriggered: room.openMedia(messageContextMenuC.eventId) + } + } + Component { + MenuItem { + enabled: visible + text: qsTr("Copy link to eve&nt") + visible: messageContextMenuC.eventId - onTriggered: room.copyLinkToEvent(messageContextMenuC.eventId) + onTriggered: room.copyLinkToEvent(messageContextMenuC.eventId) + } + } } } Component { @@ -615,6 +658,8 @@ Item { text = text_; link = link_; eventId = eventId_; + + replyContextMenuCFilter.updateTarget(); open(); } @@ -625,26 +670,36 @@ Item { } - MenuItem { - enabled: visible - text: qsTr("&Copy") - visible: replyContextMenuC.text + NhekoMenuVisibilityFilter on contentData { + id: replyContextMenuCFilter - onTriggered: Clipboard.text = replyContextMenuC.text - } - MenuItem { - enabled: visible - text: qsTr("Copy &link location") - visible: replyContextMenuC.link + Component { + MenuItem { + enabled: visible + text: qsTr("&Copy") + visible: replyContextMenuC.text - onTriggered: Clipboard.text = replyContextMenuC.link - } - MenuItem { - enabled: visible - text: qsTr("&Go to quoted message") - visible: true + onTriggered: Clipboard.text = replyContextMenuC.text + } + } + Component { + MenuItem { + 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 { diff --git a/resources/qml/components/SpaceMenuLevel.qml b/resources/qml/components/SpaceMenuLevel.qml index 9b7735f8..6c3c0d1a 100644 --- a/resources/qml/components/SpaceMenuLevel.qml +++ b/resources/qml/components/SpaceMenuLevel.qml @@ -12,77 +12,88 @@ Menu { property string roomid property Component childMenu + property var modelData: undefined property int position: modelData == undefined ? -2 : modelData.treeIndex title: modelData != undefined ? modelData.name : qsTr("Add or remove from community") property bool loadChildren: false - onAboutToShow: loadChildren = true - //onAboutToHide: loadChildren = false + onAboutToShow: { + loadChildren = true; + menuFilter.updateTarget(); + } ButtonGroup { id: modificationGroup //visible: position != -1 } - MenuItem { - text: qsTr("Official 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, 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) - } + NhekoMenuVisibilityFilter on contentData { + id: menuFilter - MenuSeparator { - //text: qsTr("Subcommunities") - ButtonGroup.group: modificationGroup - visible: position != -1 && inst.model != undefined - } + Component { + MenuItem { + text: qsTr("Official 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, 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 } diff --git a/src/ui/NhekoMenuVisibilityFilter.cpp b/src/ui/NhekoMenuVisibilityFilter.cpp new file mode 100644 index 00000000..597cba9c --- /dev/null +++ b/src/ui/NhekoMenuVisibilityFilter.cpp @@ -0,0 +1,136 @@ +// SPDX-FileCopyrightText: Nheko Contributors +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "NhekoMenuVisibilityFilter.h" + +#include +#include + +#include "Logging.h" + +QQmlListProperty +NhekoMenuVisibilityFilter::items() +{ + return QQmlListProperty(this, + this, + &NhekoMenuVisibilityFilter::appendItem, + &NhekoMenuVisibilityFilter::itemCount, + &NhekoMenuVisibilityFilter::getItem, + &NhekoMenuVisibilityFilter::clearItems, + &NhekoMenuVisibilityFilter::replaceItem, + &NhekoMenuVisibilityFilter::removeLast); +} + +void +NhekoMenuVisibilityFilter::appendItem(QQmlListProperty *p, QQmlComponent *c) +{ + NhekoMenuVisibilityFilter *dc = static_cast(p->object); + dc->items_.append(c); + // dc->updateTarget(); + + // QQmlProperty prop(c, "visible"); + // prop.connectNotifySignal(dc, SLOT(updateTarget())); +} +qsizetype +NhekoMenuVisibilityFilter::itemCount(QQmlListProperty *p) +{ + return static_cast(p->object)->items_.count(); +} +QQmlComponent * +NhekoMenuVisibilityFilter::getItem(QQmlListProperty *p, qsizetype index) +{ + return static_cast(p->object)->items_.at(index); +} +void +NhekoMenuVisibilityFilter::clearItems(QQmlListProperty *p) +{ + static_cast(p->object)->items_.clear(); + // static_cast(p->object)->updateTarget(); +} +void +NhekoMenuVisibilityFilter::replaceItem(QQmlListProperty *p, + qsizetype index, + QQmlComponent *c) +{ + static_cast(p->object)->items_.assign(index, c); + // static_cast(p->object)->updateTarget(); +} +void +NhekoMenuVisibilityFilter::removeLast(QQmlListProperty *p) +{ + static_cast(p->object)->items_.pop_back(); + // static_cast(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(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(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" diff --git a/src/ui/NhekoMenuVisibilityFilter.h b/src/ui/NhekoMenuVisibilityFilter.h new file mode 100644 index 00000000..92ea4b29 --- /dev/null +++ b/src/ui/NhekoMenuVisibilityFilter.h @@ -0,0 +1,53 @@ +// SPDX-FileCopyrightText: Nheko Contributors +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include +#include +#include +#include +#include +#include + +class NhekoMenuVisibilityFilter + : public QObject + , public QQmlPropertyValueSource +{ + Q_OBJECT + Q_INTERFACES(QQmlPropertyValueSource) + Q_CLASSINFO("DefaultProperty", "items") + Q_PROPERTY(QQmlListProperty 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 items(); + + void setTarget(const QQmlProperty &prop) override; + +private: + QQmlProperty targetProperty; + QList items_; + QQmlComponent *delegate = nullptr; + QVariantList model; + + static void appendItem(QQmlListProperty *, QQmlComponent *); + static qsizetype itemCount(QQmlListProperty *); + static QQmlComponent *getItem(QQmlListProperty *, qsizetype index); + static void clearItems(QQmlListProperty *); + static void replaceItem(QQmlListProperty *, qsizetype index, QQmlComponent *); + static void removeLast(QQmlListProperty *); + +public slots: + // call this before showing the menu. We don't want to update elsewhere to prevent jumping menus + // and useless work + void updateTarget(); +};