// SPDX-FileCopyrightText: Nheko Contributors // // SPDX-License-Identifier: GPL-3.0-or-later #include "EventDelegateChooser.h" #include "TimelineModel.h" #include "Logging.h" #include #include #include // privat qt headers to access required properties #include #include QQmlComponent * EventDelegateChoice::delegate() const { return delegate_; } void EventDelegateChoice::setDelegate(QQmlComponent *delegate) { if (delegate != delegate_) { delegate_ = delegate; emit delegateChanged(); emit changed(); } } QList EventDelegateChoice::roleValues() const { return roleValues_; } void EventDelegateChoice::setRoleValues(const QList &value) { if (value != roleValues_) { roleValues_ = value; emit roleValuesChanged(); emit changed(); } } QQmlListProperty EventDelegateChooser::choices() { return QQmlListProperty(this, this, &EventDelegateChooser::appendChoice, &EventDelegateChooser::choiceCount, &EventDelegateChooser::choice, &EventDelegateChooser::clearChoices); } void EventDelegateChooser::appendChoice(QQmlListProperty *p, EventDelegateChoice *c) { EventDelegateChooser *dc = static_cast(p->object); dc->choices_.append(c); } qsizetype EventDelegateChooser::choiceCount(QQmlListProperty *p) { return static_cast(p->object)->choices_.count(); } EventDelegateChoice * EventDelegateChooser::choice(QQmlListProperty *p, qsizetype index) { return static_cast(p->object)->choices_.at(index); } void EventDelegateChooser::clearChoices(QQmlListProperty *p) { static_cast(p->object)->choices_.clear(); } void EventDelegateChooser::componentComplete() { QQuickItem::componentComplete(); eventIncubator.reset(eventId_); replyIncubator.reset(replyId); // eventIncubator.forceCompletion(); } void EventDelegateChooser::DelegateIncubator::setInitialState(QObject *obj) { auto item = qobject_cast(obj); if (!item) return; item->setParentItem(&chooser); item->setParent(&chooser); auto roleNames = chooser.room_->roleNames(); QHash nameToRole; for (const auto &[k, v] : roleNames.asKeyValueRange()) { nameToRole.insert(v, k); } QHash roleToPropIdx; std::vector roles; // Workaround for https://bugreports.qt.io/browse/QTBUG-98846 QHash 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); Qt::beginPropertyUpdateGroup(); auto attached = qobject_cast( qmlAttachedPropertiesObject(obj)); Q_ASSERT(attached != nullptr); attached->setIsReply(this->forReply || chooser.limitAsReply_); for (const auto &role : roles) { const auto &roleName = roleNames[role.role()]; // nhlog::ui()->critical("Setting role {}, {} to {}", // role.role(), // roleName.toStdString(), // role.data().toString().toStdString()); // nhlog::ui()->critical("Setting {}", mo->property(roleToPropIdx[role.role()]).name()); mo->property(roleToPropIdx[role.role()]).write(obj, role.data()); if (const auto &req = requiredProperties.find(roleName); req != requiredProperties.end()) QQmlIncubatorPrivate::get(this)->requiredProperties()->remove(*req); } Qt::endPropertyUpdateGroup(); // setInitialProperties(rolesToSet); auto update = [this, obj, roleToPropIdx = std::move(roleToPropIdx)]( const QList &changedRoles, TimelineModel *room) { if (!room) return; if (changedRoles.empty() || changedRoles.contains(TimelineModel::Roles::Type)) { int type = room ->dataById(currentId, TimelineModel::Roles::Type, forReply ? chooser.eventId_ : QString()) .toInt(); if (type != oldType) { // nhlog::ui()->debug("Type changed!"); reset(currentId); return; } } std::vector rolesToRequest; if (changedRoles.empty()) { for (const auto role : std::ranges::subrange(roleToPropIdx.keyBegin(), roleToPropIdx.keyEnd())) 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(); room->multiData(currentId, forReply ? chooser.eventId_ : QString(), rolesToRequest); Qt::beginPropertyUpdateGroup(); for (const auto &role : rolesToRequest) { mo->property(roleToPropIdx[role.role()]).write(obj, role.data()); } Qt::endPropertyUpdateGroup(); }; if (!forReply) { auto row = chooser.room_->idToIndex(currentId); auto connection = connect( chooser.room_, &QAbstractItemModel::dataChanged, obj, [row, update, room = chooser.room_](const QModelIndex &topLeft, const QModelIndex &bottomRight, const QList &changedRoles) { if (row < topLeft.row() || row > bottomRight.row()) return; update(changedRoles, room); }, Qt::QueuedConnection); connect( &this->chooser, &EventDelegateChooser::destroyed, obj, [connection]() { QObject::disconnect(connection); }, Qt::SingleShotConnection); connect( &this->chooser, &EventDelegateChooser::roomChanged, obj, [connection]() { QObject::disconnect(connection); }, Qt::SingleShotConnection); } } 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(); this->oldType = role; for (const auto choice : std::as_const(chooser.choices_)) { const auto &choiceValue = choice->roleValues(); if (choiceValue.contains(role) || choiceValue.empty()) { // nhlog::ui()->debug( // "Instantiating type: {}, c {}", (int)role, choiceValue.contains(role)); if (auto child = qobject_cast(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(object()); if (child == nullptr) { nhlog::ui()->error("Delegate has to be derived of Item!"); return; } child->setParentItem(&chooser); QQmlEngine::setObjectOwnership(child, QQmlEngine::ObjectOwnership::CppOwnership); // connect(child, &QQuickItem::parentChanged, child, [child](QQuickItem *) { // // QTBUG-115687 // if (child->flags().testFlag(QQuickItem::ItemObservesViewport)) { // nhlog::ui()->critical("SETTING OBSERVES VIEWPORT"); // // Re-trigger the parent traversal to get subtreeTransformChangedEnabled turned // on child->setFlag(QQuickItem::ItemObservesViewport); // } // }); if (forReply) emit chooser.replyChanged(); else emit chooser.mainChanged(); chooser.polish(); } else if (status == QQmlIncubator::Error) { auto errors_ = errors(); for (const auto &e : std::as_const(errors_)) nhlog::ui()->error("Error instantiating delegate: {}", e.toString().toStdString()); } } void EventDelegateChooser::updatePolish() { auto mainChild = qobject_cast(eventIncubator.object()); auto replyChild = qobject_cast(replyIncubator.object()); // nhlog::ui()->trace("POLISHING {}", (void *)this); auto layoutItem = [this](QQuickItem *item, int inset) { if (item) { QObject::disconnect(item, &QQuickItem::implicitWidthChanged, this, &QQuickItem::polish); auto attached = qobject_cast( qmlAttachedPropertiesObject(item)); Q_ASSERT(attached != nullptr); int maxWidth = maxWidth_ - inset; // in theory we could also reset the width, but that doesn't seem to work nicely for // text areas because of how they cache it. if (attached->maxWidth() > 0) item->setWidth(attached->maxWidth()); else item->setWidth(maxWidth); item->ensurePolished(); auto width = item->implicitWidth(); if (width < 1 || width > maxWidth) width = maxWidth; if (attached->maxWidth() > 0 && width > attached->maxWidth()) width = attached->maxWidth(); if (attached->keepAspectRatio()) { auto height = width * attached->aspectRatio(); if (attached->maxHeight() && height > attached->maxHeight()) { height = attached->maxHeight(); width = height / attached->aspectRatio(); } item->setHeight(height); } item->setWidth(width); item->ensurePolished(); QObject::connect(item, &QQuickItem::implicitWidthChanged, this, &QQuickItem::polish); } }; layoutItem(mainChild, mainInset_); layoutItem(replyChild, replyInset_); } void EventDelegateChooserAttachedType::polishChooser() { auto p = parent(); if (p) { auto chooser = qobject_cast(p->parent()); if (chooser) { chooser->polish(); } } }