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

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