diff --git a/cmake/Testing.cmake b/cmake/Testing.cmake
index 8c421862c..73083cde5 100644
--- a/cmake/Testing.cmake
+++ b/cmake/Testing.cmake
@@ -57,6 +57,7 @@ auto_test(model messageprocessor "")
auto_test(model sessionchatlog "")
auto_test(model exiftransform "")
auto_test(model notificationgenerator "${MOCK_SOURCES}")
+auto_test(widget filesform "")
if (UNIX)
auto_test(platform posixsignalnotifier "")
diff --git a/res.qrc b/res.qrc
index bb20bf79f..a99936fbf 100644
--- a/res.qrc
+++ b/res.qrc
@@ -100,10 +100,13 @@
themes/dark/chatArea/typing.svg
themes/dark/chatArea/error.svg
themes/dark/fileTransferInstance/no.svg
+ themes/dark/fileTransferInstance/no_dark.svg
themes/dark/fileTransferInstance/pause.svg
+ themes/dark/fileTransferInstance/pause_dark.svg
themes/dark/fileTransferInstance/yes.svg
themes/dark/fileTransferInstance/dir.svg
themes/dark/fileTransferInstance/arrow_white.svg
+ themes/dark/fileTransferInstance/arrow_black.svg
themes/dark/fileTransferInstance/browse.svg
themes/dark/fileTransferInstance/filetransferWidget.css
themes/dark/genericChatForm/genericChatForm.css
@@ -164,10 +167,13 @@
themes/default/chatArea/typing.svg
themes/default/chatArea/error.svg
themes/default/fileTransferInstance/no.svg
+ themes/default/fileTransferInstance/no_dark.svg
themes/default/fileTransferInstance/pause.svg
+ themes/default/fileTransferInstance/pause_dark.svg
themes/default/fileTransferInstance/yes.svg
themes/default/fileTransferInstance/dir.svg
themes/default/fileTransferInstance/arrow_white.svg
+ themes/default/fileTransferInstance/arrow_black.svg
themes/default/fileTransferInstance/browse.svg
themes/default/fileTransferInstance/filetransferWidget.css
themes/default/genericChatForm/genericChatForm.css
diff --git a/src/chatlog/content/filetransferwidget.cpp b/src/chatlog/content/filetransferwidget.cpp
index fd80ec6e2..411dad209 100644
--- a/src/chatlog/content/filetransferwidget.cpp
+++ b/src/chatlog/content/filetransferwidget.cpp
@@ -26,6 +26,7 @@
#include "src/widget/style.h"
#include "src/widget/widget.h"
#include "src/model/exiftransform.h"
+#include "util/display.h"
#include
#include
@@ -234,18 +235,6 @@ void FileTransferWidget::reloadTheme()
updateBackgroundColor(lastStatus);
}
-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(log(size) / log(1024)), static_cast(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) {
diff --git a/src/chatlog/content/filetransferwidget.h b/src/chatlog/content/filetransferwidget.h
index 50244afe2..3b639e836 100644
--- a/src/chatlog/content/filetransferwidget.h
+++ b/src/chatlog/content/filetransferwidget.h
@@ -42,8 +42,6 @@ public:
explicit FileTransferWidget(QWidget* parent, CoreFile& _coreFile, ToxFile file);
virtual ~FileTransferWidget();
bool isActive() const;
- static QString getHumanReadableSize(qint64 size);
-
void onFileTransferUpdate(ToxFile file);
protected:
diff --git a/src/core/corefile.cpp b/src/core/corefile.cpp
index 7c0114357..82ce93f08 100644
--- a/src/core/corefile.cpp
+++ b/src/core/corefile.cpp
@@ -448,7 +448,6 @@ void CoreFile::onFileDataCallback(Tox* tox, uint32_t friendId, uint32_t fileId,
file->status = ToxFile::FINISHED;
if (file->fileKind != TOX_FILE_KIND_AVATAR) {
emit coreFile->fileTransferFinished(*file);
- emit coreFile->fileUploadFinished(file->filePath);
}
coreFile->removeFile(friendId, fileId);
return;
@@ -519,7 +518,6 @@ void CoreFile::onFileRecvChunkCallback(Tox* tox, uint32_t friendId, uint32_t fil
}
} else {
emit coreFile->fileTransferFinished(*file);
- emit coreFile->fileDownloadFinished(file->filePath);
}
coreFile->removeFile(friendId, fileId);
return;
diff --git a/src/core/corefile.h b/src/core/corefile.h
index 062eba913..3085ac007 100644
--- a/src/core/corefile.h
+++ b/src/core/corefile.h
@@ -69,8 +69,6 @@ signals:
void fileTransferAccepted(ToxFile file);
void fileTransferCancelled(ToxFile file);
void fileTransferFinished(ToxFile file);
- void fileUploadFinished(const QString& path);
- void fileDownloadFinished(const QString& path);
void fileTransferPaused(ToxFile file);
void fileTransferInfo(ToxFile file);
void fileTransferRemotePausedUnpaused(ToxFile file, bool paused);
diff --git a/src/model/notificationgenerator.cpp b/src/model/notificationgenerator.cpp
index 83f8fd9a2..f25228ad1 100644
--- a/src/model/notificationgenerator.cpp
+++ b/src/model/notificationgenerator.cpp
@@ -19,6 +19,7 @@
#include "notificationgenerator.h"
#include "src/chatlog/content/filetransferwidget.h"
+#include "util/display.h"
#include
@@ -223,7 +224,7 @@ NotificationData NotificationGenerator::fileTransferNotification(const Friend* f
{
//: e.g. Bob - file transfer
ret.title = tr("%1 - file transfer").arg(f->getDisplayedName());
- ret.message = filename + " (" + FileTransferWidget::getHumanReadableSize(fileSize) + ")";
+ ret.message = filename + " (" + getHumanReadableSize(fileSize) + ")";
}
ret.pixmap = getSenderAvatar(profile, f->getPublicKey());
diff --git a/src/widget/form/filesform.cpp b/src/widget/form/filesform.cpp
index 2778c5812..be29f8790 100644
--- a/src/widget/form/filesform.cpp
+++ b/src/widget/form/filesform.cpp
@@ -18,16 +18,410 @@
*/
#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
#include
+#include
+#include
+#include
+#include
+#include
+#include
-FilesForm::FilesForm()
+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(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(Column::invalid)) {
+ return static_cast(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(EditorAction::invalid)) {
+ qWarning("Unexpected editor action %d", in);
+ return EditorAction::invalid;
+ }
+
+ return static_cast(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::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(Column::invalid);
+ }
+
+ QVariant Model::data(const QModelIndex& index, int role) const
+ {
+ const auto row = index.row();
+ if (row < 0 || static_cast(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()) {
+ 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()) {
+ 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(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(EditorAction::pause));
+ } else if (stRect.contains(pos)) {
+ model->setData(index, static_cast(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()
- , doneIcon(Style::getImagePath("fileTransferWidget/fileDone.svg"))
{
head = new QWidget();
QFont bold;
@@ -36,14 +430,34 @@ FilesForm::FilesForm()
head->setLayout(&headLayout);
headLayout.addWidget(&headLabel);
- recvd = new QListWidget;
- sent = new QListWidget;
+ 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, &QListWidget::itemActivated, this, &FilesForm::onFileActivated);
- connect(recvd, &QListWidget::itemActivated, this, &FilesForm::onFileActivated);
+ connect(sent, &QTableView::activated, this, &FilesForm::onSentFileActivated);
+ connect(recvd, &QTableView::activated, this, &FilesForm::onReceivedFileActivated);
retranslateUi();
Translator::registerHandler(std::bind(&FilesForm::retranslateUi, this), this);
@@ -75,28 +489,33 @@ void FilesForm::show(ContentLayout* contentLayout)
head->show();
}
-void FilesForm::onFileDownloadComplete(const QString& path)
+void FilesForm::onFileUpdated(const ToxFile& inFile)
{
- QListWidgetItem* tmp = new QListWidgetItem(doneIcon, QFileInfo(path).fileName());
- tmp->setData(Qt::UserRole, path);
- recvd->addItem(tmp);
+ 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::onFileUploadComplete(const QString& path)
+void FilesForm::onSentFileActivated(const QModelIndex& index)
{
- QListWidgetItem* tmp = new QListWidgetItem(doneIcon, QFileInfo(path).fileName());
- tmp->setData(Qt::UserRole, path);
- sent->addItem(tmp);
+ const auto& filePath = sentModel->data(index, Qt::UserRole).toString();
+ Widget::confirmExecutableOpen(filePath);
}
-// sadly, the ToxFile struct in core only has the file name, not the file path...
-// so currently, these don't work as intended (though for now, downloads might work
-// whenever they're not saved anywhere custom, thanks to the hack)
-// I could do some digging around, but for now I'm tired and others already
-// might know it without me needing to dig, so...
-void FilesForm::onFileActivated(QListWidgetItem* item)
+void FilesForm::onReceivedFileActivated(const QModelIndex& index)
{
- Widget::confirmExecutableOpen(QFileInfo(item->data(Qt::UserRole).toString()));
+ const auto& filePath = recvdModel->data(index, Qt::UserRole).toString();
+ Widget::confirmExecutableOpen(filePath);
}
void FilesForm::retranslateUi()
diff --git a/src/widget/form/filesform.h b/src/widget/form/filesform.h
index 2322ea140..b39c8035f 100644
--- a/src/widget/form/filesform.h
+++ b/src/widget/form/filesform.h
@@ -19,41 +19,121 @@
#pragma once
+#include "src/core/toxfile.h"
+#include "src/core/corefile.h"
+
#include
#include
+#include
#include
#include
#include
+#include
+#include
+#include
class ContentLayout;
-class QListWidget;
+class QTableView;
+
+namespace FileTransferList
+{
+
+ enum class Column : int {
+ // NOTE: Order defines order in UI
+ fileName,
+ contact,
+ progress,
+ size,
+ speed,
+ status,
+ control,
+ invalid
+ };
+
+ Column toFileTransferListColumn(int in);
+ QString toQString(Column column);
+
+ enum class EditorAction : int {
+ pause,
+ cancel,
+ invalid,
+ };
+
+ EditorAction toEditorAction(int in);
+
+ class Model : public QAbstractTableModel
+ {
+ Q_OBJECT
+ public:
+ Model(QObject* parent = nullptr);
+ ~Model() = default;
+
+ void onFileUpdated(const ToxFile& file);
+
+ QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override;
+ int rowCount(const QModelIndex& parent = QModelIndex()) const override;
+ int columnCount(const QModelIndex& parent = QModelIndex()) const override;
+ QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override;
+ bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override;
+
+ signals:
+ void togglePause(ToxFile file);
+ void cancel(ToxFile file);
+
+ private:
+ QHash idToRow;
+ std::vector files;
+ };
+
+ class Delegate : public QStyledItemDelegate
+ {
+ public:
+ Delegate(QWidget* parent = nullptr);
+ void paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const override;
+
+ bool editorEvent(QEvent* event, QAbstractItemModel* model, const QStyleOptionViewItem& option, const QModelIndex& index) override;
+ };
+
+ class View : public QTableView
+ {
+ public:
+ View(QAbstractItemModel* model, QWidget* parent = nullptr);
+ ~View();
+
+ };
+} // namespace FileTransferList
class FilesForm : public QObject
{
Q_OBJECT
public:
- FilesForm();
+ FilesForm(CoreFile& coreFile);
~FilesForm();
bool isShown() const;
void show(ContentLayout* contentLayout);
public slots:
- void onFileDownloadComplete(const QString& path);
- void onFileUploadComplete(const QString& path);
+ void onFileUpdated(const ToxFile& file);
private slots:
- void onFileActivated(QListWidgetItem* item);
+ void onSentFileActivated(const QModelIndex& item);
+ void onReceivedFileActivated(const QModelIndex& item);
private:
+ struct FileInfo
+ {
+ QListWidgetItem* item = nullptr;
+ ToxFile file;
+ };
+
void retranslateUi();
-private:
QWidget* head;
- QIcon doneIcon;
QLabel headLabel;
QVBoxLayout headLayout;
QTabWidget main;
- QListWidget *sent, *recvd;
+ QTableView *sent, *recvd;
+ FileTransferList::Model *sentModel, *recvdModel;
};
diff --git a/src/widget/widget.cpp b/src/widget/widget.cpp
index 14c8d31ea..3767913a7 100644
--- a/src/widget/widget.cpp
+++ b/src/widget/widget.cpp
@@ -279,7 +279,8 @@ void Widget::init()
Style::setThemeColor(settings.getThemeColor());
- filesForm = new FilesForm();
+ CoreFile* coreFile = core->getCoreFile();
+ filesForm = new FilesForm(*coreFile);
addFriendForm = new AddFriendForm(core->getSelfId());
groupInviteForm = new GroupInviteForm;
@@ -292,7 +293,6 @@ void Widget::init()
updateCheck->checkForUpdate();
#endif
- CoreFile* coreFile = core->getCoreFile();
profileInfo = new ProfileInfo(core, &profile);
profileForm = new ProfileForm(profileInfo);
@@ -307,8 +307,6 @@ void Widget::init()
connect(&profile, &Profile::selfAvatarChanged, profileForm, &ProfileForm::onSelfAvatarLoaded);
connect(coreFile, &CoreFile::fileReceiveRequested, this, &Widget::onFileReceiveRequested);
- connect(coreFile, &CoreFile::fileDownloadFinished, filesForm, &FilesForm::onFileDownloadComplete);
- connect(coreFile, &CoreFile::fileUploadFinished, filesForm, &FilesForm::onFileUploadComplete);
connect(ui->addButton, &QPushButton::clicked, this, &Widget::onAddClicked);
connect(ui->groupButton, &QPushButton::clicked, this, &Widget::onGroupClicked);
connect(ui->transferButton, &QPushButton::clicked, this, &Widget::onTransferClicked);
@@ -1136,6 +1134,8 @@ void Widget::dispatchFile(ToxFile file)
const auto senderPk = (file.direction == ToxFile::SENDING) ? core->getSelfPublicKey() : pk;
friendChatLogs[pk]->onFileUpdated(senderPk, file);
+
+ filesForm->onFileUpdated(file);
}
void Widget::dispatchFileWithBool(ToxFile file, bool)
diff --git a/test/widget/filesform_test.cpp b/test/widget/filesform_test.cpp
new file mode 100644
index 000000000..d5bf85ea5
--- /dev/null
+++ b/test/widget/filesform_test.cpp
@@ -0,0 +1,282 @@
+/*
+ Copyright © 2021 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 .
+*/
+
+#include "src/widget/form/filesform.h"
+#include "src/friendlist.h"
+#include "src/model/friend.h"
+
+#include
+#include
+
+class TestFileTransferList : public QObject
+{
+ Q_OBJECT
+private slots:
+
+ void testFileTransferListConversion();
+ void testEditorActionConversion();
+
+ void testFileName();
+ // NOTE: Testing contact return requires a lookup in FriendList which goes
+ // down a large dependency chain that is not linked to this test
+ // void testContact();
+ void testProgress();
+ void testSize();
+ void testSpeed();
+ void testStatus();
+ void testControl();
+ void testAvatarIgnored();
+ void testMultipleFiles();
+ void testFileRemoval();
+};
+
+using namespace FileTransferList;
+
+void TestFileTransferList::testFileTransferListConversion()
+{
+ Model model;
+ for (int i = 0; i < model.columnCount(); ++i) {
+ QVERIFY(toFileTransferListColumn(i) != Column::invalid);
+ }
+ QCOMPARE(toFileTransferListColumn(100), Column::invalid);
+}
+
+void TestFileTransferList::testEditorActionConversion()
+{
+ QCOMPARE(toEditorAction(static_cast(EditorAction::pause)), EditorAction::pause);
+ QCOMPARE(toEditorAction(static_cast(EditorAction::cancel)), EditorAction::cancel);
+ QCOMPARE(toEditorAction(55), EditorAction::invalid);
+}
+
+void TestFileTransferList::testFileName()
+{
+ Model model;
+
+ ToxFile file;
+ file.fileKind = TOX_FILE_KIND_DATA;
+ file.fileName = "Test";
+ model.onFileUpdated(file);
+
+ const auto idx = model.index(0, static_cast(Column::fileName));
+ const auto fileName = idx.data();
+
+ QCOMPARE(fileName.toString(), QString("Test"));
+}
+
+void TestFileTransferList::testProgress()
+{
+ Model model;
+
+ ToxFile file(0, 0, "", "", 1000, ToxFile::FileDirection::SENDING);
+ file.progress.addSample(100, QTime(1, 0, 0));
+ model.onFileUpdated(file);
+
+ const auto idx = model.index(0, static_cast(Column::progress));
+ const auto progress = idx.data();
+
+ // Progress should be in percent units, 100/1000 == 10
+ QCOMPARE(progress.toFloat(), 10.0f);
+}
+
+void TestFileTransferList::testSize()
+{
+ Model model;
+
+ ToxFile file(0, 0, "", "", 1000, ToxFile::FileDirection::SENDING);
+ model.onFileUpdated(file);
+
+ const auto idx = model.index(0, static_cast(Column::size));
+ auto size = idx.data();
+
+ // Size should be a human readable string
+ QCOMPARE(size.toString(), QString("1000B"));
+
+ // 1GB + a little to avoid floating point inaccuracy
+ file = ToxFile(0, 0, "", "", 1024 * 1024 * 1024 + 2, ToxFile::FileDirection::SENDING);
+ model.onFileUpdated(file);
+ size = idx.data();
+ QCOMPARE(size.toString(), QString("1.00GiB"));
+}
+
+void TestFileTransferList::testSpeed()
+{
+ Model model;
+
+ ToxFile file(0, 0, "", "", 1024 * 1024, ToxFile::FileDirection::SENDING);
+ file.progress.addSample(100 * 1024, QTime(1, 0, 0));
+ file.progress.addSample(200 * 1024, QTime(1, 0, 1));
+ model.onFileUpdated(file);
+
+ const auto idx = model.index(0, static_cast(Column::speed));
+ const auto speed = idx.data();
+
+ // Speed should be a human readable string
+ QCOMPARE(speed.toString(), QString("100KiB/s"));
+}
+
+void TestFileTransferList::testStatus()
+{
+ Model model;
+
+ ToxFile file(0, 0, "", "", 1024 * 1024, ToxFile::FileDirection::SENDING);
+ file.status = ToxFile::TRANSMITTING;
+ model.onFileUpdated(file);
+
+ const auto idx = model.index(0, static_cast(Column::status));
+ auto status = idx.data();
+
+ QCOMPARE(status.toString(), QString("Transmitting"));
+
+ file.status = ToxFile::PAUSED;
+ file.pauseStatus.remotePause();
+ model.onFileUpdated(file);
+ status = idx.data();
+
+ QCOMPARE(status.toString(), QString("Remote paused"));
+
+ file.status = ToxFile::PAUSED;
+ file.pauseStatus.localPause();
+ file.pauseStatus.remoteResume();
+ model.onFileUpdated(file);
+ status = idx.data();
+
+ QCOMPARE(status.toString(), QString("Paused"));
+}
+
+void TestFileTransferList::testControl()
+{
+ Model model;
+ bool cancelCalled = false;
+ bool pauseCalled = false;
+
+ QObject::connect(&model, &Model::cancel, [&] (ToxFile file) {
+ cancelCalled = true;
+ });
+
+ QObject::connect(&model, &Model::togglePause, [&] (ToxFile file) {
+ pauseCalled = true;
+ });
+
+ ToxFile file(0, 0, "", "", 1024 * 1024, ToxFile::FileDirection::SENDING);
+ file.status = ToxFile::TRANSMITTING;
+ model.onFileUpdated(file);
+
+ const auto idx = model.index(0, static_cast(Column::control));
+ model.setData(idx, static_cast(EditorAction::pause));
+
+ QVERIFY(pauseCalled);
+ QVERIFY(!cancelCalled);
+
+ pauseCalled = false;
+ model.setData(idx, static_cast(EditorAction::cancel));
+ QVERIFY(!pauseCalled);
+ QVERIFY(cancelCalled);
+
+ file.status = ToxFile::TRANSMITTING;
+ model.onFileUpdated(file);
+ // True if paused
+ QCOMPARE(idx.data().toBool(), false);
+
+ file.status = ToxFile::PAUSED;
+ file.pauseStatus.localPause();
+ model.onFileUpdated(file);
+ // True if _local_ paused
+ QCOMPARE(idx.data().toBool(), true);
+}
+
+void TestFileTransferList::testAvatarIgnored()
+{
+ Model model;
+
+ ToxFile file;
+ file.fileKind = TOX_FILE_KIND_AVATAR;
+ model.onFileUpdated(file);
+
+ QCOMPARE(model.rowCount(), 0);
+}
+
+void TestFileTransferList::testMultipleFiles()
+{
+ Model model;
+
+ ToxFile file;
+ file.resumeFileId = QByteArray();
+ file.fileKind = TOX_FILE_KIND_DATA;
+ file.status = ToxFile::TRANSMITTING;
+ file.fileName = "a";
+ model.onFileUpdated(file);
+
+ // File map keys off resume file ID
+ file.resumeFileId = QByteArray("asdfasdf");
+ file.fileName = "b";
+ model.onFileUpdated(file);
+
+ QCOMPARE(model.rowCount(), 2);
+
+ auto idx = model.index(0, static_cast(Column::fileName));
+ QCOMPARE(idx.data().toString(), QString("a"));
+
+ idx = model.index(1, static_cast(Column::fileName));
+ QCOMPARE(idx.data().toString(), QString("b"));
+
+ // File name should be updated instead of inserting a new file since the
+ // resume file ID is the same
+ file.fileName = "c";
+ model.onFileUpdated(file);
+ QCOMPARE(model.rowCount(), 2);
+ QCOMPARE(idx.data().toString(), QString("c"));
+}
+
+void TestFileTransferList::testFileRemoval()
+{
+ // Model should keep files in the list if they are finished, but not if they
+ // were broken or canceled
+
+ Model model;
+
+ ToxFile file;
+ file.fileKind = TOX_FILE_KIND_DATA;
+ file.status = ToxFile::TRANSMITTING;
+ model.onFileUpdated(file);
+
+ QCOMPARE(model.rowCount(), 1);
+
+ file.status = ToxFile::BROKEN;
+ model.onFileUpdated(file);
+ QCOMPARE(model.rowCount(), 0);
+
+ file.status = ToxFile::TRANSMITTING;
+ model.onFileUpdated(file);
+ QCOMPARE(model.rowCount(), 1);
+
+ file.status = ToxFile::CANCELED;
+ model.onFileUpdated(file);
+ QCOMPARE(model.rowCount(), 0);
+
+ file.status = ToxFile::TRANSMITTING;
+ model.onFileUpdated(file);
+ QCOMPARE(model.rowCount(), 1);
+
+ file.status = ToxFile::FINISHED;
+ model.onFileUpdated(file);
+ QCOMPARE(model.rowCount(), 1);
+}
+
+QTEST_GUILESS_MAIN(TestFileTransferList)
+#include "filesform_test.moc"
diff --git a/themes/dark/fileTransferInstance/arrow_black.svg b/themes/dark/fileTransferInstance/arrow_black.svg
new file mode 100644
index 000000000..a10b55c94
--- /dev/null
+++ b/themes/dark/fileTransferInstance/arrow_black.svg
@@ -0,0 +1,44 @@
+
+
+
+
diff --git a/themes/dark/fileTransferInstance/no_dark.svg b/themes/dark/fileTransferInstance/no_dark.svg
new file mode 100644
index 000000000..dabe9996b
--- /dev/null
+++ b/themes/dark/fileTransferInstance/no_dark.svg
@@ -0,0 +1,47 @@
+
+
+
+
diff --git a/themes/dark/fileTransferInstance/pause_dark.svg b/themes/dark/fileTransferInstance/pause_dark.svg
new file mode 100644
index 000000000..a5a87edfa
--- /dev/null
+++ b/themes/dark/fileTransferInstance/pause_dark.svg
@@ -0,0 +1,58 @@
+
+
+
+
diff --git a/themes/default/fileTransferInstance/arrow_black.svg b/themes/default/fileTransferInstance/arrow_black.svg
new file mode 100644
index 000000000..a10b55c94
--- /dev/null
+++ b/themes/default/fileTransferInstance/arrow_black.svg
@@ -0,0 +1,44 @@
+
+
+
+
diff --git a/themes/default/fileTransferInstance/no_dark.svg b/themes/default/fileTransferInstance/no_dark.svg
new file mode 100644
index 000000000..dabe9996b
--- /dev/null
+++ b/themes/default/fileTransferInstance/no_dark.svg
@@ -0,0 +1,47 @@
+
+
+
+
diff --git a/themes/default/fileTransferInstance/pause_dark.svg b/themes/default/fileTransferInstance/pause_dark.svg
new file mode 100644
index 000000000..a5a87edfa
--- /dev/null
+++ b/themes/default/fileTransferInstance/pause_dark.svg
@@ -0,0 +1,58 @@
+
+
+
+
diff --git a/util/CMakeLists.txt b/util/CMakeLists.txt
index 945b8b438..5952aca29 100644
--- a/util/CMakeLists.txt
+++ b/util/CMakeLists.txt
@@ -14,13 +14,15 @@
# You should have received a copy of the GNU General Public License
# along with qTox. If not, see
-set(HEADER_LIST
+
+add_library(util_library STATIC
"include/util/compatiblerecursivemutex.h"
"include/util/interface.h"
- "include/util/strongtype.h")
-
-add_library(util_library STATIC ${HEADER_LIST})
+ "include/util/strongtype.h"
+ "include/util/display.h"
+ "src/display.cpp")
# We need this directory, and users of our library will need it too
target_include_directories(util_library PUBLIC include/)
+target_link_libraries(util_library PRIVATE Qt5::Core)
diff --git a/util/include/util/display.h b/util/include/util/display.h
new file mode 100644
index 000000000..8980d5fb9
--- /dev/null
+++ b/util/include/util/display.h
@@ -0,0 +1,26 @@
+/*
+ Copyright © 2021 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 .
+*/
+
+#pragma once
+
+#include
+
+#include
+
+QString getHumanReadableSize(uint64_t sizeBytes);
diff --git a/util/src/display.cpp b/util/src/display.cpp
new file mode 100644
index 000000000..920e1f5f1
--- /dev/null
+++ b/util/src/display.cpp
@@ -0,0 +1,34 @@
+/*
+ Copyright © 2021 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 .
+*/
+
+#include "util/display.h"
+
+#include
+
+QString getHumanReadableSize(uint64_t size)
+{
+ static const char* suffix[] = {"B", "KiB", "MiB", "GiB", "TiB"};
+ int exp = 0;
+
+ if (size > 0) {
+ exp = std::min(static_cast(log(size) / log(1024)), static_cast(sizeof(suffix) / sizeof(suffix[0]) - 1));
+ }
+
+ return QString().setNum(size / pow(1024, exp), 'f', exp > 1 ? 2 : 0).append(suffix[exp]);
+}