1
0
mirror of https://github.com/qTox/qTox.git synced 2024-03-22 14:00:36 +08:00

feat(chatlog): Add image preview on paste

Reuse code from the file transfer widget to provide an image preview
on paste/grab. This prevents users from accidentally sending images they
did not mean to when their clipboard is not in the state they thought it
was.

Implementation exposes the genericchatlog vbox to the child classes and
chatform injects the imagepreview into it.
This commit is contained in:
Mick Sayson 2020-05-16 13:22:36 -07:00 committed by Anthony Bilinski
parent 8462207a58
commit 7c218b389d
No known key found for this signature in database
GPG Key ID: 2AA8E0DA1B31FB3C
12 changed files with 232 additions and 81 deletions

View File

@ -373,6 +373,8 @@ set(${PROJECT_NAME}_SOURCES
src/widget/extensionstatus.h
src/widget/flowlayout.cpp
src/widget/flowlayout.h
src/widget/imagepreviewwidget.h
src/widget/imagepreviewwidget.cpp
src/widget/searchform.cpp
src/widget/searchform.h
src/widget/searchtypes.h

View File

@ -500,49 +500,8 @@ void FileTransferWidget::handleButton(QPushButton* btn)
void FileTransferWidget::showPreview(const QString& filename)
{
static const QStringList previewExtensions = {"png", "jpeg", "jpg", "gif", "svg",
"PNG", "JPEG", "JPG", "GIF", "SVG"};
if (previewExtensions.contains(QFileInfo(filename).suffix())) {
// Subtract to make border visible
const int size = qMax(ui->previewButton->width(), ui->previewButton->height()) - 4;
QFile imageFile(filename);
if (!imageFile.open(QIODevice::ReadOnly)) {
return;
}
const QByteArray imageFileData = imageFile.readAll();
QImage image = QImage::fromData(imageFileData);
auto orientation = ExifTransform::getOrientation(imageFileData);
image = ExifTransform::applyTransformation(image, orientation);
const QPixmap iconPixmap = scaleCropIntoSquare(QPixmap::fromImage(image), size);
ui->previewButton->setIcon(QIcon(iconPixmap));
ui->previewButton->setIconSize(iconPixmap.size());
ui->previewButton->show();
// Show mouseover preview, but make sure it's not larger than 50% of the screen
// width/height
const QRect desktopSize = QApplication::desktop()->geometry();
const int maxPreviewWidth{desktopSize.width() / 2};
const int maxPreviewHeight{desktopSize.height() / 2};
const QImage previewImage = [&image, maxPreviewWidth, maxPreviewHeight]() {
if (image.width() > maxPreviewWidth || image.height() > maxPreviewHeight) {
return image.scaled(maxPreviewWidth, maxPreviewHeight, Qt::KeepAspectRatio,
Qt::SmoothTransformation);
} else {
return image;
}
}();
QByteArray imageData;
QBuffer buffer(&imageData);
buffer.open(QIODevice::WriteOnly);
previewImage.save(&buffer, "PNG");
buffer.close();
ui->previewButton->setToolTip("<img src=data:image/png;base64," + imageData.toBase64() + "/>");
}
ui->previewButton->setIconFromFile(filename);
ui->previewButton->show();
}
void FileTransferWidget::onLeftButtonClicked()
@ -560,31 +519,6 @@ void FileTransferWidget::onPreviewButtonClicked()
handleButton(ui->previewButton);
}
QPixmap FileTransferWidget::scaleCropIntoSquare(const QPixmap& source, const int targetSize)
{
QPixmap result;
// Make sure smaller-than-icon images (at least one dimension is smaller) will not be
// upscaled
if (source.width() < targetSize || source.height() < targetSize) {
result = source;
} else {
result = source.scaled(targetSize, targetSize, Qt::KeepAspectRatioByExpanding,
Qt::SmoothTransformation);
}
// Then, image has to be cropped (if needed) so it will not overflow rectangle
// Only one dimension will be bigger after Qt::KeepAspectRatioByExpanding
if (result.width() > targetSize) {
return result.copy((result.width() - targetSize) / 2, 0, targetSize, targetSize);
} else if (result.height() > targetSize) {
return result.copy(0, (result.height() - targetSize) / 2, targetSize, targetSize);
}
// Picture was rectangle in the first place, no cropping
return result;
}
void FileTransferWidget::updateWidget(ToxFile const& file)
{
assert(file == fileInfo);

View File

@ -73,9 +73,6 @@ private slots:
void onPreviewButtonClicked();
private:
static QPixmap scaleCropIntoSquare(const QPixmap& source, int targetSize);
static int getExifOrientation(const char* data, const int size);
static void applyTransformation(const int oritentation, QImage& image);
static bool tryRemoveFile(const QString &filepath);
void updateWidget(ToxFile const& file);

View File

@ -270,7 +270,7 @@
</layout>
</item>
<item row="1" column="0">
<widget class="QPushButton" name="previewButton">
<widget class="ImagePreviewButton" name="previewButton">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
@ -296,7 +296,7 @@
<string notr="true">QPushButton{ border: 2px solid white }</string>
</property>
<property name="icon">
<iconset resource="../../../res.qrc">
<iconset>
<normaloff>:themes/default/fileTransferInstance/no.svg</normaloff>:themes/default/fileTransferInstance/no.svg</iconset>
</property>
<property name="iconSize">
@ -382,7 +382,7 @@
<string/>
</property>
<property name="icon">
<iconset resource="../../../res.qrc">
<iconset>
<normaloff>:themes/default/fileTransferInstance/no.svg</normaloff>:themes/default/fileTransferInstance/no.svg</iconset>
</property>
<property name="iconSize">
@ -420,7 +420,7 @@
<string/>
</property>
<property name="icon">
<iconset resource="../../../res.qrc">
<iconset>
<normaloff>:themes/default/fileTransferInstance/no.svg</normaloff>:themes/default/fileTransferInstance/no.svg</iconset>
</property>
<property name="iconSize">
@ -445,6 +445,11 @@
<extends>QLabel</extends>
<header location="global">src/widget/tool/croppinglabel.h</header>
</customwidget>
<customwidget>
<class>ImagePreviewButton</class>
<extends>QPushButton</extends>
<header location="global">src/widget/imagepreviewwidget.h</header>
</customwidget>
</customwidgets>
<tabstops>
<tabstop>previewButton</tabstop>

View File

@ -37,6 +37,7 @@
#include "src/widget/chatformheader.h"
#include "src/widget/contentdialogmanager.h"
#include "src/widget/form/loadhistorydialog.h"
#include "src/widget/imagepreviewwidget.h"
#include "src/widget/maskablepixmapwidget.h"
#include "src/widget/searchform.h"
#include "src/widget/style.h"
@ -134,6 +135,23 @@ ChatForm::ChatForm(Profile& profile, Friend* chatFriend, IChatLog& chatLog, IMes
headWidget->addWidget(callDuration, 1, Qt::AlignCenter);
callDuration->hide();
imagePreview = new ImagePreviewButton(this);
imagePreview->setFixedSize(100, 100);
imagePreview->setFlat(true);
imagePreview->setStyleSheet("QPushButton { border: 0px }");
imagePreview->hide();
auto cancelIcon = QIcon(Style::getImagePath("rejectCall/rejectCall.svg"));
QPushButton* cancelButton = new QPushButton(imagePreview);
cancelButton->setFixedSize(20, 20);
cancelButton->move(QPoint(80, 0));
cancelButton->setIcon(cancelIcon);
cancelButton->setFlat(true);
connect(cancelButton, &QPushButton::pressed, this, &ChatForm::cancelImagePreview);
contentLayout->insertWidget(3, imagePreview);
copyStatusAction = statusMessageMenu.addAction(QString(), this, SLOT(onCopyStatusMessage()));
const CoreFile* coreFile = core.getCoreFile();
@ -155,9 +173,12 @@ ChatForm::ChatForm(Profile& profile, Friend* chatFriend, IChatLog& chatLog, IMes
connect(headWidget, &ChatFormHeader::micMuteToggle, this, &ChatForm::onMicMuteToggle);
connect(headWidget, &ChatFormHeader::volMuteToggle, this, &ChatForm::onVolMuteToggle);
connect(sendButton, &QPushButton::pressed, this, &ChatForm::callUpdateFriendActivity);
connect(sendButton, &QPushButton::pressed, this, &ChatForm::sendImageFromPreview);
connect(msgEdit, &ChatTextEdit::enterPressed, this, &ChatForm::callUpdateFriendActivity);
connect(msgEdit, &ChatTextEdit::textChanged, this, &ChatForm::onTextEditChanged);
connect(msgEdit, &ChatTextEdit::pasteImage, this, &ChatForm::sendImage);
connect(msgEdit, &ChatTextEdit::pasteImage, this, &ChatForm::previewImage);
connect(msgEdit, &ChatTextEdit::enterPressed, this, &ChatForm::sendImageFromPreview);
connect(msgEdit, &ChatTextEdit::escapePressed, this, &ChatForm::cancelImagePreview);
connect(statusMessageLabel, &CroppingLabel::customContextMenuRequested, this,
[&](const QPoint& pos) {
if (!statusMessageLabel->text().isEmpty()) {
@ -561,12 +582,29 @@ void ChatForm::doScreenshot()
{
// note: grabber is self-managed and will destroy itself when done
ScreenshotGrabber* grabber = new ScreenshotGrabber;
connect(grabber, &ScreenshotGrabber::screenshotTaken, this, &ChatForm::sendImage);
connect(grabber, &ScreenshotGrabber::screenshotTaken, this, &ChatForm::previewImage);
grabber->showGrabber();
}
void ChatForm::sendImage(const QPixmap& pixmap)
void ChatForm::previewImage(const QPixmap& pixmap)
{
imagePreviewSource = pixmap;
imagePreview->setIconFromPixmap(pixmap);
imagePreview->show();
}
void ChatForm::cancelImagePreview()
{
imagePreviewSource = QPixmap();
imagePreview->hide();
}
void ChatForm::sendImageFromPreview()
{
if (!imagePreview->isVisible()) {
return;
}
QDir(Settings::getInstance().getPaths().getAppDataDirPath()).mkpath("images");
// use ~ISO 8601 for screenshot timestamp, considering FS limitations
@ -580,8 +618,10 @@ void ChatForm::sendImage(const QPixmap& pixmap)
QFile file(filepath);
if (file.open(QFile::ReadWrite)) {
pixmap.save(&file, "PNG");
imagePreviewSource.save(&file, "PNG");
qint64 filesize = file.size();
imagePreview->hide();
imagePreviewSource = QPixmap();
file.close();
QFileInfo fi(file);
CoreFile* coreFile = core.getCoreFile();

View File

@ -41,6 +41,7 @@ class OfflineMsgEngine;
class QPixmap;
class QHideEvent;
class QMoveEvent;
class ImagePreviewButton;
class ChatForm : public GenericChatForm
{
@ -96,7 +97,9 @@ private slots:
void onFriendNameChanged(const QString& name);
void onStatusMessage(const QString& message);
void onUpdateTime();
void sendImage(const QPixmap& pixmap);
void previewImage(const QPixmap& pixmap);
void cancelImagePreview();
void sendImageFromPreview();
void doScreenshot();
void onCopyStatusMessage();
@ -131,6 +134,8 @@ private:
QTimer typingTimer;
QElapsedTimer timeElapsed;
QAction* copyStatusAction;
QPixmap imagePreviewSource;
ImagePreviewButton* imagePreview;
bool isTyping;
bool lastCallIsVideo;
std::unique_ptr<NetCamView> netcam;

View File

@ -309,7 +309,7 @@ GenericChatForm::GenericChatForm(const Core& _core, const Contact* contact, ICha
mainFootLayout->addWidget(sendButton);
mainFootLayout->setSpacing(0);
QVBoxLayout* contentLayout = new QVBoxLayout(contentWidget);
contentLayout = new QVBoxLayout(contentWidget);
contentLayout->addWidget(searchForm);
contentLayout->addWidget(dateInfo);
contentLayout->addWidget(chatWidget);

View File

@ -168,6 +168,7 @@ protected:
QMenu menu;
QVBoxLayout* contentLayout;
QPushButton* emoteButton;
QPushButton* fileButton;
QPushButton* screenshotButton;

View File

@ -0,0 +1,125 @@
/*
Copyright © 2020 by The qTox Project Contributors
This file is part of qTox, a Qt-based graphical interface for Tox.
qTox is libre software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
qTox is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with qTox. If not, see <http://www.gnu.org/licenses/>.
*/
#include "imagepreviewwidget.h"
#include "src/model/exiftransform.h"
#include <QFile>
#include <QFileInfo>
#include <QString>
#include <QApplication>
#include <QDesktopWidget>
#include <QBuffer>
namespace
{
QPixmap pixmapFromFile(const QString& filename)
{
static const QStringList previewExtensions = {"png", "jpeg", "jpg", "gif", "svg",
"PNG", "JPEG", "JPG", "GIF", "SVG"};
if (!previewExtensions.contains(QFileInfo(filename).suffix())) {
return QPixmap();
}
QFile imageFile(filename);
if (!imageFile.open(QIODevice::ReadOnly)) {
return QPixmap();
}
const QByteArray imageFileData = imageFile.readAll();
QImage image = QImage::fromData(imageFileData);
auto orientation = ExifTransform::getOrientation(imageFileData);
image = ExifTransform::applyTransformation(image, orientation);
return QPixmap::fromImage(image);
}
QPixmap scaleCropIntoSquare(const QPixmap& source, const int targetSize)
{
QPixmap result;
// Make sure smaller-than-icon images (at least one dimension is smaller) will not be
// upscaled
if (source.width() < targetSize || source.height() < targetSize) {
result = source;
} else {
result = source.scaled(targetSize, targetSize, Qt::KeepAspectRatioByExpanding,
Qt::SmoothTransformation);
}
// Then, image has to be cropped (if needed) so it will not overflow rectangle
// Only one dimension will be bigger after Qt::KeepAspectRatioByExpanding
if (result.width() > targetSize) {
return result.copy((result.width() - targetSize) / 2, 0, targetSize, targetSize);
} else if (result.height() > targetSize) {
return result.copy(0, (result.height() - targetSize) / 2, targetSize, targetSize);
}
// Picture was rectangle in the first place, no cropping
return result;
}
QString getToolTipDisplayingImage(const QPixmap& image)
{
// Show mouseover preview, but make sure it's not larger than 50% of the screen
// width/height
const QRect desktopSize = QApplication::desktop()->geometry();
const int maxPreviewWidth{desktopSize.width() / 2};
const int maxPreviewHeight{desktopSize.height() / 2};
const QPixmap previewImage = [&image, maxPreviewWidth, maxPreviewHeight]() {
if (image.width() > maxPreviewWidth || image.height() > maxPreviewHeight) {
return image.scaled(maxPreviewWidth, maxPreviewHeight, Qt::KeepAspectRatio,
Qt::SmoothTransformation);
} else {
return image;
}
}();
QByteArray imageData;
QBuffer buffer(&imageData);
buffer.open(QIODevice::WriteOnly);
previewImage.save(&buffer, "PNG");
buffer.close();
return "<img src=data:image/png;base64," + imageData.toBase64() + "/>";
}
} // namespace
void ImagePreviewButton::initialize(const QPixmap& image)
{
auto desiredSize = qMin(width(), height()); // Assume widget is a square
desiredSize = qMax(desiredSize, 4) - 4; // Leave some room for a border
auto croppedImage = scaleCropIntoSquare(image, desiredSize);
setIcon(QIcon(croppedImage));
setIconSize(croppedImage.size());
setToolTip(getToolTipDisplayingImage(image));
}
void ImagePreviewButton::setIconFromFile(const QString& filename)
{
initialize(pixmapFromFile(filename));
}
void ImagePreviewButton::setIconFromPixmap(const QPixmap& pixmap)
{
initialize(pixmap);
}

View File

@ -0,0 +1,37 @@
/*
Copyright © 2020 by The qTox Project Contributors
This file is part of qTox, a Qt-based graphical interface for Tox.
qTox is libre software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
qTox is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with qTox. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <QPushButton>
#include <QPixmap>
#include <QString>
class ImagePreviewButton : public QPushButton
{
public:
ImagePreviewButton(QWidget* parent = nullptr)
: QPushButton(parent)
{}
void setIconFromFile(const QString& filename);
void setIconFromPixmap(const QPixmap& image);
private:
void initialize(const QPixmap& image);
};

View File

@ -48,6 +48,10 @@ void ChatTextEdit::keyPressEvent(QKeyEvent* event)
emit enterPressed();
return;
}
if (key == Qt::Key_Escape) {
emit escapePressed();
return;
}
if (key == Qt::Key_Tab) {
if (event->modifiers())
event->ignore();

View File

@ -32,6 +32,7 @@ public:
signals:
void enterPressed();
void escapePressed();
void tabPressed();
void keyPressed();
void pasteImage(const QPixmap& pixmap);