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
src/widget/extensionstatus.h src/widget/extensionstatus.h
src/widget/flowlayout.cpp src/widget/flowlayout.cpp
src/widget/flowlayout.h src/widget/flowlayout.h
src/widget/imagepreviewwidget.h
src/widget/imagepreviewwidget.cpp
src/widget/searchform.cpp src/widget/searchform.cpp
src/widget/searchform.h src/widget/searchform.h
src/widget/searchtypes.h src/widget/searchtypes.h

70
src/chatlog/content/filetransferwidget.cpp

@ -500,49 +500,8 @@ void FileTransferWidget::handleButton(QPushButton* btn)
void FileTransferWidget::showPreview(const QString& filename) void FileTransferWidget::showPreview(const QString& filename)
{ {
static const QStringList previewExtensions = {"png", "jpeg", "jpg", "gif", "svg", ui->previewButton->setIconFromFile(filename);
"PNG", "JPEG", "JPG", "GIF", "SVG"}; ui->previewButton->show();
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() + "/>");
}
} }
void FileTransferWidget::onLeftButtonClicked() void FileTransferWidget::onLeftButtonClicked()
@ -560,31 +519,6 @@ void FileTransferWidget::onPreviewButtonClicked()
handleButton(ui->previewButton); 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) void FileTransferWidget::updateWidget(ToxFile const& file)
{ {
assert(file == fileInfo); assert(file == fileInfo);

3
src/chatlog/content/filetransferwidget.h

@ -73,9 +73,6 @@ private slots:
void onPreviewButtonClicked(); void onPreviewButtonClicked();
private: 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); static bool tryRemoveFile(const QString &filepath);
void updateWidget(ToxFile const& file); void updateWidget(ToxFile const& file);

13
src/chatlog/content/filetransferwidget.ui

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

48
src/widget/form/chatform.cpp

@ -37,6 +37,7 @@
#include "src/widget/chatformheader.h" #include "src/widget/chatformheader.h"
#include "src/widget/contentdialogmanager.h" #include "src/widget/contentdialogmanager.h"
#include "src/widget/form/loadhistorydialog.h" #include "src/widget/form/loadhistorydialog.h"
#include "src/widget/imagepreviewwidget.h"
#include "src/widget/maskablepixmapwidget.h" #include "src/widget/maskablepixmapwidget.h"
#include "src/widget/searchform.h" #include "src/widget/searchform.h"
#include "src/widget/style.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); headWidget->addWidget(callDuration, 1, Qt::AlignCenter);
callDuration->hide(); 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())); copyStatusAction = statusMessageMenu.addAction(QString(), this, SLOT(onCopyStatusMessage()));
const CoreFile* coreFile = core.getCoreFile(); 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::micMuteToggle, this, &ChatForm::onMicMuteToggle);
connect(headWidget, &ChatFormHeader::volMuteToggle, this, &ChatForm::onVolMuteToggle); connect(headWidget, &ChatFormHeader::volMuteToggle, this, &ChatForm::onVolMuteToggle);
connect(sendButton, &QPushButton::pressed, this, &ChatForm::callUpdateFriendActivity); connect(sendButton, &QPushButton::pressed, this, &ChatForm::callUpdateFriendActivity);
connect(sendButton, &QPushButton::pressed, this, &ChatForm::sendImageFromPreview);
connect(msgEdit, &ChatTextEdit::enterPressed, this, &ChatForm::callUpdateFriendActivity); connect(msgEdit, &ChatTextEdit::enterPressed, this, &ChatForm::callUpdateFriendActivity);
connect(msgEdit, &ChatTextEdit::textChanged, this, &ChatForm::onTextEditChanged); 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, connect(statusMessageLabel, &CroppingLabel::customContextMenuRequested, this,
[&](const QPoint& pos) { [&](const QPoint& pos) {
if (!statusMessageLabel->text().isEmpty()) { if (!statusMessageLabel->text().isEmpty()) {
@ -561,12 +582,29 @@ void ChatForm::doScreenshot()
{ {
// note: grabber is self-managed and will destroy itself when done // note: grabber is self-managed and will destroy itself when done
ScreenshotGrabber* grabber = new ScreenshotGrabber; ScreenshotGrabber* grabber = new ScreenshotGrabber;
connect(grabber, &ScreenshotGrabber::screenshotTaken, this, &ChatForm::sendImage); connect(grabber, &ScreenshotGrabber::screenshotTaken, this, &ChatForm::previewImage);
grabber->showGrabber(); 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"); QDir(Settings::getInstance().getPaths().getAppDataDirPath()).mkpath("images");
// use ~ISO 8601 for screenshot timestamp, considering FS limitations // use ~ISO 8601 for screenshot timestamp, considering FS limitations
@ -580,8 +618,10 @@ void ChatForm::sendImage(const QPixmap& pixmap)
QFile file(filepath); QFile file(filepath);
if (file.open(QFile::ReadWrite)) { if (file.open(QFile::ReadWrite)) {
pixmap.save(&file, "PNG"); imagePreviewSource.save(&file, "PNG");
qint64 filesize = file.size(); qint64 filesize = file.size();
imagePreview->hide();
imagePreviewSource = QPixmap();
file.close(); file.close();
QFileInfo fi(file); QFileInfo fi(file);
CoreFile* coreFile = core.getCoreFile(); CoreFile* coreFile = core.getCoreFile();

7
src/widget/form/chatform.h

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

2
src/widget/form/genericchatform.cpp

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

1
src/widget/form/genericchatform.h

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

125
src/widget/imagepreviewwidget.cpp

@ -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 @@
/*
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)
emit enterPressed(); emit enterPressed();
return; return;
} }
if (key == Qt::Key_Escape) {
emit escapePressed();
return;
}
if (key == Qt::Key_Tab) { if (key == Qt::Key_Tab) {
if (event->modifiers()) if (event->modifiers())
event->ignore(); event->ignore();

1
src/widget/tool/chattextedit.h

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

Loading…
Cancel
Save