Create an EventDelegateChooser

This commit is contained in:
Nicolas Werner 2023-06-22 19:54:17 +02:00
parent 797dadd7e9
commit 4d8b8c3b81
No known key found for this signature in database
GPG key ID: C8D75E610773F2D9
8 changed files with 430 additions and 189 deletions

View file

@ -359,6 +359,8 @@ set(SRC_FILES
src/timeline/CommunitiesModel.h src/timeline/CommunitiesModel.h
src/timeline/DelegateChooser.cpp src/timeline/DelegateChooser.cpp
src/timeline/DelegateChooser.h src/timeline/DelegateChooser.h
src/timeline/EventDelegateChooser.cpp
src/timeline/EventDelegateChooser.h
src/timeline/EventStore.cpp src/timeline/EventStore.cpp
src/timeline/EventStore.h src/timeline/EventStore.h
src/timeline/InputBar.cpp src/timeline/InputBar.cpp
@ -882,6 +884,7 @@ target_link_libraries(nheko PRIVATE
Qt::Gui Qt::Gui
Qt::Multimedia Qt::Multimedia
Qt::Qml Qt::Qml
Qt::QmlPrivate
Qt::QuickControls2 Qt::QuickControls2
qt6keychain qt6keychain
nlohmann_json::nlohmann_json nlohmann_json::nlohmann_json

View file

@ -20,6 +20,7 @@ Item {
property int availableWidth: width property int availableWidth: width
property int padding: Nheko.paddingMedium property int padding: Nheko.paddingMedium
property string searchString: "" property string searchString: ""
property Room roommodel: room
// HACK: https://bugreports.qt.io/browse/QTBUG-83972, qtwayland cannot auto hide menu // HACK: https://bugreports.qt.io/browse/QTBUG-83972, qtwayland cannot auto hide menu
Connections { Connections {
@ -58,175 +59,35 @@ Item {
spacing: 2 spacing: 2
verticalLayoutDirection: ListView.BottomToTop verticalLayoutDirection: ListView.BottomToTop
delegate: Item { delegate: EventDelegateChooser {
id: wrapper id: wrapper
required property string blurhash
required property string body
required property string callType
required property var day
required property string duration
required property int encryptionError
required property string eventId
required property string filename
required property string filesize
required property string formattedBody
required property int index
required property bool isEditable
required property bool isEdited
required property bool isEncrypted
required property bool isOnlyEmoji
required property bool isSender
required property bool isStateEvent
required property int notificationlevel
required property int originalWidth
property var previousMessageDay: (index + 1) >= chat.count ? 0 : chat.model.dataByIndex(index + 1, Room.Day)
property bool previousMessageIsStateEvent: (index + 1) >= chat.count ? true : chat.model.dataByIndex(index + 1, Room.IsStateEvent)
property string previousMessageUserId: (index + 1) >= chat.count ? "" : chat.model.dataByIndex(index + 1, Room.UserId)
required property double proportionalHeight
required property var reactions
required property int relatedEventCacheBuster
required property string replyTo
required property string roomName
required property string roomTopic
property bool scrolledToThis: eventId === room.scrollTarget && (y + height > chat.y + chat.contentY && y < chat.y + chat.height + chat.contentY)
required property int status
required property string threadId
required property string thumbnailUrl
required property var timestamp
required property int trustlevel
required property int type
required property string typeString
required property string url
required property string userId
required property string userName
required property int userPowerlevel
ListView.delayRemove: true ListView.delayRemove: true
anchors.horizontalCenter: parent ? parent.horizontalCenter : undefined
height: (section.item?.height ?? 0) + timelinerow.height
width: chat.delegateMaxWidth width: chat.delegateMaxWidth
height: main?.height ?? 10
room: chatRoot.roommodel
Loader { EventDelegateChoice {
id: section roleValues: [
MtxEvent.TextMessage,
MtxEvent.NoticeMessage,
]
TextArea {
required property string body
property var day: wrapper.day width: parent.width
property bool isSender: wrapper.isSender text: body
property bool isStateEvent: wrapper.isStateEvent
property int parentWidth: parent.width
property var previousMessageDay: wrapper.previousMessageDay
property bool previousMessageIsStateEvent: wrapper.previousMessageIsStateEvent
property string previousMessageUserId: wrapper.previousMessageUserId
property date timestamp: wrapper.timestamp
property string userId: wrapper.userId
property string userName: wrapper.userName
property int userPowerlevel: wrapper.userPowerlevel
active: previousMessageUserId !== userId || previousMessageDay !== day || previousMessageIsStateEvent !== isStateEvent
//asynchronous: true
sourceComponent: sectionHeader
visible: status == Loader.Ready
z: 4
}
TimelineRow {
id: timelinerow
blurhash: wrapper.blurhash
body: wrapper.body
callType: wrapper.callType
duration: wrapper.duration
encryptionError: wrapper.encryptionError
eventId: chat.model, wrapper.eventId
filename: wrapper.filename
filesize: wrapper.filesize
formattedBody: wrapper.formattedBody
index: wrapper.index
isEditable: wrapper.isEditable
isEdited: wrapper.isEdited
isEncrypted: wrapper.isEncrypted
isOnlyEmoji: wrapper.isOnlyEmoji
isSender: wrapper.isSender
isStateEvent: wrapper.isStateEvent
notificationlevel: wrapper.notificationlevel
originalWidth: wrapper.originalWidth
proportionalHeight: wrapper.proportionalHeight
reactions: wrapper.reactions
relatedEventCacheBuster: wrapper.relatedEventCacheBuster
replyTo: wrapper.replyTo
roomName: wrapper.roomName
roomTopic: wrapper.roomTopic
status: wrapper.status
threadId: wrapper.threadId
thumbnailUrl: wrapper.thumbnailUrl
timestamp: wrapper.timestamp
trustlevel: wrapper.trustlevel
type: chat.model, wrapper.type
typeString: wrapper.typeString
url: wrapper.url
userId: wrapper.userId
userName: wrapper.userName
width: wrapper.width
y: section.visible && section.active ? section.y + section.height : 0
background: Rectangle {
id: scrollHighlight
color: palette.highlight
enabled: false
opacity: 0
visible: true
z: 1
states: State {
name: "revealed"
when: wrapper.scrolledToThis
}
transitions: Transition {
from: ""
to: "revealed"
SequentialAnimation {
PropertyAnimation {
duration: 500
easing.type: Easing.InOutQuad
from: 0
properties: "opacity"
target: scrollHighlight
to: 1
}
PropertyAnimation {
duration: 500
easing.type: Easing.InOutQuad
from: 1
properties: "opacity"
target: scrollHighlight
to: 0
}
ScriptAction {
script: room.eventShown()
}
}
} }
} }
onHoveredChanged: { EventDelegateChoice {
if (!Settings.mobileMode && hovered) { roleValues: [
if (!messageActions.hovered) { ]
messageActions.attached = timelinerow; TextArea {
messageActions.model = timelinerow; width: parent.width
text: "Unsupported"
} }
} }
} }
}
Connections {
function onMovementEnded() {
if (y + height + 2 * chat.spacing > chat.contentY + chat.height && y < chat.contentY + chat.height)
chat.model.currentIndex = index;
}
target: chat
}
}
footer: Item { footer: Item {
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
anchors.margins: Nheko.paddingLarge anchors.margins: Nheko.paddingLarge

View file

@ -147,7 +147,7 @@ AbstractButton {
columns: Settings.bubbles ? 1 : 2 columns: Settings.bubbles ? 1 : 2
rowSpacing: 0 rowSpacing: 0
rows: Settings.bubbles ? 3 : 2 rows: Settings.bubbles ? 3 : 2
/*
anchors { anchors {
left: parent.left left: parent.left
leftMargin: 4 leftMargin: 4
@ -230,6 +230,7 @@ AbstractButton {
userId: r.userId userId: r.userId
userName: r.userName userName: r.userName
} }
*/
Row { Row {
id: metadata id: metadata

View file

@ -95,37 +95,11 @@ AbstractButton {
onClicked: room.openUserProfile(userId) onClicked: room.openUserProfile(userId)
} }
MessageDelegate { Rectangle {
Layout.leftMargin: 4 Layout.leftMargin: 4
Layout.preferredHeight: height Layout.preferredHeight: 20
id: reply
blurhash: r.blurhash
body: r.body
formattedBody: r.formattedBody
eventId: r.eventId
filename: r.filename
filesize: r.filesize
proportionalHeight: r.proportionalHeight
type: r.type
typeString: r.typeString ?? ""
url: r.url
thumbnailUrl: r.thumbnailUrl
duration: r.duration
originalWidth: r.originalWidth
isOnlyEmoji: r.isOnlyEmoji
isStateEvent: r.isStateEvent
userId: r.userId
userName: r.userName
roomTopic: r.roomTopic
roomName: r.roomName
callType: r.callType
relatedEventCacheBuster: r.relatedEventCacheBuster
encryptionError: r.encryptionError
// This is disabled so that left clicking the reply goes to its location
enabled: false
Layout.fillWidth: true Layout.fillWidth: true
isReply: true color: "green"
keepFullText: r.keepFullText
} }
} }

View file

@ -0,0 +1,244 @@
// SPDX-FileCopyrightText: Nheko Contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later
#include "EventDelegateChooser.h"
#include "TimelineModel.h"
#include "Logging.h"
#include <QQmlEngine>
#include <QtGlobal>
// privat qt headers to access required properties
#include <QtQml/private/qqmlincubator_p.h>
#include <QtQml/private/qqmlobjectcreator_p.h>
QQmlComponent *
EventDelegateChoice::delegate() const
{
return delegate_;
}
void
EventDelegateChoice::setDelegate(QQmlComponent *delegate)
{
if (delegate != delegate_) {
delegate_ = delegate;
emit delegateChanged();
emit changed();
}
}
QList<int>
EventDelegateChoice::roleValues() const
{
return roleValues_;
}
void
EventDelegateChoice::setRoleValues(const QList<int> &value)
{
if (value != roleValues_) {
roleValues_ = value;
emit roleValuesChanged();
emit changed();
}
}
QQmlListProperty<EventDelegateChoice>
EventDelegateChooser::choices()
{
return QQmlListProperty<EventDelegateChoice>(this,
this,
&EventDelegateChooser::appendChoice,
&EventDelegateChooser::choiceCount,
&EventDelegateChooser::choice,
&EventDelegateChooser::clearChoices);
}
void
EventDelegateChooser::appendChoice(QQmlListProperty<EventDelegateChoice> *p, EventDelegateChoice *c)
{
EventDelegateChooser *dc = static_cast<EventDelegateChooser *>(p->object);
dc->choices_.append(c);
}
qsizetype
EventDelegateChooser::choiceCount(QQmlListProperty<EventDelegateChoice> *p)
{
return static_cast<EventDelegateChooser *>(p->object)->choices_.count();
}
EventDelegateChoice *
EventDelegateChooser::choice(QQmlListProperty<EventDelegateChoice> *p, qsizetype index)
{
return static_cast<EventDelegateChooser *>(p->object)->choices_.at(index);
}
void
EventDelegateChooser::clearChoices(QQmlListProperty<EventDelegateChoice> *p)
{
static_cast<EventDelegateChooser *>(p->object)->choices_.clear();
}
void
EventDelegateChooser::componentComplete()
{
QQuickItem::componentComplete();
// eventIncubator.reset(eventIndex);
}
void
EventDelegateChooser::DelegateIncubator::setInitialState(QObject *obj)
{
auto item = qobject_cast<QQuickItem *>(obj);
if (!item)
return;
item->setParentItem(&chooser);
auto roleNames = chooser.room_->roleNames();
QHash<QByteArray, int> nameToRole;
for (const auto &[k, v] : roleNames.asKeyValueRange()) {
nameToRole.insert(v, k);
}
QHash<int, int> roleToPropIdx;
std::vector<QModelRoleData> roles;
// Workaround for https://bugreports.qt.io/browse/QTBUG-98846
QHash<QString, RequiredPropertyKey> requiredProperties;
for (const auto &[propKey, prop] :
QQmlIncubatorPrivate::get(this)->requiredProperties()->asKeyValueRange()) {
requiredProperties.insert(prop.propertyName, propKey);
}
// collect required properties
auto mo = obj->metaObject();
for (int i = 0; i < mo->propertyCount(); i++) {
auto prop = mo->property(i);
// nhlog::ui()->critical("Found prop {}", prop.name());
// See https://bugreports.qt.io/browse/QTBUG-98846
if (!prop.isRequired() && !requiredProperties.contains(prop.name()))
continue;
if (auto role = nameToRole.find(prop.name()); role != nameToRole.end()) {
roleToPropIdx.insert(*role, i);
roles.emplace_back(*role);
nhlog::ui()->critical("Found prop {}, idx {}, role {}", prop.name(), i, *role);
} else {
nhlog::ui()->critical("Required property {} not found in model!", prop.name());
}
}
nhlog::ui()->debug("Querying data for id {}", currentId.toStdString());
chooser.room_->multiData(currentId, forReply ? chooser.eventId_ : QString(), roles);
QVariantMap rolesToSet;
for (const auto &role : roles) {
const auto &roleName = roleNames[role.role()];
nhlog::ui()->critical("Setting role {}, {}", role.role(), roleName.toStdString());
mo->property(roleToPropIdx[role.role()]).write(obj, role.data());
rolesToSet.insert(roleName, role.data());
if (const auto &req = requiredProperties.find(roleName); req != requiredProperties.end())
QQmlIncubatorPrivate::get(this)->requiredProperties()->remove(*req);
}
// setInitialProperties(rolesToSet);
auto update =
[this, obj, roleToPropIdx = std::move(roleToPropIdx)](const QList<int> &changedRoles) {
std::vector<QModelRoleData> rolesToRequest;
if (changedRoles.empty()) {
for (auto role : roleToPropIdx.keys())
rolesToRequest.emplace_back(role);
} else {
for (auto role : changedRoles) {
if (roleToPropIdx.contains(role)) {
rolesToRequest.emplace_back(role);
}
}
}
if (rolesToRequest.empty())
return;
auto mo = obj->metaObject();
chooser.room_->multiData(
currentId, forReply ? chooser.eventId_ : QString(), rolesToRequest);
for (const auto &role : rolesToRequest) {
mo->property(roleToPropIdx[role.role()]).write(obj, role.data());
}
};
if (!forReply) {
auto row = chooser.room_->idToIndex(currentId);
connect(chooser.room_,
&QAbstractItemModel::dataChanged,
obj,
[row, update](const QModelIndex &topLeft,
const QModelIndex &bottomRight,
const QList<int> &changedRoles) {
if (row < topLeft.row() || row > bottomRight.row())
return;
update(changedRoles);
});
}
}
void
EventDelegateChooser::DelegateIncubator::reset(QString id)
{
if (!chooser.room_ || id.isEmpty())
return;
nhlog::ui()->debug("Reset with id {}, reply {}", id.toStdString(), forReply);
this->currentId = id;
auto role =
chooser.room_
->dataById(id, TimelineModel::Roles::Type, forReply ? chooser.eventId_ : QString())
.toInt();
for (const auto choice : qAsConst(chooser.choices_)) {
const auto &choiceValue = choice->roleValues();
if (choiceValue.contains(role) || choiceValue.empty()) {
if (auto child = qobject_cast<QQuickItem *>(object())) {
child->setParentItem(nullptr);
}
choice->delegate()->create(*this, QQmlEngine::contextForObject(&chooser));
return;
}
}
}
void
EventDelegateChooser::DelegateIncubator::statusChanged(QQmlIncubator::Status status)
{
if (status == QQmlIncubator::Ready) {
auto child = qobject_cast<QQuickItem *>(object());
if (child == nullptr) {
nhlog::ui()->error("Delegate has to be derived of Item!");
return;
}
child->setParentItem(&chooser);
QQmlEngine::setObjectOwnership(child, QQmlEngine::ObjectOwnership::JavaScriptOwnership);
if (forReply)
emit chooser.replyChanged();
else
emit chooser.mainChanged();
} else if (status == QQmlIncubator::Error) {
auto errors_ = errors();
for (const auto &e : qAsConst(errors_))
nhlog::ui()->error("Error instantiating delegate: {}", e.toString().toStdString());
}
}

View file

@ -0,0 +1,136 @@
// SPDX-FileCopyrightText: Nheko Contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later
// A DelegateChooser like the one, that was added to Qt5.12 (in labs), but compatible with older Qt
// versions see KDE/kquickitemviews see qtdeclarative/qqmldelagatecomponent
#pragma once
#include <QAbstractItemModel>
#include <QQmlComponent>
#include <QQmlIncubator>
#include <QQmlListProperty>
#include <QQuickItem>
#include <QtCore/QObject>
#include <QtCore/QVariant>
#include "TimelineModel.h"
class EventDelegateChoice : public QObject
{
Q_OBJECT
QML_ELEMENT
Q_CLASSINFO("DefaultProperty", "delegate")
public:
Q_PROPERTY(QList<int> roleValues READ roleValues WRITE setRoleValues NOTIFY roleValuesChanged
REQUIRED FINAL)
Q_PROPERTY(
QQmlComponent *delegate READ delegate WRITE setDelegate NOTIFY delegateChanged REQUIRED FINAL)
[[nodiscard]] QQmlComponent *delegate() const;
void setDelegate(QQmlComponent *delegate);
[[nodiscard]] QList<int> roleValues() const;
void setRoleValues(const QList<int> &value);
signals:
void delegateChanged();
void roleValuesChanged();
void changed();
private:
QList<int> roleValues_;
QQmlComponent *delegate_ = nullptr;
};
class EventDelegateChooser : public QQuickItem
{
Q_OBJECT
QML_ELEMENT
Q_CLASSINFO("DefaultProperty", "choices")
public:
Q_PROPERTY(QQmlListProperty<EventDelegateChoice> choices READ choices CONSTANT FINAL)
Q_PROPERTY(QQuickItem *main READ main NOTIFY mainChanged FINAL)
Q_PROPERTY(TimelineModel *room READ room WRITE setRoom NOTIFY roomChanged REQUIRED FINAL)
Q_PROPERTY(QString eventId READ eventId WRITE setEventId NOTIFY eventIdChanged REQUIRED FINAL)
Q_PROPERTY(QString replyTo READ replyTo WRITE setReplyTo NOTIFY replyToChanged REQUIRED FINAL)
QQmlListProperty<EventDelegateChoice> choices();
[[nodiscard]] QQuickItem *main() const
{
return qobject_cast<QQuickItem *>(eventIncubator.object());
}
void setRoom(TimelineModel *m)
{
if (m != room_) {
room_ = m;
eventIncubator.reset(eventId_);
replyIncubator.reset(replyId);
emit roomChanged();
}
}
[[nodiscard]] TimelineModel *room() { return room_; }
void setEventId(QString idx)
{
eventId_ = idx;
emit eventIdChanged();
}
[[nodiscard]] QString eventId() const { return eventId_; }
void setReplyTo(QString id)
{
replyId = id;
emit replyToChanged();
}
[[nodiscard]] QString replyTo() const { return replyId; }
void componentComplete() override;
signals:
void mainChanged();
void replyChanged();
void roomChanged();
void eventIdChanged();
void replyToChanged();
private:
struct DelegateIncubator final : public QQmlIncubator
{
DelegateIncubator(EventDelegateChooser &parent, bool forReply)
: QQmlIncubator(QQmlIncubator::AsynchronousIfNested)
, chooser(parent)
, forReply(forReply)
{
}
void setInitialState(QObject *object) override;
void statusChanged(QQmlIncubator::Status status) override;
void reset(QString id);
EventDelegateChooser &chooser;
bool forReply;
QString currentId;
QString instantiatedId;
int instantiatedRole = -1;
QAbstractItemModel *instantiatedModel = nullptr;
};
QVariant roleValue_;
QList<EventDelegateChoice *> choices_;
DelegateIncubator eventIncubator{*this, false};
DelegateIncubator replyIncubator{*this, true};
TimelineModel *room_{nullptr};
QString eventId_;
QString replyId;
static void appendChoice(QQmlListProperty<EventDelegateChoice> *, EventDelegateChoice *);
static qsizetype choiceCount(QQmlListProperty<EventDelegateChoice> *);
static EventDelegateChoice *choice(QQmlListProperty<EventDelegateChoice> *, qsizetype index);
static void clearChoices(QQmlListProperty<EventDelegateChoice> *);
};

View file

@ -926,6 +926,26 @@ TimelineModel::multiData(const QModelIndex &index, QModelRoleDataSpan roleDataSp
} }
} }
void
TimelineModel::multiData(const QString &id,
const QString &relatedTo,
QModelRoleDataSpan roleDataSpan) const
{
if (id.isEmpty())
return;
auto event = events.get(id.toStdString(), relatedTo.toStdString());
if (!event)
return;
for (QModelRoleData &roleData : roleDataSpan) {
int role = roleData.role();
roleData.setData(data(*event, role));
}
}
QVariant QVariant
TimelineModel::dataById(const QString &id, int role, const QString &relatedTo) TimelineModel::dataById(const QString &id, int role, const QString &relatedTo)
{ {

View file

@ -286,6 +286,8 @@ public:
int rowCount(const QModelIndex &parent = QModelIndex()) const override; int rowCount(const QModelIndex &parent = QModelIndex()) const override;
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
void multiData(const QModelIndex &index, QModelRoleDataSpan roleDataSpan) const override; void multiData(const QModelIndex &index, QModelRoleDataSpan roleDataSpan) const override;
void
multiData(const QString &id, const QString &relatedTo, QModelRoleDataSpan roleDataSpan) const;
QVariant data(const mtx::events::collections::TimelineEvents &event, int role) const; QVariant data(const mtx::events::collections::TimelineEvents &event, int role) const;
Q_INVOKABLE QVariant dataById(const QString &id, int role, const QString &relatedTo); Q_INVOKABLE QVariant dataById(const QString &id, int role, const QString &relatedTo);
Q_INVOKABLE QVariant dataByIndex(int i, int role = Qt::DisplayRole) const Q_INVOKABLE QVariant dataByIndex(int i, int role = Qt::DisplayRole) const