diff --git a/CMakeLists.txt b/CMakeLists.txt index 6a98bc1c..3fbd8069 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -310,7 +310,6 @@ set(SRC_FILES # Dialogs src/dialogs/CreateRoom.cpp src/dialogs/FallbackAuth.cpp - src/dialogs/ImageOverlay.cpp src/dialogs/PreviewUploadOverlay.cpp src/dialogs/ReCaptcha.cpp @@ -519,7 +518,6 @@ qt5_wrap_cpp(MOC_HEADERS # Dialogs src/dialogs/CreateRoom.h src/dialogs/FallbackAuth.h - src/dialogs/ImageOverlay.h src/dialogs/PreviewUploadOverlay.h src/dialogs/ReCaptcha.h diff --git a/resources/qml/Root.qml b/resources/qml/Root.qml index 85ae783d..c85b641a 100644 --- a/resources/qml/Root.qml +++ b/resources/qml/Root.qml @@ -136,6 +136,14 @@ Page { } + Component { + id: imageOverlay + + ImageOverlay { + } + + } + Shortcut { sequence: "Ctrl+K" onActivated: { @@ -234,6 +242,15 @@ Page { dialog.open(); } + function onShowImageOverlay(room, eventId, url, proportionalHeight, originalWidth) { + var dialog = imageOverlay.createObject(timelineRoot, { + "room": room, + "eventId": eventId, + "url": url + }); + dialog.showFullScreen(); + } + target: TimelineManager } diff --git a/resources/qml/delegates/ImageMessage.qml b/resources/qml/delegates/ImageMessage.qml index 5e04b8fd..0bbb43cf 100644 --- a/resources/qml/delegates/ImageMessage.qml +++ b/resources/qml/delegates/ImageMessage.qml @@ -63,10 +63,9 @@ Item { } TapHandler { - // TODO(Nico): Replace this with a qml thingy, that also can show animated images - enabled: type == MtxEvent.ImageMessage && (img.status == Image.Ready || mxcimage.loaded) + //enabled: type == MtxEvent.ImageMessage && (img.status == Image.Ready || mxcimage.loaded) onSingleTapped: { - TimelineManager.openImageOverlay(url, room.data.eventId); + TimelineManager.openImageOverlay(room, url, eventId); eventPoint.accepted = true; } gesturePolicy: TapHandler.ReleaseWithinBounds diff --git a/resources/qml/dialogs/ImageOverlay.qml b/resources/qml/dialogs/ImageOverlay.qml new file mode 100644 index 00000000..246a0bab --- /dev/null +++ b/resources/qml/dialogs/ImageOverlay.qml @@ -0,0 +1,110 @@ +// SPDX-FileCopyrightText: 2022 Nheko Contributors +// +// SPDX-License-Identifier: GPL-3.0-or-later + +import QtQuick 2.15 +import QtQuick.Window 2.15 + +import ".." + +import im.nheko 1.0 + +Window { + id: imageOverlay + + required property string url + required property string eventId + required property Room room + + flags: Qt.FramelessWindowHint + + visibility: Window.FullScreen + color: Qt.rgba(0.2,0.2,0.2,0.66) + + Shortcut { + sequence: StandardKey.Cancel + onActivated: imageOverlay.close() + } + + + Item { + height: Math.min(parent.height, img.implicitHeight) + width: Math.min(parent.width, img.implicitWidth) + x: (parent.width - img.width)/2 + y: (parent.height - img.height)/2 + + Image { + id: img + + visible: !mxcimage.loaded + anchors.fill: parent + source: url.replace("mxc://", "image://MxcImage/") + asynchronous: true + fillMode: Image.PreserveAspectFit + smooth: true + mipmap: true + } + + MxcAnimatedImage { + id: mxcimage + + visible: loaded + anchors.fill: parent + roomm: imageOverlay.room + play: !Settings.animateImagesOnHover || mouseArea.hovered + eventId: imageOverlay.eventId + } + + PinchHandler { + } + + WheelHandler { + property: "scale" + } + + DragHandler { + } + + HoverHandler { + id: mouseArea + } + } + + + + Row { + anchors.top: parent.top + anchors.right: parent.right + anchors.margins: Nheko.paddingLarge + spacing: Nheko.paddingMedium + + ImageButton { + height: 48 + width: 48 + hoverEnabled: true + image: ":/icons/icons/ui/download.svg" + //ToolTip.visible: hovered + //ToolTip.delay: Nheko.tooltipDelay + //ToolTip.text: qsTr("Download") + onClicked: { + if (room) { + room.saveMedia(eventId); + } else { + TimelineManager.saveMedia(url); + } + imageOverlay.close(); + } + } + ImageButton { + height: 48 + width: 48 + hoverEnabled: true + image: ":/icons/icons/ui/dismiss.svg" + //ToolTip.visible: hovered + //ToolTip.delay: Nheko.tooltipDelay + //ToolTip.text: qsTr("Close") + onClicked: imageOverlay.close() + } + } + +} diff --git a/resources/qml/dialogs/UserProfile.qml b/resources/qml/dialogs/UserProfile.qml index 04f21f55..29ce2c3f 100644 --- a/resources/qml/dialogs/UserProfile.qml +++ b/resources/qml/dialogs/UserProfile.qml @@ -70,7 +70,7 @@ ApplicationWindow { displayName: profile.displayName userid: profile.userid Layout.alignment: Qt.AlignHCenter - onClicked: TimelineManager.openImageOverlay(profile.avatarUrl, "") + onClicked: TimelineManager.openImageOverlay(null, profile.avatarUrl, "") ImageButton { hoverEnabled: true diff --git a/resources/qml/voip/ActiveCallBar.qml b/resources/qml/voip/ActiveCallBar.qml index 677fe40b..a8a65421 100644 --- a/resources/qml/voip/ActiveCallBar.qml +++ b/resources/qml/voip/ActiveCallBar.qml @@ -37,7 +37,7 @@ Rectangle { url: CallManager.callPartyAvatarUrl.replace("mxc://", "image://MxcImage/") userid: CallManager.callParty displayName: CallManager.callPartyDisplayName - onClicked: TimelineManager.openImageOverlay(room.avatarUrl(userid), room.data.eventId) + onClicked: TimelineManager.openImageOverlay(room, room.avatarUrl(userid), room.data.eventId) } Label { diff --git a/resources/qml/voip/CallInviteBar.qml b/resources/qml/voip/CallInviteBar.qml index e7247b4f..b2c2dbad 100644 --- a/resources/qml/voip/CallInviteBar.qml +++ b/resources/qml/voip/CallInviteBar.qml @@ -44,7 +44,7 @@ Rectangle { url: CallManager.callPartyAvatarUrl.replace("mxc://", "image://MxcImage/") userid: CallManager.callParty displayName: CallManager.callPartyDisplayName - onClicked: TimelineManager.openImageOverlay(room.avatarUrl(userid), room.data.eventId) + onClicked: TimelineManager.openImageOverlay(room, room.avatarUrl(userid), room.data.eventId) } Label { diff --git a/resources/qml/voip/PlaceCall.qml b/resources/qml/voip/PlaceCall.qml index 632c0496..1404adf9 100644 --- a/resources/qml/voip/PlaceCall.qml +++ b/resources/qml/voip/PlaceCall.qml @@ -81,7 +81,7 @@ Popup { url: room.roomAvatarUrl.replace("mxc://", "image://MxcImage/") displayName: room.roomName roomid: room.roomid - onClicked: TimelineManager.openImageOverlay(room.avatarUrl(userid), room.data.eventId) + onClicked: TimelineManager.openImageOverlay(room, room.avatarUrl(userid), room.data.eventId) } Button { diff --git a/resources/res.qrc b/resources/res.qrc index bc3a8bd2..d825699a 100644 --- a/resources/res.qrc +++ b/resources/res.qrc @@ -133,6 +133,7 @@ qml/device-verification/NewVerificationRequest.qml qml/device-verification/Success.qml qml/device-verification/Waiting.qml + qml/dialogs/ImageOverlay.qml qml/dialogs/ImagePackEditorDialog.qml qml/dialogs/ImagePackSettingsDialog.qml qml/dialogs/PhoneNumberInputDialog.qml diff --git a/src/dialogs/ImageOverlay.cpp b/src/dialogs/ImageOverlay.cpp deleted file mode 100644 index d9f0a993..00000000 --- a/src/dialogs/ImageOverlay.cpp +++ /dev/null @@ -1,102 +0,0 @@ -// SPDX-FileCopyrightText: 2017 Konstantinos Sideris -// SPDX-FileCopyrightText: 2021 Nheko Contributors -// SPDX-FileCopyrightText: 2022 Nheko Contributors -// -// SPDX-License-Identifier: GPL-3.0-or-later - -#include -#include -#include -#include - -#include "dialogs/ImageOverlay.h" - -#include "Utils.h" - -using namespace dialogs; - -ImageOverlay::ImageOverlay(const QPixmap &image, QWidget *parent) - : QWidget{parent} - , originalImage_{image} -{ - setMouseTracking(true); - setParent(nullptr); - - setWindowFlags(windowFlags() | Qt::FramelessWindowHint); - setWindowRole(QStringLiteral("imageoverlay")); - - setAttribute(Qt::WA_NoSystemBackground, true); - setAttribute(Qt::WA_TranslucentBackground, true); - setAttribute(Qt::WA_DeleteOnClose, true); - setWindowState(Qt::WindowFullScreen); - close_shortcut_ = new QShortcut(QKeySequence(Qt::Key_Escape), this); - - connect(close_shortcut_, &QShortcut::activated, this, &ImageOverlay::closing); - connect(this, &ImageOverlay::closing, this, &ImageOverlay::close); - - raise(); -} - -void -ImageOverlay::paintEvent(QPaintEvent *event) -{ - Q_UNUSED(event); - - QPainter painter(this); - painter.setRenderHint(QPainter::Antialiasing); - - // Full screen overlay. - painter.fillRect(QRect(0, 0, width(), height()), QColor(55, 55, 55, 170)); - - // Left and Right margins - int outer_margin = width() * 0.12; - int buttonSize = 36; - int margin = outer_margin * 0.1; - - int max_width = width() - 2 * outer_margin; - int max_height = height(); - - image_ = utils::scaleDown(max_width, max_height, originalImage_); - - int diff_x = max_width - image_.width(); - int diff_y = max_height - image_.height(); - - content_ = QRect(outer_margin + diff_x / 2, diff_y / 2, image_.width(), image_.height()); - close_button_ = QRect(width() - margin - buttonSize, margin, buttonSize, buttonSize); - save_button_ = QRect(width() - (2 * margin) - (2 * buttonSize), margin, buttonSize, buttonSize); - - // Draw main content_. - painter.drawPixmap(content_, image_); - - // Draw top right corner X. - QPen pen; - pen.setCapStyle(Qt::RoundCap); - pen.setWidthF(5); - pen.setColor("gray"); - - auto center = close_button_.center(); - - painter.setPen(pen); - painter.drawLine(center - QPointF(15, 15), center + QPointF(15, 15)); - painter.drawLine(center + QPointF(15, -15), center - QPointF(15, -15)); - - // Draw download button - center = save_button_.center(); - painter.drawLine(center - QPointF(0, 15), center + QPointF(0, 15)); - painter.drawLine(center - QPointF(15, 0), center + QPointF(0, 15)); - painter.drawLine(center + QPointF(0, 15), center + QPointF(15, 0)); -} - -void -ImageOverlay::mousePressEvent(QMouseEvent *event) -{ - if (event->button() != Qt::LeftButton) - return; - - if (close_button_.contains(event->pos())) - emit closing(); - else if (save_button_.contains(event->pos())) - emit saving(); - else if (!content_.contains(event->pos())) - emit closing(); -} diff --git a/src/dialogs/ImageOverlay.h b/src/dialogs/ImageOverlay.h deleted file mode 100644 index 9ce2fb09..00000000 --- a/src/dialogs/ImageOverlay.h +++ /dev/null @@ -1,39 +0,0 @@ -// SPDX-FileCopyrightText: 2017 Konstantinos Sideris -// SPDX-FileCopyrightText: 2021 Nheko Contributors -// SPDX-FileCopyrightText: 2022 Nheko Contributors -// -// SPDX-License-Identifier: GPL-3.0-or-later - -#pragma once - -#include -#include -#include -#include - -namespace dialogs { - -class ImageOverlay : public QWidget -{ - Q_OBJECT -public: - ImageOverlay(const QPixmap &image, QWidget *parent = nullptr); - -protected: - void mousePressEvent(QMouseEvent *event) override; - void paintEvent(QPaintEvent *event) override; - -signals: - void closing(); - void saving(); - -private: - QPixmap originalImage_; - QPixmap image_; - - QRect content_; - QRect close_button_; - QRect save_button_; - QShortcut *close_shortcut_; -}; -} // dialogs diff --git a/src/timeline/TimelineViewManager.cpp b/src/timeline/TimelineViewManager.cpp index 6195c24a..7234caa9 100644 --- a/src/timeline/TimelineViewManager.cpp +++ b/src/timeline/TimelineViewManager.cpp @@ -6,10 +6,12 @@ #include "TimelineViewManager.h" #include +#include #include #include #include #include +#include #include #include "BlurhashProvider.h" @@ -32,7 +34,6 @@ #include "SingleImagePackModel.h" #include "UserSettingsPage.h" #include "UsersModel.h" -#include "dialogs/ImageOverlay.h" #include "emoji/EmojiModel.h" #include "emoji/Provider.h" #include "encryption/DeviceVerificationFlow.h" @@ -331,11 +332,6 @@ TimelineViewManager::TimelineViewManager(CallManager *callManager, ChatPage *par isInitialSync_ = true; emit initialSyncChanged(true); }); - - connect(this, - &TimelineViewManager::openImageOverlayInternalCb, - this, - &TimelineViewManager::openImageOverlayInternal); } void @@ -416,23 +412,13 @@ TimelineViewManager::escapeEmoji(QString str) const } void -TimelineViewManager::openImageOverlay(QString mxcUrl, QString eventId) +TimelineViewManager::openImageOverlay(TimelineModel *room, QString mxcUrl, QString eventId) { if (mxcUrl.isEmpty()) { return; } - MxcImageProvider::download(mxcUrl.remove(QStringLiteral("mxc://")), - QSize(), - [this, eventId](QString, QSize, QImage img, QString) { - if (img.isNull()) { - nhlog::ui()->error( - "Error when retrieving image for overlay."); - return; - } - - emit openImageOverlayInternalCb(eventId, std::move(img)); - }); + emit showImageOverlay(room, eventId, mxcUrl); } void @@ -443,25 +429,46 @@ TimelineViewManager::openImagePackSettings(QString roomid) } void -TimelineViewManager::openImageOverlayInternal(QString eventId, QImage img) +TimelineViewManager::saveMedia(QString mxcUrl) { - auto pixmap = QPixmap::fromImage(img); + const QString downloadsFolder = + QStandardPaths::writableLocation(QStandardPaths::DownloadLocation); + const QString openLocation = downloadsFolder + "/" + mxcUrl.splitRef(u'/').constLast(); - auto imgDialog = new dialogs::ImageOverlay(pixmap); - imgDialog->showFullScreen(); + const QString filename = QFileDialog::getSaveFileName(getWidget(), {}, openLocation); - auto room = rooms_->currentRoom(); - connect(imgDialog, &dialogs::ImageOverlay::saving, room, [eventId, imgDialog, room]() { - // hide the overlay while presenting the save dialog for better - // cross platform support. - imgDialog->hide(); + if (filename.isEmpty()) + return; - if (!room->saveMedia(eventId)) { - imgDialog->show(); - } else { - imgDialog->close(); - } - }); + const auto url = mxcUrl.toStdString(); + + http::client()->download(url, + [filename, url](const std::string &data, + const std::string &, + const std::string &, + mtx::http::RequestErr err) { + if (err) { + nhlog::net()->warn("failed to retrieve image {}: {} {}", + url, + err->matrix_error.error, + static_cast(err->status_code)); + return; + } + + try { + QFile file(filename); + + if (!file.open(QIODevice::WriteOnly)) + return; + + file.write(QByteArray(data.data(), (int)data.size())); + file.close(); + + return; + } catch (const std::exception &e) { + nhlog::ui()->warn("Error while saving file to: {}", e.what()); + } + }); } void diff --git a/src/timeline/TimelineViewManager.h b/src/timeline/TimelineViewManager.h index 47a72412..455702f4 100644 --- a/src/timeline/TimelineViewManager.h +++ b/src/timeline/TimelineViewManager.h @@ -60,8 +60,9 @@ public: Q_INVOKABLE bool isInitialSync() const { return isInitialSync_; } bool isWindowFocused() const { return isWindowFocused_; } - Q_INVOKABLE void openImageOverlay(QString mxcUrl, QString eventId); + Q_INVOKABLE void openImageOverlay(TimelineModel *room, QString mxcUrl, QString eventId); Q_INVOKABLE void openImagePackSettings(QString roomid); + Q_INVOKABLE void saveMedia(QString mxcUrl); Q_INVOKABLE QColor userColor(QString id, QColor background); Q_INVOKABLE QString escapeEmoji(QString str) const; Q_INVOKABLE QString htmlEscape(QString str) const { return str.toHtmlEscaped(); } @@ -85,13 +86,13 @@ signals: void narrowViewChanged(); void focusChanged(); void focusInput(); - void openImageOverlayInternalCb(QString eventId, QImage img); void openRoomMembersDialog(MemberList *members, TimelineModel *room); void openRoomSettingsDialog(RoomSettings *settings); void openInviteUsersDialog(InviteesModel *invitees); void openProfile(UserProfile *profile); void showImagePackSettings(TimelineModel *room, ImagePackListModel *packlist); void openLeaveRoomDialog(QString roomid); + void showImageOverlay(TimelineModel *room, QString eventId, QString url); public slots: void updateReadReceipts(const QString &room_id, const std::vector &event_ids); @@ -120,9 +121,6 @@ public slots: RoomlistModel *rooms() { return rooms_; } -private slots: - void openImageOverlayInternal(QString eventId, QImage img); - private: #ifdef USE_QUICK_VIEW QQuickView *view; diff --git a/src/ui/MxcAnimatedImage.cpp b/src/ui/MxcAnimatedImage.cpp index 36e028e3..83787fff 100644 --- a/src/ui/MxcAnimatedImage.cpp +++ b/src/ui/MxcAnimatedImage.cpp @@ -20,10 +20,12 @@ void MxcAnimatedImage::startDownload() { + nhlog::ui()->debug("START DOWNLOAD!!!"); if (!room_) return; if (eventId_.isEmpty()) return; + nhlog::ui()->debug("START DOWNLOAD2!!!"); auto event = room_->eventById(eventId_); if (!event) { @@ -92,7 +94,9 @@ MxcAnimatedImage::startDownload() "Playing movie with size: {}, {}", buffer.bytesAvailable(), buffer.isOpen()); movie.setFormat(mimeType); movie.setDevice(&buffer); - movie.setScaledSize(this->size().toSize()); + + if (height() != 0 && width() != 0) + movie.setScaledSize(this->size().toSize()); if (buffer.bytesAvailable() < 4LL * 1024 * 1024 * 1024) // cache images smaller than 4MB in RAM movie.setCacheMode(QMovie::CacheAll); @@ -145,6 +149,17 @@ MxcAnimatedImage::startDownload() }); } +void +MxcAnimatedImage::geometryChanged(const QRectF &newGeometry, const QRectF &oldGeometry) +{ + QQuickItem::geometryChanged(newGeometry, oldGeometry); + + if (newGeometry.size() != oldGeometry.size()) { + if (height() != 0 && width() != 0) + movie.setScaledSize(newGeometry.size().toSize()); + } +} + QSGNode * MxcAnimatedImage::updatePaintNode(QSGNode *oldNode, QQuickItem::UpdatePaintNodeData *) { @@ -171,8 +186,8 @@ MxcAnimatedImage::updatePaintNode(QSGNode *oldNode, QQuickItem::UpdatePaintNodeD return nullptr; } - n->setRect(QRect(0, 0, width(), height())); - n->setFiltering(QSGTexture::Nearest); + n->setRect(0, 0, width(), height()); + n->setFiltering(QSGTexture::Linear); n->setMipmapFiltering(QSGTexture::None); return n; diff --git a/src/ui/MxcAnimatedImage.h b/src/ui/MxcAnimatedImage.h index 1afe6a19..8891e57e 100644 --- a/src/ui/MxcAnimatedImage.h +++ b/src/ui/MxcAnimatedImage.h @@ -60,6 +60,7 @@ public: } } + void geometryChanged(const QRectF &newGeometry, const QRectF &oldGeometry) override; QSGNode *updatePaintNode(QSGNode *oldNode, QQuickItem::UpdatePaintNodeData *updatePaintNodeData) override;