Correctly handle "torrent finished" events
[qBittorrent.git] / src / gui / torrentcontentwidget.cpp
blob472537bf2ca5cfa68cfc249a548e58098716a96e
1 /*
2 * Bittorrent Client using Qt and libtorrent.
3 * Copyright (C) 2022-2024 Vladimir Golovnev <glassez@yandex.ru>
4 * Copyright (C) 2014 Ivan Sorokin <vanyacpp@gmail.com>
6 * This program is free software; you can redistribute it and/or
7 * modify it under the terms of the GNU General Public License
8 * as published by the Free Software Foundation; either version 2
9 * of the License, or (at your option) any later version.
11 * This program 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.
16 * You should have received a copy of the GNU General Public License
17 * along with this program; if not, write to the Free Software
18 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
20 * In addition, as a special exception, the copyright holders give permission to
21 * link this program with the OpenSSL project's "OpenSSL" library (or with
22 * modified versions of it that use the same license as the "OpenSSL" library),
23 * and distribute the linked executables. You must obey the GNU General Public
24 * License in all respects for all of the code used other than "OpenSSL". If you
25 * modify file(s), you may extend this exception to your version of the file(s),
26 * but you are not obligated to do so. If you do not wish to do so, delete this
27 * exception statement from your version.
30 #include "torrentcontentwidget.h"
32 #include <QDir>
33 #include <QHeaderView>
34 #include <QKeyEvent>
35 #include <QLineEdit>
36 #include <QMenu>
37 #include <QMessageBox>
38 #include <QModelIndexList>
39 #include <QShortcut>
40 #include <QWheelEvent>
42 #include "base/bittorrent/torrentcontenthandler.h"
43 #include "base/path.h"
44 #include "base/utils/string.h"
45 #include "autoexpandabledialog.h"
46 #include "raisedmessagebox.h"
47 #include "torrentcontentfiltermodel.h"
48 #include "torrentcontentitemdelegate.h"
49 #include "torrentcontentmodel.h"
50 #include "torrentcontentmodelitem.h"
51 #include "uithememanager.h"
52 #include "utils.h"
54 #ifdef Q_OS_MACOS
55 #include "gui/macutilities.h"
56 #endif
58 namespace
60 QList<QPersistentModelIndex> toPersistentIndexes(const QModelIndexList &indexes)
62 QList<QPersistentModelIndex> persistentIndexes;
63 persistentIndexes.reserve(indexes.size());
64 for (const QModelIndex &index : indexes)
65 persistentIndexes.emplaceBack(index);
67 return persistentIndexes;
71 TorrentContentWidget::TorrentContentWidget(QWidget *parent)
72 : QTreeView(parent)
74 setDragEnabled(true);
75 setDragDropMode(QAbstractItemView::DragOnly);
76 setExpandsOnDoubleClick(false);
77 setSortingEnabled(true);
78 setUniformRowHeights(true);
79 header()->setSortIndicator(0, Qt::AscendingOrder);
80 header()->setFirstSectionMovable(true);
81 header()->setContextMenuPolicy(Qt::CustomContextMenu);
83 m_model = new TorrentContentModel(this);
84 connect(m_model, &TorrentContentModel::renameFailed, this, [this](const QString &errorMessage)
86 RaisedMessageBox::warning(this, tr("Rename error"), errorMessage, QMessageBox::Ok);
87 });
89 m_filterModel = new TorrentContentFilterModel(this);
90 m_filterModel->setSourceModel(m_model);
91 QTreeView::setModel(m_filterModel);
93 auto *itemDelegate = new TorrentContentItemDelegate(this);
94 setItemDelegate(itemDelegate);
96 connect(this, &QAbstractItemView::clicked, this, qOverload<const QModelIndex &>(&QAbstractItemView::edit));
97 connect(this, &QAbstractItemView::doubleClicked, this, &TorrentContentWidget::onItemDoubleClicked);
98 connect(this, &QWidget::customContextMenuRequested, this, &TorrentContentWidget::displayContextMenu);
99 connect(header(), &QWidget::customContextMenuRequested, this, &TorrentContentWidget::displayColumnHeaderMenu);
100 connect(header(), &QHeaderView::sectionMoved, this, &TorrentContentWidget::stateChanged);
101 connect(header(), &QHeaderView::sectionResized, this, &TorrentContentWidget::stateChanged);
102 connect(header(), &QHeaderView::sortIndicatorChanged, this, &TorrentContentWidget::stateChanged);
104 const auto *renameFileHotkey = new QShortcut(Qt::Key_F2, this, nullptr, nullptr, Qt::WidgetShortcut);
105 connect(renameFileHotkey, &QShortcut::activated, this, &TorrentContentWidget::renameSelectedFile);
107 connect(model(), &QAbstractItemModel::modelReset, this, &TorrentContentWidget::expandRecursively);
110 void TorrentContentWidget::setContentHandler(BitTorrent::TorrentContentHandler *contentHandler)
112 m_model->setContentHandler(contentHandler);
113 if (!contentHandler)
114 return;
116 expandRecursively();
119 BitTorrent::TorrentContentHandler *TorrentContentWidget::contentHandler() const
121 return m_model->contentHandler();
124 void TorrentContentWidget::refresh()
126 setUpdatesEnabled(false);
127 m_model->refresh();
128 setUpdatesEnabled(true);
131 bool TorrentContentWidget::openByEnterKey() const
133 return m_openFileHotkeyEnter;
136 void TorrentContentWidget::setOpenByEnterKey(const bool value)
138 if (value == openByEnterKey())
139 return;
141 if (value)
143 m_openFileHotkeyReturn = new QShortcut(Qt::Key_Return, this, nullptr, nullptr, Qt::WidgetShortcut);
144 connect(m_openFileHotkeyReturn, &QShortcut::activated, this, &TorrentContentWidget::openSelectedFile);
145 m_openFileHotkeyEnter = new QShortcut(Qt::Key_Enter, this, nullptr, nullptr, Qt::WidgetShortcut);
146 connect(m_openFileHotkeyEnter, &QShortcut::activated, this, &TorrentContentWidget::openSelectedFile);
148 else
150 delete m_openFileHotkeyEnter;
151 m_openFileHotkeyEnter = nullptr;
152 delete m_openFileHotkeyReturn;
153 m_openFileHotkeyReturn = nullptr;
157 TorrentContentWidget::DoubleClickAction TorrentContentWidget::doubleClickAction() const
159 return m_doubleClickAction;
162 void TorrentContentWidget::setDoubleClickAction(DoubleClickAction action)
164 m_doubleClickAction = action;
167 TorrentContentWidget::ColumnsVisibilityMode TorrentContentWidget::columnsVisibilityMode() const
169 return m_columnsVisibilityMode;
172 void TorrentContentWidget::setColumnsVisibilityMode(ColumnsVisibilityMode mode)
174 m_columnsVisibilityMode = mode;
177 int TorrentContentWidget::getFileIndex(const QModelIndex &index) const
179 return m_filterModel->getFileIndex(index);
182 Path TorrentContentWidget::getItemPath(const QModelIndex &index) const
184 Path path;
185 for (QModelIndex i = index; i.isValid(); i = i.parent())
186 path = Path(i.data().toString()) / path;
187 return path;
190 void TorrentContentWidget::setFilterPattern(const QString &patternText, const FilterPatternFormat format)
192 if (format == FilterPatternFormat::PlainText)
194 m_filterModel->setFilterFixedString(patternText);
195 m_filterModel->setFilterCaseSensitivity(Qt::CaseInsensitive);
197 else
199 const QString pattern = ((format == FilterPatternFormat::Regex)
200 ? patternText : Utils::String::wildcardToRegexPattern(patternText));
201 m_filterModel->setFilterRegularExpression(QRegularExpression(pattern, QRegularExpression::CaseInsensitiveOption));
204 if (patternText.isEmpty())
206 collapseAll();
207 expand(m_filterModel->index(0, 0));
209 else
211 expandAll();
215 void TorrentContentWidget::checkAll()
217 for (int i = 0; i < model()->rowCount(); ++i)
218 model()->setData(model()->index(i, TorrentContentModelItem::COL_NAME), Qt::Checked, Qt::CheckStateRole);
221 void TorrentContentWidget::checkNone()
223 for (int i = 0; i < model()->rowCount(); ++i)
224 model()->setData(model()->index(i, TorrentContentModelItem::COL_NAME), Qt::Unchecked, Qt::CheckStateRole);
227 void TorrentContentWidget::keyPressEvent(QKeyEvent *event)
229 if ((event->key() != Qt::Key_Space) && (event->key() != Qt::Key_Select))
231 QTreeView::keyPressEvent(event);
232 return;
235 event->accept();
237 const QVariant value = currentNameCell().data(Qt::CheckStateRole);
238 if (!value.isValid())
240 Q_ASSERT(false);
241 return;
244 const Qt::CheckState state = (static_cast<Qt::CheckState>(value.toInt()) == Qt::Checked)
245 ? Qt::Unchecked : Qt::Checked;
246 const QList<QPersistentModelIndex> selection = toPersistentIndexes(selectionModel()->selectedRows(TorrentContentModelItem::COL_NAME));
248 for (const QPersistentModelIndex &index : selection)
249 model()->setData(index, state, Qt::CheckStateRole);
252 void TorrentContentWidget::renameSelectedFile()
254 const QModelIndexList selectedIndexes = selectionModel()->selectedRows(0);
255 if (selectedIndexes.size() != 1)
256 return;
258 const QPersistentModelIndex modelIndex = selectedIndexes.first();
259 if (!modelIndex.isValid())
260 return;
262 // Ask for new name
263 const bool isFile = (m_filterModel->itemType(modelIndex) == TorrentContentModelItem::FileType);
264 bool ok = false;
265 QString newName = AutoExpandableDialog::getText(this, tr("Renaming"), tr("New name:"), QLineEdit::Normal
266 , modelIndex.data().toString(), &ok, isFile).trimmed();
267 if (!ok || !modelIndex.isValid())
268 return;
270 model()->setData(modelIndex, newName);
273 void TorrentContentWidget::applyPriorities(const BitTorrent::DownloadPriority priority)
275 const QList<QPersistentModelIndex> selectedRows = toPersistentIndexes(selectionModel()->selectedRows(Priority));
276 for (const QPersistentModelIndex &index : selectedRows)
278 model()->setData(index, static_cast<int>(priority));
282 void TorrentContentWidget::applyPrioritiesByOrder()
284 // Equally distribute the selected items into groups and for each group assign
285 // a download priority that will apply to each item. The number of groups depends on how
286 // many "download priority" are available to be assigned
288 const QList<QPersistentModelIndex> selectedRows = toPersistentIndexes(selectionModel()->selectedRows(Priority));
290 const qsizetype priorityGroups = 3;
291 const auto priorityGroupSize = std::max<qsizetype>((selectedRows.length() / priorityGroups), 1);
293 for (qsizetype i = 0; i < selectedRows.length(); ++i)
295 auto priority = BitTorrent::DownloadPriority::Ignored;
296 switch (i / priorityGroupSize)
298 case 0:
299 priority = BitTorrent::DownloadPriority::Maximum;
300 break;
301 case 1:
302 priority = BitTorrent::DownloadPriority::High;
303 break;
304 default:
305 case 2:
306 priority = BitTorrent::DownloadPriority::Normal;
307 break;
310 const QPersistentModelIndex &index = selectedRows[i];
311 model()->setData(index, static_cast<int>(priority));
315 void TorrentContentWidget::openSelectedFile()
317 const QModelIndexList selectedIndexes = selectionModel()->selectedRows(0);
318 if (selectedIndexes.size() != 1)
319 return;
320 openItem(selectedIndexes.first());
323 void TorrentContentWidget::setModel([[maybe_unused]] QAbstractItemModel *model)
325 Q_ASSERT_X(false, Q_FUNC_INFO, "Changing the model of TorrentContentWidget is not allowed.");
328 QModelIndex TorrentContentWidget::currentNameCell() const
330 const QModelIndex current = currentIndex();
331 if (!current.isValid())
333 Q_ASSERT(false);
334 return {};
337 return current.siblingAtColumn(TorrentContentModelItem::COL_NAME);
340 void TorrentContentWidget::displayColumnHeaderMenu()
342 QMenu *menu = new QMenu(this);
343 menu->setAttribute(Qt::WA_DeleteOnClose);
344 menu->setToolTipsVisible(true);
346 if (m_columnsVisibilityMode == ColumnsVisibilityMode::Editable)
348 menu->setTitle(tr("Column visibility"));
349 for (int i = 0; i < TorrentContentModelItem::NB_COL; ++i)
351 const auto columnName = model()->headerData(i, Qt::Horizontal, Qt::DisplayRole).toString();
352 QAction *action = menu->addAction(columnName, this, [this, i](bool checked)
354 setColumnHidden(i, !checked);
356 if (checked && (columnWidth(i) <= 5))
357 resizeColumnToContents(i);
359 emit stateChanged();
361 action->setCheckable(true);
362 action->setChecked(!isColumnHidden(i));
364 if (i == TorrentContentModelItem::COL_NAME)
365 action->setEnabled(false);
368 menu->addSeparator();
371 QAction *resizeAction = menu->addAction(tr("Resize columns"), this, [this]()
373 for (int i = 0, count = header()->count(); i < count; ++i)
375 if (!isColumnHidden(i))
376 resizeColumnToContents(i);
379 emit stateChanged();
381 resizeAction->setToolTip(tr("Resize all non-hidden columns to the size of their contents"));
383 menu->popup(QCursor::pos());
386 void TorrentContentWidget::displayContextMenu()
388 const QModelIndexList selectedRows = selectionModel()->selectedRows(0);
389 if (selectedRows.empty())
390 return;
392 QMenu *menu = new QMenu(this);
393 menu->setAttribute(Qt::WA_DeleteOnClose);
395 if (selectedRows.size() == 1)
397 const QModelIndex index = selectedRows[0];
399 if (!contentHandler()->actualStorageLocation().isEmpty())
401 menu->addAction(UIThemeManager::instance()->getIcon(u"folder-documents"_s), tr("Open")
402 , this, [this, index]() { openItem(index); });
403 menu->addAction(UIThemeManager::instance()->getIcon(u"directory"_s), tr("Open containing folder")
404 , this, [this, index]() { openParentFolder(index); });
406 menu->addAction(UIThemeManager::instance()->getIcon(u"edit-rename"_s), tr("Rename...")
407 , this, &TorrentContentWidget::renameSelectedFile);
408 menu->addSeparator();
410 QMenu *subMenu = menu->addMenu(tr("Priority"));
412 subMenu->addAction(tr("Do not download"), this, [this]
414 applyPriorities(BitTorrent::DownloadPriority::Ignored);
416 subMenu->addAction(tr("Normal"), this, [this]
418 applyPriorities(BitTorrent::DownloadPriority::Normal);
420 subMenu->addAction(tr("High"), this, [this]
422 applyPriorities(BitTorrent::DownloadPriority::High);
424 subMenu->addAction(tr("Maximum"), this, [this]
426 applyPriorities(BitTorrent::DownloadPriority::Maximum);
428 subMenu->addSeparator();
429 subMenu->addAction(tr("By shown file order"), this, &TorrentContentWidget::applyPrioritiesByOrder);
431 else
433 menu->addAction(tr("Do not download"), this, [this]
435 applyPriorities(BitTorrent::DownloadPriority::Ignored);
437 menu->addAction(tr("Normal priority"), this, [this]
439 applyPriorities(BitTorrent::DownloadPriority::Normal);
441 menu->addAction(tr("High priority"), this, [this]
443 applyPriorities(BitTorrent::DownloadPriority::High);
445 menu->addAction(tr("Maximum priority"), this, [this]
447 applyPriorities(BitTorrent::DownloadPriority::Maximum);
449 menu->addSeparator();
450 menu->addAction(tr("Priority by shown file order"), this, &TorrentContentWidget::applyPrioritiesByOrder);
453 // The selected torrent might have disappeared during exec()
454 // so we just close menu when an appropriate model is reset
455 connect(model(), &QAbstractItemModel::modelAboutToBeReset, menu, [menu]()
457 menu->setActiveAction(nullptr);
458 menu->close();
461 menu->popup(QCursor::pos());
464 void TorrentContentWidget::openItem(const QModelIndex &index) const
466 if (!index.isValid())
467 return;
469 m_model->contentHandler()->flushCache(); // Flush data
470 Utils::Gui::openPath(getFullPath(index));
473 void TorrentContentWidget::openParentFolder(const QModelIndex &index) const
475 const Path path = getFullPath(index);
476 m_model->contentHandler()->flushCache(); // Flush data
477 #ifdef Q_OS_MACOS
478 MacUtils::openFiles({path});
479 #else
480 Utils::Gui::openFolderSelect(path);
481 #endif
484 Path TorrentContentWidget::getFullPath(const QModelIndex &index) const
486 const auto *contentHandler = m_model->contentHandler();
487 if (const int fileIdx = getFileIndex(index); fileIdx >= 0)
489 const Path fullPath = contentHandler->actualStorageLocation() / contentHandler->actualFilePath(fileIdx);
490 return fullPath;
493 // folder type
494 const Path fullPath = contentHandler->actualStorageLocation() / getItemPath(index);
495 return fullPath;
498 void TorrentContentWidget::onItemDoubleClicked(const QModelIndex &index)
500 const auto *contentHandler = m_model->contentHandler();
501 Q_ASSERT(contentHandler && contentHandler->hasMetadata());
503 if (!contentHandler || !contentHandler->hasMetadata()) [[unlikely]]
504 return;
506 if (m_doubleClickAction == DoubleClickAction::Rename)
507 renameSelectedFile();
508 else
509 openItem(index);
512 void TorrentContentWidget::expandRecursively()
514 QModelIndex currentIndex;
515 while (model()->rowCount(currentIndex) == 1)
517 currentIndex = model()->index(0, 0, currentIndex);
518 setExpanded(currentIndex, true);
522 void TorrentContentWidget::wheelEvent(QWheelEvent *event)
524 if (event->modifiers() & Qt::ShiftModifier)
526 // Shift + scroll = horizontal scroll
527 event->accept();
528 QWheelEvent scrollHEvent {event->position(), event->globalPosition()
529 , event->pixelDelta(), event->angleDelta().transposed(), event->buttons()
530 , event->modifiers(), event->phase(), event->inverted(), event->source()};
531 QTreeView::wheelEvent(&scrollHEvent);
532 return;
535 QTreeView::wheelEvent(event); // event delegated to base class