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
Andrew Morgan 42a9534b24
fix(ui): Use native file picker dialog
The original reason that the Qt picker was used instead of the native
picker was that the native option would cause Nautilus/GNOME-based
pickers to hang.

This turned out to be due with a Qt bug with parenting Gtk windows. As a
result the parent of each file dialog window has been set to NULL,
eliminating the crash. As far as tests have shown, this produces no
adverse effects on either floating or tiling wms.

Fixes #3494
2017-07-06 06:40:33 -07:00

588 lines
20 KiB
C++

/*
Copyright © 2014-2015 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/nexus.h"
#include "src/persistence/settings.h"
#include "src/widget/gui.h"
#include "src/widget/style.h"
#include "src/widget/widget.h"
#include <QBuffer>
#include <QDebug>
#include <QDesktopServices>
#include <QDesktopWidget>
#include <QFile>
#include <QFileDialog>
#include <QMessageBox>
#include <QMouseEvent>
#include <QPainter>
#include <QVariantAnimation>
#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)
, lastTick(QTime::currentTime())
, backgroundColor(Style::getColor(Style::LightGrey))
, buttonColor(Style::getColor(Style::Yellow))
, buttonBackgroundColor(Style::getColor(Style::White))
, 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();
});
setBackgroundColor(Style::getColor(Style::LightGrey), false);
connect(Core::getInstance(), &Core::fileTransferInfo, this,
&FileTransferWidget::onFileTransferInfo);
connect(Core::getInstance(), &Core::fileTransferAccepted, this,
&FileTransferWidget::onFileTransferAccepted);
connect(Core::getInstance(), &Core::fileTransferCancelled, this,
&FileTransferWidget::onFileTransferCancelled);
connect(Core::getInstance(), &Core::fileTransferPaused, this,
&FileTransferWidget::onFileTransferPaused);
connect(Core::getInstance(), &Core::fileTransferFinished, this,
&FileTransferWidget::onFileTransferFinished);
connect(Core::getInstance(), &Core::fileTransferRemotePausedUnpaused, this,
&FileTransferWidget::fileTransferRemotePausedUnpaused);
connect(Core::getInstance(), &Core::fileTransferBrokenUnbroken, this,
&FileTransferWidget::fileTransferBrokenUnbroken);
connect(ui->leftButton, &QPushButton::clicked, this, &FileTransferWidget::onLeftButtonClicked);
connect(ui->rightButton, &QPushButton::clicked, this, &FileTransferWidget::onRightButtonClicked);
connect(ui->previewButton, &QPushButton::clicked, this,
&FileTransferWidget::onPreviewButtonClicked);
setupButtons();
// preview
if (fileInfo.direction == ToxFile::SENDING) {
showPreview(fileInfo.filePath);
ui->progressLabel->setText(tr("Waiting to send...", "file transfer widget"));
} else {
ui->progressLabel->setText(tr("Accept to receive this file", "file transfer widget"));
}
setFixedHeight(64);
}
FileTransferWidget::~FileTransferWidget()
{
delete ui;
}
void FileTransferWidget::autoAcceptTransfer(const QString& path)
{
QString filepath;
int number = 0;
QString suffix = QFileInfo(fileInfo.fileName).completeSuffix();
QString base = QFileInfo(fileInfo.fileName).baseName();
do {
filepath = QString("%1/%2%3.%4")
.arg(path, base,
number > 0 ? QString(" (%1)").arg(QString::number(number)) : QString(),
suffix);
++number;
} while (QFileInfo(filepath).exists());
// Do not automatically accept the file-transfer if the path is not writable.
// The user can still accept it manually.
if (Nexus::tryRemoveFile(filepath))
Core::getInstance()->acceptFileRecvRequest(fileInfo.friendId, fileInfo.fileNum, filepath);
else
qWarning() << "Cannot write to " << filepath;
}
bool FileTransferWidget::isActive() const
{
return active;
}
void FileTransferWidget::acceptTransfer(const QString& filepath)
{
if (filepath.isEmpty())
return;
// test if writable
if (!Nexus::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!
Core::getInstance()->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(":/ui/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.drawRoundRect(geometry(), r * ratio, r);
if (drawButtonAreaNeeded()) {
// Draw the button background:
QPainterPath buttonBackground;
buttonBackground.addRoundRect(width() - 2 * buttonFieldWidth - lineWidth * 2, 0,
buttonFieldWidth, buttonFieldWidth + lineWidth, 50, 50);
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.addRoundRect(QRect(width() - 2 * buttonFieldWidth - lineWidth, 0,
buttonFieldWidth, buttonFieldWidth),
50, 50);
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.drawRoundRect(geometry(), r * ratio, r);
}
}
void FileTransferWidget::onFileTransferInfo(ToxFile file)
{
QTime now = QTime::currentTime();
qint64 dt = lastTick.msecsTo(now); // ms
if (fileInfo != file || dt < 1000)
return;
fileInfo = file;
if (fileInfo.status == ToxFile::TRANSMITTING) {
// update progress
qreal progress = static_cast<qreal>(file.bytesSent) / static_cast<qreal>(file.filesize);
ui->progressBar->setValue(static_cast<int>(progress * 100.0));
// ETA, speed
qreal deltaSecs = dt / 1000.0;
// (can't use ::abs or ::max on unsigned types substraction, they'd just overflow)
quint64 deltaBytes = file.bytesSent > lastBytesSent ? file.bytesSent - lastBytesSent
: lastBytesSent - file.bytesSent;
qreal bytesPerSec = static_cast<int>(static_cast<qreal>(deltaBytes) / deltaSecs);
// calculate mean
meanIndex = meanIndex % TRANSFER_ROLLING_AVG_COUNT;
meanData[meanIndex++] = bytesPerSec;
qreal meanBytesPerSec = 0.0;
for (size_t i = 0; i < TRANSFER_ROLLING_AVG_COUNT; ++i)
meanBytesPerSec += meanData[i];
meanBytesPerSec /= static_cast<qreal>(TRANSFER_ROLLING_AVG_COUNT);
// update UI
if (meanBytesPerSec > 0) {
// ETA
QTime toGo = QTime(0, 0).addSecs((file.filesize - file.bytesSent) / meanBytesPerSec);
QString format = toGo.hour() > 0 ? "hh:mm:ss" : "mm:ss";
ui->etaLabel->setText(toGo.toString(format));
} else {
ui->etaLabel->setText("");
}
ui->progressLabel->setText(getHumanReadableSize(meanBytesPerSec) + "/s");
lastBytesSent = file.bytesSent;
}
lastTick = now;
// trigger repaint
update();
}
void FileTransferWidget::onFileTransferAccepted(ToxFile file)
{
if (fileInfo != file)
return;
fileInfo = file;
setBackgroundColor(Style::getColor(Style::LightGrey), false);
setupButtons();
}
void FileTransferWidget::onFileTransferCancelled(ToxFile file)
{
if (fileInfo != file)
return;
fileInfo = file;
active = false;
setBackgroundColor(Style::getColor(Style::Red), true);
setupButtons();
hideWidgets();
disconnect(Core::getInstance(), 0, this, 0);
}
void FileTransferWidget::onFileTransferPaused(ToxFile file)
{
if (fileInfo != file)
return;
fileInfo = file;
ui->etaLabel->setText("");
ui->progressLabel->setText(tr("Paused", "file transfer widget"));
// reset mean
meanIndex = 0;
for (size_t i = 0; i < TRANSFER_ROLLING_AVG_COUNT; ++i)
meanData[i] = 0.0;
setBackgroundColor(Style::getColor(Style::LightGrey), false);
setupButtons();
}
void FileTransferWidget::onFileTransferResumed(ToxFile file)
{
if (fileInfo != file)
return;
fileInfo = file;
ui->etaLabel->setText("");
ui->progressLabel->setText(tr("Resuming...", "file transfer widget"));
// reset mean
meanIndex = 0;
for (size_t i = 0; i < TRANSFER_ROLLING_AVG_COUNT; ++i)
meanData[i] = 0.0;
setBackgroundColor(Style::getColor(Style::LightGrey), false);
setupButtons();
}
void FileTransferWidget::onFileTransferFinished(ToxFile file)
{
if (fileInfo != file)
return;
fileInfo = file;
active = false;
setBackgroundColor(Style::getColor(Style::Green), true);
setupButtons();
hideWidgets();
ui->leftButton->setIcon(QIcon(":/ui/fileTransferInstance/yes.svg"));
ui->leftButton->setObjectName("ok");
ui->leftButton->setToolTip(tr("Open file"));
ui->leftButton->show();
ui->rightButton->setIcon(QIcon(":/ui/fileTransferInstance/dir.svg"));
ui->rightButton->setObjectName("dir");
ui->rightButton->setToolTip(tr("Open file directory"));
ui->rightButton->show();
// preview
if (fileInfo.direction == ToxFile::RECEIVING)
showPreview(fileInfo.filePath);
disconnect(Core::getInstance(), 0, this, 0);
}
void FileTransferWidget::fileTransferRemotePausedUnpaused(ToxFile file, bool paused)
{
if (paused)
onFileTransferPaused(file);
else
onFileTransferResumed(file);
}
void FileTransferWidget::fileTransferBrokenUnbroken(ToxFile file, bool broken)
{
// TODO: Handle broken transfer differently once we have resuming code
if (broken)
onFileTransferCancelled(file);
}
QString FileTransferWidget::getHumanReadableSize(qint64 size)
{
static const char* suffix[] = {"B", "kiB", "MiB", "GiB", "TiB"};
int exp = 0;
if (size > 0)
exp = std::min((int)(log(size) / log(1024)), (int)(sizeof(suffix) / sizeof(suffix[0]) - 1));
return QString().setNum(size / pow(1024, exp), 'f', exp > 1 ? 2 : 0).append(suffix[exp]);
}
void FileTransferWidget::hideWidgets()
{
ui->leftButton->hide();
ui->rightButton->hide();
ui->progressBar->hide();
ui->progressLabel->hide();
ui->etaLabel->hide();
}
void FileTransferWidget::setupButtons()
{
switch (fileInfo.status) {
case ToxFile::TRANSMITTING:
ui->leftButton->setIcon(QIcon(":/ui/fileTransferInstance/pause.svg"));
ui->leftButton->setObjectName("pause");
ui->leftButton->setToolTip(tr("Pause transfer"));
ui->rightButton->setIcon(QIcon(":/ui/fileTransferInstance/no.svg"));
ui->rightButton->setObjectName("cancel");
ui->rightButton->setToolTip(tr("Cancel transfer"));
setButtonColor(Style::getColor(Style::Green));
break;
case ToxFile::PAUSED:
ui->leftButton->setIcon(QIcon(":/ui/fileTransferInstance/arrow_white.svg"));
ui->leftButton->setObjectName("resume");
ui->leftButton->setToolTip(tr("Resume transfer"));
ui->rightButton->setIcon(QIcon(":/ui/fileTransferInstance/no.svg"));
ui->rightButton->setObjectName("cancel");
ui->rightButton->setToolTip(tr("Cancel transfer"));
setButtonColor(Style::getColor(Style::LightGrey));
break;
case ToxFile::STOPPED:
case ToxFile::BROKEN:
ui->rightButton->setIcon(QIcon(":/ui/fileTransferInstance/no.svg"));
ui->rightButton->setObjectName("cancel");
ui->rightButton->setToolTip(tr("Cancel transfer"));
if (fileInfo.direction == ToxFile::SENDING) {
ui->leftButton->setIcon(QIcon(":/ui/fileTransferInstance/pause.svg"));
ui->leftButton->setObjectName("pause");
ui->leftButton->setToolTip(tr("Pause transfer"));
} else {
ui->leftButton->setIcon(QIcon(":/ui/fileTransferInstance/yes.svg"));
ui->leftButton->setObjectName("accept");
ui->leftButton->setToolTip(tr("Accept transfer"));
}
break;
}
}
void FileTransferWidget::handleButton(QPushButton* btn)
{
if (fileInfo.direction == ToxFile::SENDING) {
if (btn->objectName() == "cancel")
Core::getInstance()->cancelFileSend(fileInfo.friendId, fileInfo.fileNum);
else if (btn->objectName() == "pause")
Core::getInstance()->pauseResumeFileSend(fileInfo.friendId, fileInfo.fileNum);
else if (btn->objectName() == "resume")
Core::getInstance()->pauseResumeFileSend(fileInfo.friendId, fileInfo.fileNum);
} else // receiving or paused
{
if (btn->objectName() == "cancel")
Core::getInstance()->cancelFileRecv(fileInfo.friendId, fileInfo.fileNum);
else if (btn->objectName() == "pause")
Core::getInstance()->pauseResumeFileRecv(fileInfo.friendId, fileInfo.fileNum);
else if (btn->objectName() == "resume")
Core::getInstance()->pauseResumeFileRecv(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,
0, 0);
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;
const QImage image = QImage(filename);
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()->screenGeometry();
const QImage previewImage = image.scaled(0.5 * desktopSize.width(), 0.5 * desktopSize.height(),
Qt::KeepAspectRatio, Qt::SmoothTransformation);
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;
}