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 3 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. 55
      resources/qml/MessageView.qml
  4. 33
      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/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

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

@ -419,6 +419,9 @@ Item {
link = link_;
else
link = "";
messageActionsCFilter.updateTarget();
if (showAt_)
popup(showAt_);
else
@ -453,6 +456,10 @@ Item {
}
}
NhekoMenuVisibilityFilter on contentData {
id: messageActionsCFilter
Component {
MenuItem {
enabled: visible
text: qsTr("Go to &message")
@ -463,6 +470,8 @@ Item {
room.showEvent(messageContextMenuC.eventId);
}
}
}
Component {
MenuItem {
enabled: visible
text: qsTr("&Copy")
@ -470,6 +479,8 @@ Item {
onTriggered: Clipboard.text = messageContextMenuC.text
}
}
Component {
MenuItem {
enabled: visible
text: qsTr("Copy &link location")
@ -477,6 +488,8 @@ Item {
onTriggered: Clipboard.text = messageContextMenuC.link
}
}
Component {
MenuItem {
enabled: visible
id: reactionOption
@ -489,6 +502,8 @@ Item {
TimelineManager.focusMessageInput();
})
}
}
Component {
MenuItem {
enabled: visible
text: qsTr("Repl&y")
@ -496,6 +511,8 @@ Item {
onTriggered: room.reply = (messageContextMenuC.eventId)
}
}
Component {
MenuItem {
enabled: visible
text: qsTr("&Edit")
@ -503,6 +520,8 @@ Item {
onTriggered: room.edit = (messageContextMenuC.eventId)
}
}
Component {
MenuItem {
enabled: visible
text: qsTr("&Thread")
@ -510,6 +529,8 @@ Item {
onTriggered: room.thread = (messageContextMenuC.threadId || messageContextMenuC.eventId)
}
}
Component {
MenuItem {
enabled: visible
text: visible && room.pinnedMessages.includes(messageContextMenuC.eventId) ? qsTr("Un&pin") : qsTr("&Pin")
@ -517,12 +538,16 @@ Item {
onTriggered: visible && room.pinnedMessages.includes(messageContextMenuC.eventId) ? room.unpin(messageContextMenuC.eventId) : room.pin(messageContextMenuC.eventId)
}
}
Component {
MenuItem {
enabled: visible
text: qsTr("&Read receipts")
onTriggered: room.showReadReceipts(messageContextMenuC.eventId)
}
}
Component {
MenuItem {
enabled: visible
text: qsTr("&Forward")
@ -535,16 +560,22 @@ Item {
timelineRoot.destroyOnClose(forwardMess);
}
}
}
Component {
MenuItem {
enabled: visible
text: qsTr("&Mark as read")
}
}
Component {
MenuItem {
enabled: visible
text: qsTr("View raw message")
onTriggered: room.viewRawMessage(messageContextMenuC.eventId)
}
}
Component {
MenuItem {
enabled: visible
text: qsTr("View decrypted raw message")
@ -553,6 +584,8 @@ Item {
onTriggered: room.viewDecryptedRawMessage(messageContextMenuC.eventId)
}
}
Component {
MenuItem {
enabled: visible
text: qsTr("Remo&ve message")
@ -566,6 +599,8 @@ Item {
timelineRoot.destroyOnClose(dialog);
}
}
}
Component {
MenuItem {
text: qsTr("Report message")
enabled: visible
@ -576,6 +611,8 @@ Item {
timelineRoot.destroyOnClose(dialog);
}
}
}
Component {
MenuItem {
enabled: visible
text: qsTr("&Save as")
@ -583,6 +620,8 @@ Item {
onTriggered: room.saveMedia(messageContextMenuC.eventId)
}
}
Component {
MenuItem {
enabled: visible
text: qsTr("&Open in external program")
@ -590,6 +629,8 @@ Item {
onTriggered: room.openMedia(messageContextMenuC.eventId)
}
}
Component {
MenuItem {
enabled: visible
text: qsTr("Copy link to eve&nt")
@ -598,6 +639,8 @@ Item {
onTriggered: room.copyLinkToEvent(messageContextMenuC.eventId)
}
}
}
}
Component {
id: forwardCompleterComponent
@ -615,6 +658,8 @@ Item {
text = text_;
link = link_;
eventId = eventId_;
replyContextMenuCFilter.updateTarget();
open();
}
@ -625,6 +670,10 @@ Item {
}
NhekoMenuVisibilityFilter on contentData {
id: replyContextMenuCFilter
Component {
MenuItem {
enabled: visible
text: qsTr("&Copy")
@ -632,6 +681,8 @@ Item {
onTriggered: Clipboard.text = replyContextMenuC.text
}
}
Component {
MenuItem {
enabled: visible
text: qsTr("Copy &link location")
@ -639,6 +690,8 @@ Item {
onTriggered: Clipboard.text = replyContextMenuC.link
}
}
Component {
MenuItem {
enabled: visible
text: qsTr("&Go to quoted message")
@ -647,6 +700,8 @@ Item {
onTriggered: room.showEvent(replyContextMenuC.eventId)
}
}
}
}
RoundButton {
id: toEndButton

@ -12,18 +12,25 @@ 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
}
NhekoMenuVisibilityFilter on contentData {
id: menuFilter
Component {
MenuItem {
text: qsTr("Official community for this room")
ButtonGroup.group: modificationGroup
@ -33,6 +40,8 @@ Menu {
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
@ -42,6 +51,8 @@ Menu {
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
@ -51,6 +62,8 @@ Menu {
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
@ -60,6 +73,8 @@ Menu {
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
@ -70,19 +85,15 @@ Menu {
onTriggered: if (checked) Communities.updateSpaceStatus(modelData.roomid, spacesMenu.roomid, false, false, false)
}
}
Component {
MenuSeparator {
//text: qsTr("Subcommunities")
ButtonGroup.group: modificationGroup
visible: position != -1 && inst.model != undefined
visible: position != -1// && menuFilter.model != undefined
}
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)
model: spacesMenu.loadChildren ? Communities.spaceChildrenListFromIndex(spacesMenu.roomid, spacesMenu.position) : []
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