qTox  Version: nightly | Commit: bc751c8e1cac455f9690654fcfe0f560d2d7dfdd
filesform.cpp
Go to the documentation of this file.
1 /*
2  Copyright © 2014-2019 by The qTox Project Contributors
3 
4  This file is part of qTox, a Qt-based graphical interface for Tox.
5 
6  qTox is libre software: you can redistribute it and/or modify
7  it under the terms of the GNU General Public License as published by
8  the Free Software Foundation, either version 3 of the License, or
9  (at your option) any later version.
10 
11  qTox is distributed in the hope that it will be useful,
12  but WITHOUT ANY WARRANTY; without even the implied warranty of
13  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14  GNU General Public License for more details.
15 
16  You should have received a copy of the GNU General Public License
17  along with qTox. If not, see <http://www.gnu.org/licenses/>.
18 */
19 
20 #include "filesform.h"
21 #include "src/core/toxfile.h"
23 #include "src/widget/translator.h"
24 #include "src/widget/style.h"
25 #include "src/widget/widget.h"
26 #include "src/friendlist.h"
27 #include "util/display.h"
28 #include <QFileInfo>
29 #include <QWindow>
30 #include <QTableView>
31 #include <QHeaderView>
32 #include <QPushButton>
33 #include <QPainter>
34 #include <QMouseEvent>
35 #include <cmath>
36 
37 namespace {
38  QRect pauseRect(const QStyleOptionViewItem& option)
39  {
40  float controlSize = option.rect.height() * 0.8f;
41  float rectWidth = option.rect.width();
42  float buttonHorizontalArea = rectWidth / 2;
43 
44  // To center the button, we find the horizontal center and subtract half
45  // our width from it
46  int buttonXPos = std::round(option.rect.x() + buttonHorizontalArea / 2 - controlSize / 2);
47  int buttonYPos = std::round(option.rect.y() + option.rect.height() * 0.1f);
48  return QRect(buttonXPos, buttonYPos, controlSize, controlSize);
49  }
50 
51  QRect stopRect(const QStyleOptionViewItem& option)
52  {
53  float controlSize = option.rect.height() * 0.8;
54  float rectWidth = option.rect.width();
55  float buttonHorizontalArea = rectWidth / 2;
56 
57  // To center the button, we find the horizontal center and subtract half
58  // our width from it
59  int buttonXPos = std::round(option.rect.x() + buttonHorizontalArea + buttonHorizontalArea / 2 - controlSize / 2);
60  int buttonYPos = std::round(option.rect.y() + option.rect.height() * 0.1f);
61  return QRect(buttonXPos, buttonYPos, controlSize, controlSize);
62  }
63 
64  QString fileStatusString(ToxFile file)
65  {
66  switch (file.status)
67  {
69  return QObject::tr("Initializing");
71  return QObject::tr("Transmitting");
72  case ToxFile::FINISHED:
73  return QObject::tr("Finished");
74  case ToxFile::BROKEN:
75  return QObject::tr("Broken");
76  case ToxFile::CANCELED:
77  return QObject::tr("Canceled");
78  case ToxFile::PAUSED:
79  if (file.pauseStatus.localPaused()) {
80  return QObject::tr("Paused");
81  } else {
82  return QObject::tr("Remote paused");
83  }
84  }
85 
86  qWarning("Corrupt file status %d", file.status);
87  return "";
88  }
89 
90  bool fileTransferFailed(const ToxFile::FileStatus& status) {
91  switch (status)
92  {
94  case ToxFile::PAUSED:
96  case ToxFile::FINISHED:
97  return false;
98  case ToxFile::BROKEN:
99  case ToxFile::CANCELED:
100  return true;
101  }
102 
103  qWarning("Invalid file status: %d", status);
104  return true;
105  }
106 
107  bool shouldProcessFileKind(uint8_t inKind)
108  {
109  auto kind = static_cast<TOX_FILE_KIND>(inKind);
110 
111  switch (kind)
112  {
113  case TOX_FILE_KIND_DATA: return true;
114  // Avatar sharing should be seamless, the user does not need to see
115  // these in their file transfer list.
116  case TOX_FILE_KIND_AVATAR: return false;
117  }
118 
119  qWarning("Unexpected file kind %d", kind);
120  return false;
121  }
122 
123 } // namespace
124 
126 {
128  if (in >= 0 && in < static_cast<int>(Column::invalid)) {
129  return static_cast<Column>(in);
130  }
131 
132  qWarning("Invalid file transfer list column %d", in);
133  return Column::invalid;
134  }
135 
136  QString toQString(Column column) {
137  switch (column)
138  {
139  case Column::fileName:
140  return QObject::tr("File Name");
141  case Column::contact:
142  return QObject::tr("Contact");
143  case Column::progress:
144  return QObject::tr("Progress");
145  case Column::size:
146  return QObject::tr("Size");
147  case Column::speed:
148  return QObject::tr("Speed");
149  case Column::status:
150  return QObject::tr("Status");
151  case Column::control:
152  return QObject::tr("Control");
153  case Column::invalid:
154  break;
155  }
156 
157  return "";
158  }
159 
161  if (in < 0 || in >= static_cast<int>(EditorAction::invalid)) {
162  qWarning("Unexpected editor action %d", in);
163  return EditorAction::invalid;
164  }
165 
166  return static_cast<EditorAction>(in);
167  }
168 
169  Model::Model(QObject* parent)
170  : QAbstractTableModel(parent)
171  {}
172 
173  QVariant Model::headerData(int section, Qt::Orientation orientation, int role) const
174  {
175  if (role != Qt::DisplayRole) {
176  return QVariant();
177  }
178 
179  if (orientation != Qt::Orientation::Horizontal) {
180  return QVariant();
181  }
182 
183  const auto column = toFileTransferListColumn(section);
184  return toQString(column);
185  }
186 
187  void Model::onFileUpdated(const ToxFile& file)
188  {
189  if (!shouldProcessFileKind(file.fileKind)) {
190  return;
191  }
192 
193  auto idxIt = idToRow.find(file.resumeFileId);
194  int rowIdx = 0;
195 
196  if (idxIt == idToRow.end()) {
197  if (files.size() >= std::numeric_limits<int>::max()) {
198  // Bug waiting to happen, but also what can we do if qt just doesn't
199  // support this many items in a list
200  qWarning("Too many file transfers rendered, ignoring");
201  return;
202  }
203 
204  auto insertedIdx = files.size();
205 
206  emit rowsAboutToBeInserted(QModelIndex(), insertedIdx, insertedIdx, {});
207 
208  files.push_back(file);
209  idToRow.insert(file.resumeFileId, insertedIdx);
210 
211  emit rowsInserted(QModelIndex(), insertedIdx, insertedIdx, {});
212  } else {
213  rowIdx = idxIt.value();
214  files[rowIdx] = file;
215  if (fileTransferFailed(file.status)) {
216  emit rowsAboutToBeRemoved(QModelIndex(), rowIdx, rowIdx, {});
217 
218  for (auto it = idToRow.begin(); it != idToRow.end(); ++it) {
219  if (it.value() > rowIdx) {
220  it.value() -= 1;
221  }
222  }
223  idToRow.remove(file.resumeFileId);
224  files.erase(files.begin() + rowIdx);
225 
226  emit rowsRemoved(QModelIndex(), rowIdx, rowIdx, {});
227  }
228  else {
229  emit dataChanged(index(rowIdx, 0), index(rowIdx, columnCount()));
230  }
231  }
232 
233  }
234 
235  int Model::rowCount(const QModelIndex& parent) const
236  {
237  return files.size();
238  }
239 
240  int Model::columnCount(const QModelIndex& parent) const
241  {
242  return static_cast<int>(Column::invalid);
243  }
244 
245  QVariant Model::data(const QModelIndex& index, int role) const
246  {
247  const auto row = index.row();
248  if (row < 0 || static_cast<size_t>(row) > files.size()) {
249  qWarning("Invalid file transfer row %d (files: %lu)", row, files.size());
250  return QVariant();
251  }
252 
253  if (role == Qt::UserRole) {
254  return files[row].filePath;
255  }
256 
257  if (role != Qt::DisplayRole) {
258  return QVariant();
259  }
260 
261  const auto column = toFileTransferListColumn(index.column());
262 
263  switch (column)
264  {
265  case Column::fileName:
266  return files[row].fileName;
267  case Column::contact:
268  {
269  auto f = FriendList::findFriend(FriendList::id2Key(files[row].friendId));
270  if (f == nullptr) {
271  qWarning("Invalid friend for file transfer");
272  return "Unknown";
273  }
274 
275  return f->getDisplayedName();
276  }
277  case Column::progress:
278  return files[row].progress.getProgress() * 100.0;
279  case Column::size:
280  return getHumanReadableSize(files[row].progress.getFileSize());
281  case Column::speed:
282  return getHumanReadableSize(files[row].progress.getSpeed()) + "/s";
283  case Column::status:
284  return fileStatusString(files[row]);
285  case Column::control:
286  return files[row].pauseStatus.localPaused();
287  case Column::invalid:
288  break;
289  }
290 
291  return QVariant();
292  }
293 
294  bool Model::setData(const QModelIndex &index, const QVariant &value, int role)
295  {
296  const auto column = toFileTransferListColumn(index.column());
297 
298  if (column != Column::control) {
299  return false;
300  }
301 
302  if (!value.canConvert<int>()) {
303  qWarning("Unexpected model data");
304  return false;
305  }
306 
307  const auto action = toEditorAction(value.toInt());
308 
309  switch (action)
310  {
312  emit cancel(files[index.row()]);
313  break;
314  case EditorAction::pause:
315  emit togglePause(files[index.row()]);
316  break;
318  return false;
319  }
320 
321  return true;
322  }
323 
324  Delegate::Delegate(QWidget* parent)
325  : QStyledItemDelegate(parent)
326  {}
327 
328  void Delegate::paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const
329  {
330  const auto column = toFileTransferListColumn(index.column());
331  switch (column)
332  {
333  case Column::progress:
334  {
335  int progress = index.data().toInt();
336 
337  QStyleOptionProgressBar progressBarOption;
338  progressBarOption.rect = option.rect;
339  progressBarOption.minimum = 0;
340  progressBarOption.maximum = 100;
341  progressBarOption.progress = progress;
342  progressBarOption.text = QString::number(progress) + "%";
343  progressBarOption.textVisible = true;
344 
345  QApplication::style()->drawControl(QStyle::CE_ProgressBar,
346  &progressBarOption, painter);
347  return;
348  }
349  case Column::control:
350  {
351  const auto data = index.data();
352  if (!data.canConvert<bool>()) {
353  qWarning("Unexpected control type, not rendering controls");
354  return;
355  }
356  const auto localPaused = data.toBool();
357  QPixmap pausePixmap = localPaused
358  ? QPixmap(Style::getImagePath("fileTransferInstance/arrow_black.svg"))
359  : QPixmap(Style::getImagePath("fileTransferInstance/pause_dark.svg"));
360  QApplication::style()->drawItemPixmap(painter, pauseRect(option), Qt::AlignCenter, pausePixmap);
361 
362  QPixmap stopPixmap(Style::getImagePath("fileTransferInstance/no_dark.svg"));
363  QApplication::style()->drawItemPixmap(painter, stopRect(option), Qt::AlignCenter, stopPixmap);
364  return;
365  }
366  case Column::fileName:
367  case Column::contact:
368  case Column::size:
369  case Column::speed:
370  case Column::status:
371  case Column::invalid:
372  break;
373  }
374 
375  QStyledItemDelegate::paint(painter, option, index);
376  }
377 
378  bool Delegate::editorEvent(QEvent* event, QAbstractItemModel* model,
379  const QStyleOptionViewItem& option, const QModelIndex& index)
380  {
381  if (toFileTransferListColumn(index.column()) == Column::control)
382  {
383  if (event->type() == QEvent::MouseButtonPress) {
384  auto mouseEvent = reinterpret_cast<QMouseEvent*>(event);
385  const auto pos = mouseEvent->pos();
386  const auto posRect = pauseRect(option);
387  const auto stRect = stopRect(option);
388 
389  if (posRect.contains(pos)) {
390  model->setData(index, static_cast<int>(EditorAction::pause));
391  } else if (stRect.contains(pos)) {
392  model->setData(index, static_cast<int>(EditorAction::cancel));
393  }
394  }
395  return true;
396  }
397  return false;
398  }
399 
400 
401  View::View(QAbstractItemModel* model, QWidget* parent)
402  : QTableView(parent)
403  {
404  setModel(model);
405 
406  // Resize to contents but stretch the file name to fill the full view
407  horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents);
408  horizontalHeader()->setSectionResizeMode(0, QHeaderView::Stretch);
409  // Visually tuned until it looked ok
410  horizontalHeader()->setMinimumSectionSize(75);
411  horizontalHeader()->setStretchLastSection(false);
412  verticalHeader()->hide();
413  setShowGrid(false);
414  setSelectionBehavior(QAbstractItemView::SelectRows);
415  setSelectionMode(QAbstractItemView::SingleSelection);
416  setItemDelegate(new Delegate(this));
417  }
418 
419  View::~View() = default;
420 
421 } // namespace FileTransferList
422 
424  : QObject()
425 {
426  head = new QWidget();
427  QFont bold;
428  bold.setBold(true);
429  headLabel.setFont(bold);
430  head->setLayout(&headLayout);
431  headLayout.addWidget(&headLabel);
432 
435 
436  auto pauseFile = [&coreFile] (ToxFile file) {
437  coreFile.pauseResumeFile(file.friendId, file.fileNum);
438  };
439 
440  auto cancelFileRecv = [&coreFile] (ToxFile file) {
441  coreFile.cancelFileRecv(file.friendId, file.fileNum);
442  };
443 
444  auto cancelFileSend = [&coreFile] (ToxFile file) {
445  coreFile.cancelFileSend(file.friendId, file.fileNum);
446  };
447 
448  connect(recvdModel, &FileTransferList::Model::togglePause, pauseFile);
449  connect(recvdModel, &FileTransferList::Model::cancel, cancelFileRecv);
450  connect(sentModel, &FileTransferList::Model::togglePause, pauseFile);
451  connect(sentModel, &FileTransferList::Model::cancel, cancelFileSend);
452 
455 
456  main.addTab(recvd, QString());
457  main.addTab(sent, QString());
458 
459  connect(sent, &QTableView::activated, this, &FilesForm::onSentFileActivated);
460  connect(recvd, &QTableView::activated, this, &FilesForm::onReceivedFileActivated);
461 
462  retranslateUi();
463  Translator::registerHandler(std::bind(&FilesForm::retranslateUi, this), this);
464 }
465 
467 {
469  delete recvd;
470  delete sent;
471  head->deleteLater();
472 }
473 
474 bool FilesForm::isShown() const
475 {
476  if (main.isVisible()) {
477  head->window()->windowHandle()->alert(0);
478  return true;
479  }
480 
481  return false;
482 }
483 
484 void FilesForm::show(ContentLayout* contentLayout)
485 {
486  contentLayout->mainContent->layout()->addWidget(&main);
487  contentLayout->mainHead->layout()->addWidget(head);
488  main.show();
489  head->show();
490 }
491 
493 {
494  if (!shouldProcessFileKind(inFile.fileKind)) {
495  return;
496  }
497 
498  if (inFile.direction == ToxFile::SENDING) {
499  sentModel->onFileUpdated(inFile);
500  }
501  else if (inFile.direction == ToxFile::RECEIVING) {
502  recvdModel->onFileUpdated(inFile);
503  }
504  else {
505  qWarning("Unexpected file direction");
506  }
507 }
508 
509 void FilesForm::onSentFileActivated(const QModelIndex& index)
510 {
511  const auto& filePath = sentModel->data(index, Qt::UserRole).toString();
513 }
514 
515 void FilesForm::onReceivedFileActivated(const QModelIndex& index)
516 {
517  const auto& filePath = recvdModel->data(index, Qt::UserRole).toString();
519 }
520 
522 {
523  headLabel.setText(tr("Transferred files", "\"Headline\" of the window"));
524  main.setTabText(0, tr("Downloads"));
525  main.setTabText(1, tr("Uploads"));
526 }
FileTransferList::toFileTransferListColumn
Column toFileTransferListColumn(int in)
Definition: filesform.cpp:127
FileTransferList::Delegate::paint
void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override
Definition: filesform.cpp:328
FileTransferList::View
Definition: filesform.h:97
FileTransferList::EditorAction::pause
@ pause
FileTransferList::Column::speed
@ speed
style.h
FileTransferList::Model::rowCount
int rowCount(const QModelIndex &parent=QModelIndex()) const override
Definition: filesform.cpp:235
ToxFile::BROKEN
@ BROKEN
Definition: toxfile.h:41
FileTransferList::Model::togglePause
void togglePause(ToxFile file)
toxfile.h
FilesForm::retranslateUi
void retranslateUi()
Definition: filesform.cpp:521
friendlist.h
FileTransferList::Model::columnCount
int columnCount(const QModelIndex &parent=QModelIndex()) const override
Definition: filesform.cpp:240
FileTransferList::Model::data
QVariant data(const QModelIndex &index, int role=Qt::DisplayRole) const override
Definition: filesform.cpp:245
FilesForm::main
QTabWidget main
Definition: filesform.h:136
FileTransferList::Delegate::Delegate
Delegate(QWidget *parent=nullptr)
Definition: filesform.cpp:324
FilesForm::~FilesForm
~FilesForm()
Definition: filesform.cpp:466
FileTransferList::toQString
QString toQString(Column column)
Definition: filesform.cpp:136
ToxFile::TRANSMITTING
@ TRANSMITTING
Definition: toxfile.h:40
FileTransferList
Definition: filesform.cpp:125
HistMessageContentType::file
@ file
ToxFile::FileStatus
FileStatus
Definition: toxfile.h:36
Widget::confirmExecutableOpen
static void confirmExecutableOpen(const QFileInfo &file)
Definition: widget.cpp:911
FilesForm::FilesForm
FilesForm(CoreFile &coreFile)
Definition: filesform.cpp:423
Translator::unregister
static void unregister(void *owner)
Unregisters all handlers of an owner.
Definition: translator.cpp:103
FilesForm::sentModel
FileTransferList::Model * sentModel
Definition: filesform.h:138
CoreFile::cancelFileSend
void cancelFileSend(uint32_t friendId, uint32_t fileId)
Definition: corefile.cpp:208
FileTransferList::Model::headerData
QVariant headerData(int section, Qt::Orientation orientation, int role=Qt::DisplayRole) const override
Definition: filesform.cpp:173
Style::getImagePath
static const QString getImagePath(const QString &filename)
Definition: style.cpp:182
FileTransferList::Delegate::editorEvent
bool editorEvent(QEvent *event, QAbstractItemModel *model, const QStyleOptionViewItem &option, const QModelIndex &index) override
Definition: filesform.cpp:378
FileTransferList::Column::fileName
@ fileName
FriendList::findFriend
static Friend * findFriend(const ToxPk &friendPk)
Definition: friendlist.cpp:47
ToxFile::direction
FileDirection direction
Definition: toxfile.h:71
FileTransferList::Model
Definition: filesform.h:64
FilesForm::sent
QTableView * sent
Definition: filesform.h:137
FileTransferList::Column::size
@ size
ToxFile::RECEIVING
@ RECEIVING
Definition: toxfile.h:51
FileTransferList::Model::cancel
void cancel(ToxFile file)
FileTransferList::View::View
View(QAbstractItemModel *model, QWidget *parent=nullptr)
Definition: filesform.cpp:401
FilesForm::onReceivedFileActivated
void onReceivedFileActivated(const QModelIndex &item)
Definition: filesform.cpp:515
ContentLayout::mainHead
QWidget * mainHead
Definition: contentlayout.h:37
FilesForm::onSentFileActivated
void onSentFileActivated(const QModelIndex &item)
Definition: filesform.cpp:509
widget.h
ToxFile::INITIALIZING
@ INITIALIZING
Definition: toxfile.h:38
FilesForm::onFileUpdated
void onFileUpdated(const ToxFile &file)
Definition: filesform.cpp:492
ToxFile::PAUSED
@ PAUSED
Definition: toxfile.h:39
FileTransferList::Delegate
Definition: filesform.h:88
FileTransferList::EditorAction
EditorAction
Definition: filesform.h:56
ToxFile::CANCELED
@ CANCELED
Definition: toxfile.h:42
CoreFile::pauseResumeFile
void pauseResumeFile(uint32_t friendId, uint32_t fileId)
Definition: corefile.cpp:173
FileTransferList::View::~View
~View()
FileTransferList::EditorAction::invalid
@ invalid
FileTransferList::Column
Column
Definition: filesform.h:41
FilesForm::show
void show(ContentLayout *contentLayout)
Definition: filesform.cpp:484
ToxFile::FINISHED
@ FINISHED
Definition: toxfile.h:43
FileTransferList::Model::Model
Model(QObject *parent=nullptr)
Definition: filesform.cpp:169
ToxFile::SENDING
@ SENDING
Definition: toxfile.h:50
FilesForm::head
QWidget * head
Definition: filesform.h:133
filesform.h
CoreFile::cancelFileRecv
void cancelFileRecv(uint32_t friendId, uint32_t fileId)
Definition: corefile.cpp:224
FileTransferList::Column::invalid
@ invalid
Translator::registerHandler
static void registerHandler(const std::function< void()> &, void *owner)
Register a function to be called when the UI needs to be retranslated.
Definition: translator.cpp:93
FileTransferList::Column::control
@ control
FilesForm::headLabel
QLabel headLabel
Definition: filesform.h:134
ExifTransform::Orientation
Orientation
Definition: exiftransform.h:26
FileTransferList::Model::setData
bool setData(const QModelIndex &index, const QVariant &value, int role=Qt::EditRole) override
Definition: filesform.cpp:294
FilesForm::headLayout
QVBoxLayout headLayout
Definition: filesform.h:135
FileTransferList::Model::files
std::vector< ToxFile > files
Definition: filesform.h:85
FileTransferList::Column::contact
@ contact
FileTransferList::Column::progress
@ progress
contentlayout.h
ToxFile::fileKind
uint8_t fileKind
Data file (default) or avatar.
Definition: toxfile.h:64
FilesForm::recvd
QTableView * recvd
Definition: filesform.h:137
FilesForm::recvdModel
FileTransferList::Model * recvdModel
Definition: filesform.h:138
ContentLayout::mainContent
QWidget * mainContent
Definition: contentlayout.h:36
ContentLayout
Definition: contentlayout.h:25
FileTransferList::toEditorAction
EditorAction toEditorAction(int in)
Definition: filesform.cpp:160
FileTransferList::EditorAction::cancel
@ cancel
FilesForm::isShown
bool isShown() const
Definition: filesform.cpp:474
translator.h
FriendList::id2Key
static const ToxPk & id2Key(uint32_t friendId)
Definition: friendlist.cpp:57
FileTransferList::Column::status
@ status
ToxFile
Definition: toxfile.h:32
FileTransferList::Model::idToRow
QHash< QByteArray, int > idToRow
Definition: filesform.h:84
CoreFile
Manages the file transfer service of toxcore.
Definition: corefile.h:46
FileTransferList::Model::onFileUpdated
void onFileUpdated(const ToxFile &file)
Definition: filesform.cpp:187