1
0
mirror of https://github.com/qTox/qTox.git synced 2024-03-22 14:00:36 +08:00
qTox/src/chatlog/content/filetransferwidget.cpp
Mick Sayson 99c1753a76 fix(preview): Fix exif orientations
Previous exif transformations were not valid. The exif spec defines the
orientations as where the 0th row and the 0th column should end. The
previous mappings used in qTox did not respect these mappings and needed
to be updated.
2020-05-16 16:53:14 -07:00

628 lines
21 KiB
C++

/*
Copyright © 2014-2019 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 "filetransferwidget.h"
#include "ui_filetransferwidget.h"
#include "src/core/core.h"
#include "src/core/corefile.h"
#include "src/persistence/settings.h"
#include "src/widget/gui.h"
#include "src/widget/style.h"
#include "src/widget/widget.h"
#include "src/model/exiftransform.h"
#include <QBuffer>
#include <QDebug>
#include <QDesktopServices>
#include <QDesktopWidget>
#include <QFile>
#include <QFileDialog>
#include <QMessageBox>
#include <QMouseEvent>
#include <QPainter>
#include <QVariantAnimation>
#include <cassert>
#include <math.h>
// The leftButton is used to accept, pause, or resume a file transfer, as well as to open a
// received file.
// The rightButton is used to cancel a file transfer, or to open the directory a file was
// downloaded to.
FileTransferWidget::FileTransferWidget(QWidget* parent, ToxFile file)
: QWidget(parent)
, ui(new Ui::FileTransferWidget)
, fileInfo(file)
, backgroundColor(Style::getColor(Style::TransferMiddle))
, buttonColor(Style::getColor(Style::TransferWait))
, buttonBackgroundColor(Style::getColor(Style::GroundBase))
, active(true)
{
ui->setupUi(this);
// hide the QWidget background (background-color: transparent doesn't seem to work)
setAttribute(Qt::WA_TranslucentBackground, true);
ui->previewButton->hide();
ui->filenameLabel->setText(file.fileName);
ui->progressBar->setValue(0);
ui->fileSizeLabel->setText(getHumanReadableSize(file.filesize));
ui->etaLabel->setText("");
backgroundColorAnimation = new QVariantAnimation(this);
backgroundColorAnimation->setDuration(500);
backgroundColorAnimation->setEasingCurve(QEasingCurve::OutCubic);
connect(backgroundColorAnimation, &QVariantAnimation::valueChanged, this,
[this](const QVariant& val) {
backgroundColor = val.value<QColor>();
update();
});
buttonColorAnimation = new QVariantAnimation(this);
buttonColorAnimation->setDuration(500);
buttonColorAnimation->setEasingCurve(QEasingCurve::OutCubic);
connect(buttonColorAnimation, &QVariantAnimation::valueChanged, this, [this](const QVariant& val) {
buttonColor = val.value<QColor>();
update();
});
connect(ui->leftButton, &QPushButton::clicked, this, &FileTransferWidget::onLeftButtonClicked);
connect(ui->rightButton, &QPushButton::clicked, this, &FileTransferWidget::onRightButtonClicked);
connect(ui->previewButton, &QPushButton::clicked, this,
&FileTransferWidget::onPreviewButtonClicked);
// Set lastStatus to anything but the file's current value, this forces an update
lastStatus = file.status == ToxFile::FINISHED ? ToxFile::INITIALIZING : ToxFile::FINISHED;
updateWidget(file);
setFixedHeight(64);
}
FileTransferWidget::~FileTransferWidget()
{
delete ui;
}
// TODO(sudden6): remove file IO from the UI
/**
* @brief Dangerous way to find out if a path is writable.
* @param filepath Path to file which should be deleted.
* @return True, if file writeable, false otherwise.
*/
bool FileTransferWidget::tryRemoveFile(const QString& filepath)
{
QFile tmp(filepath);
bool writable = tmp.open(QIODevice::WriteOnly);
tmp.remove();
return writable;
}
void FileTransferWidget::onFileTransferUpdate(ToxFile file)
{
updateWidget(file);
}
bool FileTransferWidget::isActive() const
{
return active;
}
void FileTransferWidget::acceptTransfer(const QString& filepath)
{
if (filepath.isEmpty()) {
return;
}
// test if writable
if (!tryRemoveFile(filepath)) {
GUI::showWarning(tr("Location not writable", "Title of permissions popup"),
tr("You do not have permission to write that location. Choose another, or "
"cancel the save dialog.",
"text of permissions popup"));
return;
}
// everything ok!
CoreFile* coreFile = Core::getInstance()->getCoreFile();
coreFile->acceptFileRecvRequest(fileInfo.friendId, fileInfo.fileNum, filepath);
}
void FileTransferWidget::setBackgroundColor(const QColor& c, bool whiteFont)
{
if (c != backgroundColor) {
backgroundColorAnimation->setStartValue(backgroundColor);
backgroundColorAnimation->setEndValue(c);
backgroundColorAnimation->start();
}
setProperty("fontColor", whiteFont ? "white" : "black");
setStyleSheet(Style::getStylesheet("fileTransferInstance/filetransferWidget.css"));
Style::repolish(this);
update();
}
void FileTransferWidget::setButtonColor(const QColor& c)
{
if (c != buttonColor) {
buttonColorAnimation->setStartValue(buttonColor);
buttonColorAnimation->setEndValue(c);
buttonColorAnimation->start();
}
}
bool FileTransferWidget::drawButtonAreaNeeded() const
{
return (ui->rightButton->isVisible() || ui->leftButton->isVisible())
&& !(ui->leftButton->isVisible() && ui->leftButton->objectName() == "ok");
}
void FileTransferWidget::paintEvent(QPaintEvent*)
{
// required by Hi-DPI support as border-image doesn't work.
QPainter painter(this);
painter.setRenderHint(QPainter::Antialiasing);
painter.setPen(Qt::NoPen);
qreal ratio = static_cast<qreal>(geometry().height()) / static_cast<qreal>(geometry().width());
const int r = 24;
const int buttonFieldWidth = 32;
const int lineWidth = 1;
// Draw the widget background:
painter.setClipRect(QRect(0, 0, width(), height()));
painter.setBrush(QBrush(backgroundColor));
painter.drawRoundedRect(geometry(), r * ratio, r, Qt::RelativeSize);
if (drawButtonAreaNeeded()) {
// Draw the button background:
QPainterPath buttonBackground;
buttonBackground.addRoundedRect(width() - 2 * buttonFieldWidth - lineWidth * 2, 0,
buttonFieldWidth, buttonFieldWidth + lineWidth, 50, 50,
Qt::RelativeSize);
buttonBackground.addRect(width() - 2 * buttonFieldWidth - lineWidth * 2, 0,
buttonFieldWidth * 2, buttonFieldWidth / 2);
buttonBackground.addRect(width() - 1.5 * buttonFieldWidth - lineWidth * 2, 0,
buttonFieldWidth * 2, buttonFieldWidth + 1);
buttonBackground.setFillRule(Qt::WindingFill);
painter.setBrush(QBrush(buttonBackgroundColor));
painter.drawPath(buttonBackground);
// Draw the left button:
QPainterPath leftButton;
leftButton.addRoundedRect(QRect(width() - 2 * buttonFieldWidth - lineWidth, 0,
buttonFieldWidth, buttonFieldWidth),
50, 50, Qt::RelativeSize);
leftButton.addRect(QRect(width() - 2 * buttonFieldWidth - lineWidth, 0,
buttonFieldWidth / 2, buttonFieldWidth / 2));
leftButton.addRect(QRect(width() - 1.5 * buttonFieldWidth - lineWidth, 0,
buttonFieldWidth / 2, buttonFieldWidth));
leftButton.setFillRule(Qt::WindingFill);
painter.setBrush(QBrush(buttonColor));
painter.drawPath(leftButton);
// Draw the right button:
painter.setBrush(QBrush(buttonColor));
painter.setClipRect(QRect(width() - buttonFieldWidth, 0, buttonFieldWidth, buttonFieldWidth));
painter.drawRoundedRect(geometry(), r * ratio, r, Qt::RelativeSize);
}
}
QString FileTransferWidget::getHumanReadableSize(qint64 size)
{
static const char* suffix[] = {"B", "KiB", "MiB", "GiB", "TiB"};
int exp = 0;
if (size > 0) {
exp = std::min(static_cast<int>(log(size) / log(1024)), static_cast<int>(sizeof(suffix) / sizeof(suffix[0]) - 1));
}
return QString().setNum(size / pow(1024, exp), 'f', exp > 1 ? 2 : 0).append(suffix[exp]);
}
void FileTransferWidget::updateWidgetColor(ToxFile const& file)
{
if (lastStatus == file.status) {
return;
}
switch (file.status) {
case ToxFile::INITIALIZING:
case ToxFile::PAUSED:
case ToxFile::TRANSMITTING:
setBackgroundColor(Style::getColor(Style::TransferMiddle), false);
break;
case ToxFile::BROKEN:
case ToxFile::CANCELED:
setBackgroundColor(Style::getColor(Style::TransferBad), true);
break;
case ToxFile::FINISHED:
setBackgroundColor(Style::getColor(Style::TransferGood), true);
break;
default:
qCritical() << "Invalid file status";
assert(false);
}
}
void FileTransferWidget::updateWidgetText(ToxFile const& file)
{
if (lastStatus == file.status && file.status != ToxFile::PAUSED) {
return;
}
switch (file.status) {
case ToxFile::INITIALIZING:
if (file.direction == ToxFile::SENDING) {
ui->progressLabel->setText(tr("Waiting to send...", "file transfer widget"));
} else {
ui->progressLabel->setText(tr("Accept to receive this file", "file transfer widget"));
}
break;
case ToxFile::PAUSED:
ui->etaLabel->setText("");
if (file.pauseStatus.localPaused()) {
ui->progressLabel->setText(tr("Paused", "file transfer widget"));
} else {
ui->progressLabel->setText(tr("Remote paused", "file transfer widget"));
}
break;
case ToxFile::TRANSMITTING:
ui->etaLabel->setText("");
ui->progressLabel->setText(tr("Resuming...", "file transfer widget"));
break;
case ToxFile::BROKEN:
case ToxFile::CANCELED:
break;
case ToxFile::FINISHED:
break;
default:
qCritical() << "Invalid file status";
assert(false);
}
}
void FileTransferWidget::updatePreview(ToxFile const& file)
{
if (lastStatus == file.status) {
return;
}
switch (file.status) {
case ToxFile::INITIALIZING:
case ToxFile::PAUSED:
case ToxFile::TRANSMITTING:
case ToxFile::BROKEN:
case ToxFile::CANCELED:
if (file.direction == ToxFile::SENDING) {
showPreview(file.filePath);
}
break;
case ToxFile::FINISHED:
showPreview(file.filePath);
break;
default:
qCritical() << "Invalid file status";
assert(false);
}
}
void FileTransferWidget::updateFileProgress(ToxFile const& file)
{
switch (file.status) {
case ToxFile::INITIALIZING:
break;
case ToxFile::PAUSED:
fileProgress.resetSpeed();
break;
case ToxFile::TRANSMITTING: {
if (!fileProgress.needsUpdate()) {
break;
}
fileProgress.addSample(file);
auto speed = fileProgress.getSpeed();
auto progress = fileProgress.getProgress();
auto remainingTime = fileProgress.getTimeLeftSeconds();
ui->progressBar->setValue(static_cast<int>(progress * 100.0));
// update UI
if (speed > 0) {
// ETA
QTime toGo = QTime(0, 0).addSecs(remainingTime);
QString format = toGo.hour() > 0 ? "hh:mm:ss" : "mm:ss";
ui->etaLabel->setText(toGo.toString(format));
} else {
ui->etaLabel->setText("");
}
ui->progressLabel->setText(getHumanReadableSize(speed) + "/s");
break;
}
case ToxFile::BROKEN:
case ToxFile::CANCELED:
case ToxFile::FINISHED: {
ui->progressBar->hide();
ui->progressLabel->hide();
ui->etaLabel->hide();
break;
}
default:
qCritical() << "Invalid file status";
assert(false);
}
}
void FileTransferWidget::updateSignals(ToxFile const& file)
{
if (lastStatus == file.status) {
return;
}
switch (file.status) {
case ToxFile::CANCELED:
case ToxFile::BROKEN:
case ToxFile::FINISHED:
active = false;
disconnect(Core::getInstance()->getCoreFile(), nullptr, this, nullptr);
break;
case ToxFile::INITIALIZING:
case ToxFile::PAUSED:
case ToxFile::TRANSMITTING:
break;
default:
qCritical() << "Invalid file status";
assert(false);
}
}
void FileTransferWidget::setupButtons(ToxFile const& file)
{
if (lastStatus == file.status && file.status != ToxFile::PAUSED) {
return;
}
switch (file.status) {
case ToxFile::TRANSMITTING:
ui->leftButton->setIcon(QIcon(Style::getImagePath("fileTransferInstance/pause.svg")));
ui->leftButton->setObjectName("pause");
ui->leftButton->setToolTip(tr("Pause transfer"));
ui->rightButton->setIcon(QIcon(Style::getImagePath("fileTransferInstance/no.svg")));
ui->rightButton->setObjectName("cancel");
ui->rightButton->setToolTip(tr("Cancel transfer"));
setButtonColor(Style::getColor(Style::TransferGood));
break;
case ToxFile::PAUSED:
if (file.pauseStatus.localPaused()) {
ui->leftButton->setIcon(QIcon(Style::getImagePath("fileTransferInstance/arrow_white.svg")));
ui->leftButton->setObjectName("resume");
ui->leftButton->setToolTip(tr("Resume transfer"));
} else {
ui->leftButton->setIcon(QIcon(Style::getImagePath("fileTransferInstance/pause.svg")));
ui->leftButton->setObjectName("pause");
ui->leftButton->setToolTip(tr("Pause transfer"));
}
ui->rightButton->setIcon(QIcon(Style::getImagePath("fileTransferInstance/no.svg")));
ui->rightButton->setObjectName("cancel");
ui->rightButton->setToolTip(tr("Cancel transfer"));
setButtonColor(Style::getColor(Style::TransferMiddle));
break;
case ToxFile::INITIALIZING:
ui->rightButton->setIcon(QIcon(Style::getImagePath("fileTransferInstance/no.svg")));
ui->rightButton->setObjectName("cancel");
ui->rightButton->setToolTip(tr("Cancel transfer"));
if (file.direction == ToxFile::SENDING) {
ui->leftButton->setIcon(QIcon(Style::getImagePath("fileTransferInstance/pause.svg")));
ui->leftButton->setObjectName("pause");
ui->leftButton->setToolTip(tr("Pause transfer"));
} else {
ui->leftButton->setIcon(QIcon(Style::getImagePath("fileTransferInstance/yes.svg")));
ui->leftButton->setObjectName("accept");
ui->leftButton->setToolTip(tr("Accept transfer"));
}
break;
case ToxFile::CANCELED:
case ToxFile::BROKEN:
ui->leftButton->hide();
ui->rightButton->hide();
break;
case ToxFile::FINISHED:
ui->leftButton->setIcon(QIcon(Style::getImagePath("fileTransferInstance/yes.svg")));
ui->leftButton->setObjectName("ok");
ui->leftButton->setToolTip(tr("Open file"));
ui->leftButton->show();
ui->rightButton->setIcon(QIcon(Style::getImagePath("fileTransferInstance/dir.svg")));
ui->rightButton->setObjectName("dir");
ui->rightButton->setToolTip(tr("Open file directory"));
ui->rightButton->show();
break;
default:
qCritical() << "Invalid file status";
assert(false);
}
}
void FileTransferWidget::handleButton(QPushButton* btn)
{
CoreFile* coreFile = Core::getInstance()->getCoreFile();
if (fileInfo.direction == ToxFile::SENDING) {
if (btn->objectName() == "cancel") {
coreFile->cancelFileSend(fileInfo.friendId, fileInfo.fileNum);
} else if (btn->objectName() == "pause") {
coreFile->pauseResumeFile(fileInfo.friendId, fileInfo.fileNum);
} else if (btn->objectName() == "resume") {
coreFile->pauseResumeFile(fileInfo.friendId, fileInfo.fileNum);
}
} else // receiving or paused
{
if (btn->objectName() == "cancel") {
coreFile->cancelFileRecv(fileInfo.friendId, fileInfo.fileNum);
} else if (btn->objectName() == "pause") {
coreFile->pauseResumeFile(fileInfo.friendId, fileInfo.fileNum);
} else if (btn->objectName() == "resume") {
coreFile->pauseResumeFile(fileInfo.friendId, fileInfo.fileNum);
} else if (btn->objectName() == "accept") {
QString path =
QFileDialog::getSaveFileName(Q_NULLPTR,
tr("Save a file", "Title of the file saving dialog"),
Settings::getInstance().getGlobalAutoAcceptDir() + "/"
+ fileInfo.fileName);
acceptTransfer(path);
}
}
if (btn->objectName() == "ok" || btn->objectName() == "previewButton") {
Widget::confirmExecutableOpen(QFileInfo(fileInfo.filePath));
} else if (btn->objectName() == "dir") {
QString dirPath = QFileInfo(fileInfo.filePath).dir().path();
QDesktopServices::openUrl(QUrl::fromLocalFile(dirPath));
}
}
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() + "/>");
}
}
void FileTransferWidget::onLeftButtonClicked()
{
handleButton(ui->leftButton);
}
void FileTransferWidget::onRightButtonClicked()
{
handleButton(ui->rightButton);
}
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);
fileInfo = file;
// If we repainted on every packet our gui would be *very* slow
bool bTransmitNeedsUpdate = fileProgress.needsUpdate();
updatePreview(file);
updateFileProgress(file);
updateWidgetText(file);
updateWidgetColor(file);
setupButtons(file);
updateSignals(file);
lastStatus = file.status;
// trigger repaint
switch (file.status) {
case ToxFile::TRANSMITTING:
if (!bTransmitNeedsUpdate) {
break;
}
// fallthrough
default:
update();
}
}