mirror of https://github.com/qTox/qTox
527 lines
17 KiB
C++
527 lines
17 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 "filesform.h"
|
|
#include "src/core/toxfile.h"
|
|
#include "src/widget/contentlayout.h"
|
|
#include "src/widget/translator.h"
|
|
#include "src/widget/style.h"
|
|
#include "src/widget/widget.h"
|
|
#include "src/friendlist.h"
|
|
#include "util/display.h"
|
|
#include <QFileInfo>
|
|
#include <QWindow>
|
|
#include <QTableView>
|
|
#include <QHeaderView>
|
|
#include <QPushButton>
|
|
#include <QPainter>
|
|
#include <QMouseEvent>
|
|
#include <cmath>
|
|
|
|
namespace {
|
|
QRect pauseRect(const QStyleOptionViewItem& option)
|
|
{
|
|
float controlSize = option.rect.height() * 0.8f;
|
|
float rectWidth = option.rect.width();
|
|
float buttonHorizontalArea = rectWidth / 2;
|
|
|
|
// To center the button, we find the horizontal center and subtract half
|
|
// our width from it
|
|
int buttonXPos = std::round(option.rect.x() + buttonHorizontalArea / 2 - controlSize / 2);
|
|
int buttonYPos = std::round(option.rect.y() + option.rect.height() * 0.1f);
|
|
return QRect(buttonXPos, buttonYPos, controlSize, controlSize);
|
|
}
|
|
|
|
QRect stopRect(const QStyleOptionViewItem& option)
|
|
{
|
|
float controlSize = option.rect.height() * 0.8;
|
|
float rectWidth = option.rect.width();
|
|
float buttonHorizontalArea = rectWidth / 2;
|
|
|
|
// To center the button, we find the horizontal center and subtract half
|
|
// our width from it
|
|
int buttonXPos = std::round(option.rect.x() + buttonHorizontalArea + buttonHorizontalArea / 2 - controlSize / 2);
|
|
int buttonYPos = std::round(option.rect.y() + option.rect.height() * 0.1f);
|
|
return QRect(buttonXPos, buttonYPos, controlSize, controlSize);
|
|
}
|
|
|
|
QString fileStatusString(ToxFile file)
|
|
{
|
|
switch (file.status)
|
|
{
|
|
case ToxFile::INITIALIZING:
|
|
return QObject::tr("Initializing");
|
|
case ToxFile::TRANSMITTING:
|
|
return QObject::tr("Transmitting");
|
|
case ToxFile::FINISHED:
|
|
return QObject::tr("Finished");
|
|
case ToxFile::BROKEN:
|
|
return QObject::tr("Broken");
|
|
case ToxFile::CANCELED:
|
|
return QObject::tr("Canceled");
|
|
case ToxFile::PAUSED:
|
|
if (file.pauseStatus.localPaused()) {
|
|
return QObject::tr("Paused");
|
|
} else {
|
|
return QObject::tr("Remote paused");
|
|
}
|
|
}
|
|
|
|
qWarning("Corrupt file status %d", file.status);
|
|
return "";
|
|
}
|
|
|
|
bool fileTransferFailed(const ToxFile::FileStatus& status) {
|
|
switch (status)
|
|
{
|
|
case ToxFile::INITIALIZING:
|
|
case ToxFile::PAUSED:
|
|
case ToxFile::TRANSMITTING:
|
|
case ToxFile::FINISHED:
|
|
return false;
|
|
case ToxFile::BROKEN:
|
|
case ToxFile::CANCELED:
|
|
return true;
|
|
}
|
|
|
|
qWarning("Invalid file status: %d", status);
|
|
return true;
|
|
}
|
|
|
|
bool shouldProcessFileKind(uint8_t inKind)
|
|
{
|
|
auto kind = static_cast<TOX_FILE_KIND>(inKind);
|
|
|
|
switch (kind)
|
|
{
|
|
case TOX_FILE_KIND_DATA: return true;
|
|
// Avatar sharing should be seamless, the user does not need to see
|
|
// these in their file transfer list.
|
|
case TOX_FILE_KIND_AVATAR: return false;
|
|
}
|
|
|
|
qWarning("Unexpected file kind %d", kind);
|
|
return false;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
namespace FileTransferList
|
|
{
|
|
Column toFileTransferListColumn(int in) {
|
|
if (in >= 0 && in < static_cast<int>(Column::invalid)) {
|
|
return static_cast<Column>(in);
|
|
}
|
|
|
|
qWarning("Invalid file transfer list column %d", in);
|
|
return Column::invalid;
|
|
}
|
|
|
|
QString toQString(Column column) {
|
|
switch (column)
|
|
{
|
|
case Column::fileName:
|
|
return QObject::tr("File Name");
|
|
case Column::contact:
|
|
return QObject::tr("Contact");
|
|
case Column::progress:
|
|
return QObject::tr("Progress");
|
|
case Column::size:
|
|
return QObject::tr("Size");
|
|
case Column::speed:
|
|
return QObject::tr("Speed");
|
|
case Column::status:
|
|
return QObject::tr("Status");
|
|
case Column::control:
|
|
return QObject::tr("Control");
|
|
case Column::invalid:
|
|
break;
|
|
}
|
|
|
|
return "";
|
|
}
|
|
|
|
EditorAction toEditorAction(int in) {
|
|
if (in < 0 || in >= static_cast<int>(EditorAction::invalid)) {
|
|
qWarning("Unexpected editor action %d", in);
|
|
return EditorAction::invalid;
|
|
}
|
|
|
|
return static_cast<EditorAction>(in);
|
|
}
|
|
|
|
Model::Model(QObject* parent)
|
|
: QAbstractTableModel(parent)
|
|
{}
|
|
|
|
QVariant Model::headerData(int section, Qt::Orientation orientation, int role) const
|
|
{
|
|
if (role != Qt::DisplayRole) {
|
|
return QVariant();
|
|
}
|
|
|
|
if (orientation != Qt::Orientation::Horizontal) {
|
|
return QVariant();
|
|
}
|
|
|
|
const auto column = toFileTransferListColumn(section);
|
|
return toQString(column);
|
|
}
|
|
|
|
void Model::onFileUpdated(const ToxFile& file)
|
|
{
|
|
if (!shouldProcessFileKind(file.fileKind)) {
|
|
return;
|
|
}
|
|
|
|
auto idxIt = idToRow.find(file.resumeFileId);
|
|
int rowIdx = 0;
|
|
|
|
if (idxIt == idToRow.end()) {
|
|
if (files.size() >= std::numeric_limits<int>::max()) {
|
|
// Bug waiting to happen, but also what can we do if qt just doesn't
|
|
// support this many items in a list
|
|
qWarning("Too many file transfers rendered, ignoring");
|
|
return;
|
|
}
|
|
|
|
auto insertedIdx = files.size();
|
|
|
|
emit rowsAboutToBeInserted(QModelIndex(), insertedIdx, insertedIdx, {});
|
|
|
|
files.push_back(file);
|
|
idToRow.insert(file.resumeFileId, insertedIdx);
|
|
|
|
emit rowsInserted(QModelIndex(), insertedIdx, insertedIdx, {});
|
|
} else {
|
|
rowIdx = idxIt.value();
|
|
files[rowIdx] = file;
|
|
if (fileTransferFailed(file.status)) {
|
|
emit rowsAboutToBeRemoved(QModelIndex(), rowIdx, rowIdx, {});
|
|
|
|
for (auto it = idToRow.begin(); it != idToRow.end(); ++it) {
|
|
if (it.value() > rowIdx) {
|
|
it.value() -= 1;
|
|
}
|
|
}
|
|
idToRow.remove(file.resumeFileId);
|
|
files.erase(files.begin() + rowIdx);
|
|
|
|
emit rowsRemoved(QModelIndex(), rowIdx, rowIdx, {});
|
|
}
|
|
else {
|
|
emit dataChanged(index(rowIdx, 0), index(rowIdx, columnCount()));
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
int Model::rowCount(const QModelIndex& parent) const
|
|
{
|
|
return files.size();
|
|
}
|
|
|
|
int Model::columnCount(const QModelIndex& parent) const
|
|
{
|
|
return static_cast<int>(Column::invalid);
|
|
}
|
|
|
|
QVariant Model::data(const QModelIndex& index, int role) const
|
|
{
|
|
const auto row = index.row();
|
|
if (row < 0 || static_cast<size_t>(row) > files.size()) {
|
|
qWarning("Invalid file transfer row %d (files: %lu)", row, files.size());
|
|
return QVariant();
|
|
}
|
|
|
|
if (role == Qt::UserRole) {
|
|
return files[row].filePath;
|
|
}
|
|
|
|
if (role != Qt::DisplayRole) {
|
|
return QVariant();
|
|
}
|
|
|
|
const auto column = toFileTransferListColumn(index.column());
|
|
|
|
switch (column)
|
|
{
|
|
case Column::fileName:
|
|
return files[row].fileName;
|
|
case Column::contact:
|
|
{
|
|
auto f = FriendList::findFriend(FriendList::id2Key(files[row].friendId));
|
|
if (f == nullptr) {
|
|
qWarning("Invalid friend for file transfer");
|
|
return "Unknown";
|
|
}
|
|
|
|
return f->getDisplayedName();
|
|
}
|
|
case Column::progress:
|
|
return files[row].progress.getProgress() * 100.0;
|
|
case Column::size:
|
|
return getHumanReadableSize(files[row].progress.getFileSize());
|
|
case Column::speed:
|
|
return getHumanReadableSize(files[row].progress.getSpeed()) + "/s";
|
|
case Column::status:
|
|
return fileStatusString(files[row]);
|
|
case Column::control:
|
|
return files[row].pauseStatus.localPaused();
|
|
case Column::invalid:
|
|
break;
|
|
}
|
|
|
|
return QVariant();
|
|
}
|
|
|
|
bool Model::setData(const QModelIndex &index, const QVariant &value, int role)
|
|
{
|
|
const auto column = toFileTransferListColumn(index.column());
|
|
|
|
if (column != Column::control) {
|
|
return false;
|
|
}
|
|
|
|
if (!value.canConvert<int>()) {
|
|
qWarning("Unexpected model data");
|
|
return false;
|
|
}
|
|
|
|
const auto action = toEditorAction(value.toInt());
|
|
|
|
switch (action)
|
|
{
|
|
case EditorAction::cancel:
|
|
emit cancel(files[index.row()]);
|
|
break;
|
|
case EditorAction::pause:
|
|
emit togglePause(files[index.row()]);
|
|
break;
|
|
case EditorAction::invalid:
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
Delegate::Delegate(QWidget* parent)
|
|
: QStyledItemDelegate(parent)
|
|
{}
|
|
|
|
void Delegate::paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const
|
|
{
|
|
const auto column = toFileTransferListColumn(index.column());
|
|
switch (column)
|
|
{
|
|
case Column::progress:
|
|
{
|
|
int progress = index.data().toInt();
|
|
|
|
QStyleOptionProgressBar progressBarOption;
|
|
progressBarOption.rect = option.rect;
|
|
progressBarOption.minimum = 0;
|
|
progressBarOption.maximum = 100;
|
|
progressBarOption.progress = progress;
|
|
progressBarOption.text = QString::number(progress) + "%";
|
|
progressBarOption.textVisible = true;
|
|
|
|
QApplication::style()->drawControl(QStyle::CE_ProgressBar,
|
|
&progressBarOption, painter);
|
|
return;
|
|
}
|
|
case Column::control:
|
|
{
|
|
const auto data = index.data();
|
|
if (!data.canConvert<bool>()) {
|
|
qWarning("Unexpected control type, not rendering controls");
|
|
return;
|
|
}
|
|
const auto localPaused = data.toBool();
|
|
QPixmap pausePixmap = localPaused
|
|
? QPixmap(Style::getImagePath("fileTransferInstance/arrow_black.svg"))
|
|
: QPixmap(Style::getImagePath("fileTransferInstance/pause_dark.svg"));
|
|
QApplication::style()->drawItemPixmap(painter, pauseRect(option), Qt::AlignCenter, pausePixmap);
|
|
|
|
QPixmap stopPixmap(Style::getImagePath("fileTransferInstance/no_dark.svg"));
|
|
QApplication::style()->drawItemPixmap(painter, stopRect(option), Qt::AlignCenter, stopPixmap);
|
|
return;
|
|
}
|
|
case Column::fileName:
|
|
case Column::contact:
|
|
case Column::size:
|
|
case Column::speed:
|
|
case Column::status:
|
|
case Column::invalid:
|
|
break;
|
|
}
|
|
|
|
QStyledItemDelegate::paint(painter, option, index);
|
|
}
|
|
|
|
bool Delegate::editorEvent(QEvent* event, QAbstractItemModel* model,
|
|
const QStyleOptionViewItem& option, const QModelIndex& index)
|
|
{
|
|
if (toFileTransferListColumn(index.column()) == Column::control)
|
|
{
|
|
if (event->type() == QEvent::MouseButtonPress) {
|
|
auto mouseEvent = reinterpret_cast<QMouseEvent*>(event);
|
|
const auto pos = mouseEvent->pos();
|
|
const auto posRect = pauseRect(option);
|
|
const auto stRect = stopRect(option);
|
|
|
|
if (posRect.contains(pos)) {
|
|
model->setData(index, static_cast<int>(EditorAction::pause));
|
|
} else if (stRect.contains(pos)) {
|
|
model->setData(index, static_cast<int>(EditorAction::cancel));
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
|
|
View::View(QAbstractItemModel* model, QWidget* parent)
|
|
: QTableView(parent)
|
|
{
|
|
setModel(model);
|
|
|
|
// Resize to contents but stretch the file name to fill the full view
|
|
horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents);
|
|
horizontalHeader()->setSectionResizeMode(0, QHeaderView::Stretch);
|
|
// Visually tuned until it looked ok
|
|
horizontalHeader()->setMinimumSectionSize(75);
|
|
horizontalHeader()->setStretchLastSection(false);
|
|
verticalHeader()->hide();
|
|
setShowGrid(false);
|
|
setSelectionBehavior(QAbstractItemView::SelectRows);
|
|
setSelectionMode(QAbstractItemView::SingleSelection);
|
|
setItemDelegate(new Delegate(this));
|
|
}
|
|
|
|
View::~View() = default;
|
|
|
|
} // namespace FileTransferList
|
|
|
|
FilesForm::FilesForm(CoreFile& coreFile)
|
|
: QObject()
|
|
{
|
|
head = new QWidget();
|
|
QFont bold;
|
|
bold.setBold(true);
|
|
headLabel.setFont(bold);
|
|
head->setLayout(&headLayout);
|
|
headLayout.addWidget(&headLabel);
|
|
|
|
recvdModel = new FileTransferList::Model(this);
|
|
sentModel = new FileTransferList::Model(this);
|
|
|
|
auto pauseFile = [&coreFile] (ToxFile file) {
|
|
coreFile.pauseResumeFile(file.friendId, file.fileNum);
|
|
};
|
|
|
|
auto cancelFileRecv = [&coreFile] (ToxFile file) {
|
|
coreFile.cancelFileRecv(file.friendId, file.fileNum);
|
|
};
|
|
|
|
auto cancelFileSend = [&coreFile] (ToxFile file) {
|
|
coreFile.cancelFileSend(file.friendId, file.fileNum);
|
|
};
|
|
|
|
connect(recvdModel, &FileTransferList::Model::togglePause, pauseFile);
|
|
connect(recvdModel, &FileTransferList::Model::cancel, cancelFileRecv);
|
|
connect(sentModel, &FileTransferList::Model::togglePause, pauseFile);
|
|
connect(sentModel, &FileTransferList::Model::cancel, cancelFileSend);
|
|
|
|
recvd = new FileTransferList::View(recvdModel);
|
|
sent = new FileTransferList::View(sentModel);
|
|
|
|
main.addTab(recvd, QString());
|
|
main.addTab(sent, QString());
|
|
|
|
connect(sent, &QTableView::activated, this, &FilesForm::onSentFileActivated);
|
|
connect(recvd, &QTableView::activated, this, &FilesForm::onReceivedFileActivated);
|
|
|
|
retranslateUi();
|
|
Translator::registerHandler(std::bind(&FilesForm::retranslateUi, this), this);
|
|
}
|
|
|
|
FilesForm::~FilesForm()
|
|
{
|
|
Translator::unregister(this);
|
|
delete recvd;
|
|
delete sent;
|
|
head->deleteLater();
|
|
}
|
|
|
|
bool FilesForm::isShown() const
|
|
{
|
|
if (main.isVisible()) {
|
|
head->window()->windowHandle()->alert(0);
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
void FilesForm::show(ContentLayout* contentLayout)
|
|
{
|
|
contentLayout->mainContent->layout()->addWidget(&main);
|
|
contentLayout->mainHead->layout()->addWidget(head);
|
|
main.show();
|
|
head->show();
|
|
}
|
|
|
|
void FilesForm::onFileUpdated(const ToxFile& inFile)
|
|
{
|
|
if (!shouldProcessFileKind(inFile.fileKind)) {
|
|
return;
|
|
}
|
|
|
|
if (inFile.direction == ToxFile::SENDING) {
|
|
sentModel->onFileUpdated(inFile);
|
|
}
|
|
else if (inFile.direction == ToxFile::RECEIVING) {
|
|
recvdModel->onFileUpdated(inFile);
|
|
}
|
|
else {
|
|
qWarning("Unexpected file direction");
|
|
}
|
|
}
|
|
|
|
void FilesForm::onSentFileActivated(const QModelIndex& index)
|
|
{
|
|
const auto& filePath = sentModel->data(index, Qt::UserRole).toString();
|
|
Widget::confirmExecutableOpen(filePath);
|
|
}
|
|
|
|
void FilesForm::onReceivedFileActivated(const QModelIndex& index)
|
|
{
|
|
const auto& filePath = recvdModel->data(index, Qt::UserRole).toString();
|
|
Widget::confirmExecutableOpen(filePath);
|
|
}
|
|
|
|
void FilesForm::retranslateUi()
|
|
{
|
|
headLabel.setText(tr("Transferred files", "\"Headline\" of the window"));
|
|
main.setTabText(0, tr("Downloads"));
|
|
main.setTabText(1, tr("Uploads"));
|
|
}
|