From ebeb1eb7721f357b016f6e914509918b6bee5356 Mon Sep 17 00:00:00 2001 From: Nicolas Werner Date: Sat, 7 Sep 2019 22:22:07 +0200 Subject: [PATCH] Implement avatars in qml timeline --- CMakeLists.txt | 1 + resources/qml/Avatar.qml | 45 +++++++++++++++ resources/qml/TimelineView.qml | 5 +- resources/res.qrc | 1 + src/MxcImageProvider.cpp | 79 +++++++++++++++++++++++++++ src/MxcImageProvider.h | 48 ++++++++++++++++ src/RoomInfoListItem.cpp | 2 +- src/UserSettingsPage.cpp | 6 +- src/timeline2/TimelineModel.cpp | 6 ++ src/timeline2/TimelineModel.h | 1 + src/timeline2/TimelineViewManager.cpp | 2 + src/ui/Avatar.cpp | 2 +- 12 files changed, 190 insertions(+), 8 deletions(-) create mode 100644 resources/qml/Avatar.qml create mode 100644 src/MxcImageProvider.cpp create mode 100644 src/MxcImageProvider.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 8013fed9..d386efbf 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -231,6 +231,7 @@ set(SRC_FILES src/Logging.cpp src/MainWindow.cpp src/MatrixClient.cpp + src/MxcImageProvider.cpp src/QuickSwitcher.cpp src/Olm.cpp src/RegisterPage.cpp diff --git a/resources/qml/Avatar.qml b/resources/qml/Avatar.qml new file mode 100644 index 00000000..9d7b54fe --- /dev/null +++ b/resources/qml/Avatar.qml @@ -0,0 +1,45 @@ +import QtQuick 2.6 +import QtGraphicalEffects 1.0 +import Qt.labs.settings 1.0 + +Rectangle { + id: avatar + width: 48 + height: 48 + radius: settings.avatar_circles ? height/2 : 3 + + Settings { + id: settings + category: "user" + property bool avatar_circles: true + } + + property alias url: img.source + property string displayName + + Text { + anchors.fill: parent + text: String.fromCodePoint(displayName.codePointAt(0)) + color: colors.text + font.pixelSize: avatar.height/2 + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignHCenter + } + + Image { + id: img + anchors.fill: parent + asynchronous: true + + layer.enabled: true + layer.effect: OpacityMask { + maskSource: Rectangle { + anchors.fill: parent + width: avatar.width + height: avatar.height + radius: settings.avatar_circles ? height/2 : 3 + } + } + } + color: colors.dark +} diff --git a/resources/qml/TimelineView.qml b/resources/qml/TimelineView.qml index 5f068e57..0151686a 100644 --- a/resources/qml/TimelineView.qml +++ b/resources/qml/TimelineView.qml @@ -181,10 +181,11 @@ Rectangle { Row { height: userName.height spacing: 4 - Rectangle { + Avatar { width: 48 height: 48 - color: "green" + url: chat.model.avatarUrl(section.split(" ")[0]).replace("mxc://", "image://MxcImage/") + displayName: chat.model.displayName(section.split(" ")[0]) } Text { diff --git a/resources/res.qrc b/resources/res.qrc index b18835fb..6f6d480a 100644 --- a/resources/res.qrc +++ b/resources/res.qrc @@ -116,6 +116,7 @@ qml/TimelineView.qml + qml/Avatar.qml qml/delegates/TextMessage.qml qml/delegates/NoticeMessage.qml diff --git a/src/MxcImageProvider.cpp b/src/MxcImageProvider.cpp new file mode 100644 index 00000000..305439fc --- /dev/null +++ b/src/MxcImageProvider.cpp @@ -0,0 +1,79 @@ +#include "MxcImageProvider.h" + +#include "Cache.h" + +void +MxcImageResponse::run() +{ + if (m_requestedSize.isValid()) { + QString fileName = QString("%1_%2x%3") + .arg(m_id) + .arg(m_requestedSize.width()) + .arg(m_requestedSize.height()); + + auto data = cache::client()->image(fileName); + if (!data.isNull() && m_image.loadFromData(data)) { + m_image = m_image.scaled(m_requestedSize, Qt::KeepAspectRatio); + m_image.setText("mxc url", "mxc://" + m_id); + emit finished(); + return; + } + + mtx::http::ThumbOpts opts; + opts.mxc_url = "mxc://" + m_id.toStdString(); + opts.width = m_requestedSize.width() > 0 ? m_requestedSize.width() : -1; + opts.height = m_requestedSize.height() > 0 ? m_requestedSize.height() : -1; + opts.method = "scale"; + http::client()->get_thumbnail( + opts, [this, fileName](const std::string &res, mtx::http::RequestErr err) { + if (err) { + nhlog::net()->error("Failed to download image {}", + m_id.toStdString()); + m_error = "Failed download"; + emit finished(); + + return; + } + + auto data = QByteArray(res.data(), res.size()); + cache::client()->saveImage(fileName, data); + m_image.loadFromData(data); + m_image = m_image.scaled(m_requestedSize, Qt::KeepAspectRatio); + m_image.setText("mxc url", "mxc://" + m_id); + + emit finished(); + }); + } else { + auto data = cache::client()->image(m_id); + if (!data.isNull() && m_image.loadFromData(data)) { + m_image.setText("mxc url", "mxc://" + m_id); + emit finished(); + return; + } + + http::client()->download( + "mxc://" + m_id.toStdString(), + [this](const std::string &res, + const std::string &, + const std::string &originalFilename, + mtx::http::RequestErr err) { + if (err) { + nhlog::net()->error("Failed to download image {}", + m_id.toStdString()); + m_error = "Failed download"; + emit finished(); + + return; + } + + auto data = QByteArray(res.data(), res.size()); + m_image.loadFromData(data); + m_image.setText("original filename", + QString::fromStdString(originalFilename)); + m_image.setText("mxc url", "mxc://" + m_id); + cache::client()->saveImage(m_id, data); + + emit finished(); + }); + } +} diff --git a/src/MxcImageProvider.h b/src/MxcImageProvider.h new file mode 100644 index 00000000..8710171c --- /dev/null +++ b/src/MxcImageProvider.h @@ -0,0 +1,48 @@ +#pragma once + +#include +#include + +#include +#include + +class MxcImageResponse + : public QQuickImageResponse + , public QRunnable +{ +public: + MxcImageResponse(const QString &id, const QSize &requestedSize) + : m_id(id) + , m_requestedSize(requestedSize) + { + setAutoDelete(false); + } + + QQuickTextureFactory *textureFactory() const override + { + return QQuickTextureFactory::textureFactoryForImage(m_image); + } + QString errorString() const override { return m_error; } + + void run() override; + + QString m_id, m_error; + QSize m_requestedSize; + QImage m_image; +}; + +class MxcImageProvider : public QQuickAsyncImageProvider +{ +public: + QQuickImageResponse *requestImageResponse(const QString &id, + const QSize &requestedSize) override + { + MxcImageResponse *response = new MxcImageResponse(id, requestedSize); + pool.start(response); + return response; + } + +private: + QThreadPool pool; +}; + diff --git a/src/RoomInfoListItem.cpp b/src/RoomInfoListItem.cpp index 8aadbea2..f135451c 100644 --- a/src/RoomInfoListItem.cpp +++ b/src/RoomInfoListItem.cpp @@ -142,7 +142,7 @@ RoomInfoListItem::resizeEvent(QResizeEvent *) void RoomInfoListItem::paintEvent(QPaintEvent *event) { - bool rounded = QSettings().value("user/avatar/circles", true).toBool(); + bool rounded = QSettings().value("user/avatar_circles", true).toBool(); Q_UNUSED(event); diff --git a/src/UserSettingsPage.cpp b/src/UserSettingsPage.cpp index 9fd033e9..1caea449 100644 --- a/src/UserSettingsPage.cpp +++ b/src/UserSettingsPage.cpp @@ -53,7 +53,7 @@ UserSettings::load() isReadReceiptsEnabled_ = settings.value("user/read_receipts", true).toBool(); theme_ = settings.value("user/theme", defaultTheme_).toString(); font_ = settings.value("user/font_family", "default").toString(); - avatarCircles_ = settings.value("user/avatar/circles", true).toBool(); + avatarCircles_ = settings.value("user/avatar_circles", true).toBool(); emojiFont_ = settings.value("user/emoji_font_family", "default").toString(); baseFontSize_ = settings.value("user/font_size", QFont().pointSizeF()).toDouble(); @@ -119,9 +119,7 @@ UserSettings::save() settings.setValue("start_in_tray", isStartInTrayEnabled_); settings.endGroup(); - settings.beginGroup("avatar"); - settings.setValue("circles", avatarCircles_); - settings.endGroup(); + settings.setValue("avatar_circles", avatarCircles_); settings.setValue("font_size", baseFontSize_); settings.setValue("typing_notifications", isTypingNotificationsEnabled_); diff --git a/src/timeline2/TimelineModel.cpp b/src/timeline2/TimelineModel.cpp index 28820205..310494b4 100644 --- a/src/timeline2/TimelineModel.cpp +++ b/src/timeline2/TimelineModel.cpp @@ -325,6 +325,12 @@ TimelineModel::displayName(QString id) const return Cache::displayName(room_id_, id); } +QString +TimelineModel::avatarUrl(QString id) const +{ + return Cache::avatarUrl(room_id_, id); +} + QString TimelineModel::formatDateSeparator(QDate date) const { diff --git a/src/timeline2/TimelineModel.h b/src/timeline2/TimelineModel.h index e37c6542..954da5eb 100644 --- a/src/timeline2/TimelineModel.h +++ b/src/timeline2/TimelineModel.h @@ -90,6 +90,7 @@ public: Q_INVOKABLE QColor userColor(QString id, QColor background); Q_INVOKABLE QString displayName(QString id) const; + Q_INVOKABLE QString avatarUrl(QString id) const; Q_INVOKABLE QString formatDateSeparator(QDate date) const; Q_INVOKABLE QString escapeEmoji(QString str) const; diff --git a/src/timeline2/TimelineViewManager.cpp b/src/timeline2/TimelineViewManager.cpp index 0e0e74e4..eb9bea54 100644 --- a/src/timeline2/TimelineViewManager.cpp +++ b/src/timeline2/TimelineViewManager.cpp @@ -4,6 +4,7 @@ #include #include "Logging.h" +#include "MxcImageProvider.h" TimelineViewManager::TimelineViewManager(QWidget *parent) { @@ -18,6 +19,7 @@ TimelineViewManager::TimelineViewManager(QWidget *parent) container = QWidget::createWindowContainer(view, parent); container->setMinimumSize(200, 200); view->rootContext()->setContextProperty("timelineManager", this); + view->engine()->addImageProvider("MxcImage", new MxcImageProvider()); view->setSource(QUrl("qrc:///qml/TimelineView.qml")); } diff --git a/src/ui/Avatar.cpp b/src/ui/Avatar.cpp index 501a8968..e4a90f81 100644 --- a/src/ui/Avatar.cpp +++ b/src/ui/Avatar.cpp @@ -101,7 +101,7 @@ Avatar::setIcon(const QIcon &icon) void Avatar::paintEvent(QPaintEvent *) { - bool rounded = QSettings().value("user/avatar/circles", true).toBool(); + bool rounded = QSettings().value("user/avatar_circles", true).toBool(); QPainter painter(this); painter.setRenderHint(QPainter::Antialiasing);