Browse Source

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.
reviewable/pr6175/r3
Mick Sayson 6 years ago committed by Anthony Bilinski
parent
commit
7c218b389d
No known key found for this signature in database
GPG Key ID: 2AA8E0DA1B31FB3C
  1. 2
      CMakeLists.txt
  2. 70
      src/chatlog/content/filetransferwidget.cpp
  3. 3
      src/chatlog/content/filetransferwidget.h
  4. 13
      src/chatlog/content/filetransferwidget.ui
  5. 48
      src/widget/form/chatform.cpp
  6. 7
      src/widget/form/chatform.h
  7. 2
      src/widget/form/genericchatform.cpp
  8. 1
      src/widget/form/genericchatform.h
  9. 125
      src/widget/imagepreviewwidget.cpp
  10. 37
      src/widget/imagepreviewwidget.h
  11. 4
      src/widget/tool/chattextedit.cpp
  12. 1
      src/widget/tool/chattextedit.h

2
CMakeLists.txt

@ -373,6 +373,8 @@ set(${PROJECT_NAME}_SOURCES @@ -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

70
src/chatlog/content/filetransferwidget.cpp

@ -500,49 +500,8 @@ void FileTransferWidget::handleButton(QPushButton* btn) @@ -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() @@ -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);

3
src/chatlog/content/filetransferwidget.h

@ -73,9 +73,6 @@ private slots: @@ -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);

13
src/chatlog/content/filetransferwidget.ui

@ -270,7 +270,7 @@ @@ -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 @@ @@ -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 @@ @@ -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 @@ @@ -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 @@ @@ -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>

48
src/widget/form/chatform.cpp

@ -37,6 +37,7 @@ @@ -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 @@ -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 @@ -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() @@ -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) @@ -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();

7
src/widget/form/chatform.h

@ -41,6 +41,7 @@ class OfflineMsgEngine; @@ -41,6 +41,7 @@ class OfflineMsgEngine;
class QPixmap;
class QHideEvent;
class QMoveEvent;
class ImagePreviewButton;
class ChatForm : public GenericChatForm
{
@ -96,7 +97,9 @@ private slots: @@ -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: @@ -131,6 +134,8 @@ private:
QTimer typingTimer;
QElapsedTimer timeElapsed;
QAction* copyStatusAction;
QPixmap imagePreviewSource;
ImagePreviewButton* imagePreview;
bool isTyping;
bool lastCallIsVideo;
std::unique_ptr<NetCamView> netcam;

2
src/widget/form/genericchatform.cpp

@ -309,7 +309,7 @@ GenericChatForm::GenericChatForm(const Core& _core, const Contact* contact, ICha @@ -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);

1
src/widget/form/genericchatform.h

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

125
src/widget/imagepreviewwidget.cpp

@ -0,0 +1,125 @@ @@ -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

@ -0,0 +1,37 @@ @@ -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);
};

4
src/widget/tool/chattextedit.cpp

@ -48,6 +48,10 @@ void ChatTextEdit::keyPressEvent(QKeyEvent* event) @@ -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();

1
src/widget/tool/chattextedit.h

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

Loading…
Cancel
Save