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:
parent
8462207a58
commit
7c218b389d
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -168,6 +168,7 @@ protected:
|
|||
|
||||
QMenu menu;
|
||||
|
||||
QVBoxLayout* contentLayout;
|
||||
QPushButton* emoteButton;
|
||||
QPushButton* fileButton;
|
||||
QPushButton* screenshotButton;
|
||||
|
|
125
src/widget/imagepreviewwidget.cpp
Normal file
125
src/widget/imagepreviewwidget.cpp
Normal 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);
|
||||
}
|
37
src/widget/imagepreviewwidget.h
Normal file
37
src/widget/imagepreviewwidget.h
Normal 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);
|
||||
};
|
|
@ -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();
|
||||
|
|
|
@ -32,6 +32,7 @@ public:
|
|||
|
||||
signals:
|
||||
void enterPressed();
|
||||
void escapePressed();
|
||||
void tabPressed();
|
||||
void keyPressed();
|
||||
void pasteImage(const QPixmap& pixmap);
|
||||
|
|
Loading…
Reference in New Issue
Block a user