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(); +};