Port image overlay to qml

Allows you to zoom and pan now.

relates to #647
This commit is contained in:
Nicolas Werner 2022-01-02 21:46:29 +01:00
parent c3e2e73175
commit 66520eae19
No known key found for this signature in database
GPG Key ID: C8D75E610773F2D9
15 changed files with 196 additions and 191 deletions

View File

@ -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

View File

@ -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
}

View File

@ -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

View File

@ -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()
}
}
}

View File

@ -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

View File

@ -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 {

View File

@ -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 {

View File

@ -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 {

View File

@ -133,6 +133,7 @@
<file>qml/device-verification/NewVerificationRequest.qml</file>
<file>qml/device-verification/Success.qml</file>
<file>qml/device-verification/Waiting.qml</file>
<file>qml/dialogs/ImageOverlay.qml</file>
<file>qml/dialogs/ImagePackEditorDialog.qml</file>
<file>qml/dialogs/ImagePackSettingsDialog.qml</file>
<file>qml/dialogs/PhoneNumberInputDialog.qml</file>

View File

@ -1,102 +0,0 @@
// SPDX-FileCopyrightText: 2017 Konstantinos Sideris <siderisk@auth.gr>
// SPDX-FileCopyrightText: 2021 Nheko Contributors
// SPDX-FileCopyrightText: 2022 Nheko Contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later
#include <QApplication>
#include <QGuiApplication>
#include <QPainter>
#include <QScreen>
#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();
}

View File

@ -1,39 +0,0 @@
// SPDX-FileCopyrightText: 2017 Konstantinos Sideris <siderisk@auth.gr>
// SPDX-FileCopyrightText: 2021 Nheko Contributors
// SPDX-FileCopyrightText: 2022 Nheko Contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <QDialog>
#include <QMouseEvent>
#include <QPixmap>
#include <QShortcut>
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

View File

@ -6,10 +6,12 @@
#include "TimelineViewManager.h"
#include <QDropEvent>
#include <QFileDialog>
#include <QMetaType>
#include <QPalette>
#include <QQmlContext>
#include <QQmlEngine>
#include <QStandardPaths>
#include <QString>
#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<int>(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

View File

@ -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<QString> &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;

View File

@ -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;

View File

@ -60,6 +60,7 @@ public:
}
}
void geometryChanged(const QRectF &newGeometry, const QRectF &oldGeometry) override;
QSGNode *updatePaintNode(QSGNode *oldNode,
QQuickItem::UpdatePaintNodeData *updatePaintNodeData) override;