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

Merge pull request #6175

Mick Sayson (1):
      feat(chatlog): Add image preview on paste
This commit is contained in:
Anthony Bilinski 2021-10-24 22:09:22 -07:00
commit bb67d48e6e
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/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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -168,6 +168,7 @@ protected:
QMenu menu; QMenu menu;
QVBoxLayout* contentLayout;
QPushButton* emoteButton; QPushButton* emoteButton;
QPushButton* fileButton; QPushButton* fileButton;
QPushButton* screenshotButton; 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(); 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();

View File

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