/* * Bittorrent Client using Qt and libtorrent. * Copyright (C) 2022 Vladimir Golovnev * Copyright (C) 2014 Ivan Sorokin * * This program is free 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 2 * of the License, or (at your option) any later version. * * This program 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 this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * * In addition, as a special exception, the copyright holders give permission to * link this program with the OpenSSL project's "OpenSSL" library (or with * modified versions of it that use the same license as the "OpenSSL" library), * and distribute the linked executables. You must obey the GNU General Public * License in all respects for all of the code used other than "OpenSSL". If you * modify file(s), you may extend this exception to your version of the file(s), * but you are not obligated to do so. If you do not wish to do so, delete this * exception statement from your version. */ #include "torrentcontentwidget.h" #include #include #include #include #include #include #include #include #include #include #include "base/bittorrent/torrentcontenthandler.h" #include "base/path.h" #include "base/utils/string.h" #include "autoexpandabledialog.h" #include "raisedmessagebox.h" #include "torrentcontentfiltermodel.h" #include "torrentcontentitemdelegate.h" #include "torrentcontentmodel.h" #include "torrentcontentmodelitem.h" #include "uithememanager.h" #include "utils.h" #ifdef Q_OS_MACOS #include "gui/macutilities.h" #endif TorrentContentWidget::TorrentContentWidget(QWidget *parent) : QTreeView(parent) { setExpandsOnDoubleClick(false); setSortingEnabled(true); header()->setSortIndicator(0, Qt::AscendingOrder); header()->setFirstSectionMovable(true); header()->setContextMenuPolicy(Qt::CustomContextMenu); m_model = new TorrentContentModel(this); connect(m_model, &TorrentContentModel::renameFailed, this, [this](const QString &errorMessage) { RaisedMessageBox::warning(this, tr("Rename error"), errorMessage, QMessageBox::Ok); }); m_filterModel = new TorrentContentFilterModel(this); m_filterModel->setSourceModel(m_model); QTreeView::setModel(m_filterModel); auto itemDelegate = new TorrentContentItemDelegate(this); setItemDelegate(itemDelegate); connect(this, &QAbstractItemView::clicked, this, qOverload(&QAbstractItemView::edit)); connect(this, &QAbstractItemView::doubleClicked, this, &TorrentContentWidget::onItemDoubleClicked); connect(this, &QWidget::customContextMenuRequested, this, &TorrentContentWidget::displayContextMenu); connect(header(), &QWidget::customContextMenuRequested, this, &TorrentContentWidget::displayColumnHeaderMenu); connect(header(), &QHeaderView::sectionMoved, this, &TorrentContentWidget::stateChanged); connect(header(), &QHeaderView::sectionResized, this, &TorrentContentWidget::stateChanged); connect(header(), &QHeaderView::sortIndicatorChanged, this, &TorrentContentWidget::stateChanged); const auto *renameFileHotkey = new QShortcut(Qt::Key_F2, this, nullptr, nullptr, Qt::WidgetShortcut); connect(renameFileHotkey, &QShortcut::activated, this, &TorrentContentWidget::renameSelectedFile); const auto *openFileHotkeyReturn = new QShortcut(Qt::Key_Return, this, nullptr, nullptr, Qt::WidgetShortcut); connect(openFileHotkeyReturn, &QShortcut::activated, this, &TorrentContentWidget::openSelectedFile); const auto *openFileHotkeyEnter = new QShortcut(Qt::Key_Enter, this, nullptr, nullptr, Qt::WidgetShortcut); connect(openFileHotkeyEnter, &QShortcut::activated, this, &TorrentContentWidget::openSelectedFile); connect(model(), &QAbstractItemModel::modelReset, this, &TorrentContentWidget::expandRecursively); } void TorrentContentWidget::setContentHandler(BitTorrent::TorrentContentHandler *contentHandler) { m_model->setContentHandler(contentHandler); if (!contentHandler) return; expandRecursively(); } BitTorrent::TorrentContentHandler *TorrentContentWidget::contentHandler() const { return m_model->contentHandler(); } void TorrentContentWidget::refresh() { setUpdatesEnabled(false); m_model->refresh(); setUpdatesEnabled(true); } TorrentContentWidget::DoubleClickAction TorrentContentWidget::doubleClickAction() const { return m_doubleClickAction; } void TorrentContentWidget::setDoubleClickAction(DoubleClickAction action) { m_doubleClickAction = action; } TorrentContentWidget::ColumnsVisibilityMode TorrentContentWidget::columnsVisibilityMode() const { return m_columnsVisibilityMode; } void TorrentContentWidget::setColumnsVisibilityMode(ColumnsVisibilityMode mode) { m_columnsVisibilityMode = mode; } int TorrentContentWidget::getFileIndex(const QModelIndex &index) const { return m_filterModel->getFileIndex(index); } Path TorrentContentWidget::getItemPath(const QModelIndex &index) const { Path path; for (QModelIndex i = index; i.isValid(); i = i.parent()) path = Path(i.data().toString()) / path; return path; } void TorrentContentWidget::setFilterPattern(const QString &patternText) { const QString pattern = Utils::String::wildcardToRegexPattern(patternText); m_filterModel->setFilterRegularExpression(QRegularExpression(pattern, QRegularExpression::CaseInsensitiveOption)); if (patternText.isEmpty()) { collapseAll(); expand(m_filterModel->index(0, 0)); } else { expandAll(); } } void TorrentContentWidget::checkAll() { for (int i = 0; i < model()->rowCount(); ++i) model()->setData(model()->index(i, TorrentContentModelItem::COL_NAME), Qt::Checked, Qt::CheckStateRole); } void TorrentContentWidget::checkNone() { for (int i = 0; i < model()->rowCount(); ++i) model()->setData(model()->index(i, TorrentContentModelItem::COL_NAME), Qt::Unchecked, Qt::CheckStateRole); } void TorrentContentWidget::keyPressEvent(QKeyEvent *event) { if ((event->key() != Qt::Key_Space) && (event->key() != Qt::Key_Select)) { QTreeView::keyPressEvent(event); return; } event->accept(); const QVariant value = currentNameCell().data(Qt::CheckStateRole); if (!value.isValid()) { Q_ASSERT(false); return; } const Qt::CheckState state = (static_cast(value.toInt()) == Qt::Checked) ? Qt::Unchecked : Qt::Checked; const QModelIndexList selection = selectionModel()->selectedRows(TorrentContentModelItem::COL_NAME); for (const QModelIndex &index : selection) model()->setData(index, state, Qt::CheckStateRole); } void TorrentContentWidget::renameSelectedFile() { const QModelIndexList selectedIndexes = selectionModel()->selectedRows(0); if (selectedIndexes.size() != 1) return; const QPersistentModelIndex modelIndex = selectedIndexes.first(); if (!modelIndex.isValid()) return; // Ask for new name const bool isFile = (m_filterModel->itemType(modelIndex) == TorrentContentModelItem::FileType); bool ok = false; QString newName = AutoExpandableDialog::getText(this, tr("Renaming"), tr("New name:"), QLineEdit::Normal , modelIndex.data().toString(), &ok, isFile).trimmed(); if (!ok || !modelIndex.isValid()) return; model()->setData(modelIndex, newName); } void TorrentContentWidget::applyPriorities(const BitTorrent::DownloadPriority priority) { const QModelIndexList selectedRows = selectionModel()->selectedRows(0); for (const QModelIndex &index : selectedRows) { model()->setData(index.sibling(index.row(), Priority), static_cast(priority)); } } void TorrentContentWidget::applyPrioritiesByOrder() { // Equally distribute the selected items into groups and for each group assign // a download priority that will apply to each item. The number of groups depends on how // many "download priority" are available to be assigned const QModelIndexList selectedRows = selectionModel()->selectedRows(0); const qsizetype priorityGroups = 3; const auto priorityGroupSize = std::max((selectedRows.length() / priorityGroups), 1); for (qsizetype i = 0; i < selectedRows.length(); ++i) { auto priority = BitTorrent::DownloadPriority::Ignored; switch (i / priorityGroupSize) { case 0: priority = BitTorrent::DownloadPriority::Maximum; break; case 1: priority = BitTorrent::DownloadPriority::High; break; default: case 2: priority = BitTorrent::DownloadPriority::Normal; break; } const QModelIndex &index = selectedRows[i]; model()->setData(index.sibling(index.row(), Priority), static_cast(priority)); } } void TorrentContentWidget::openSelectedFile() { const QModelIndexList selectedIndexes = selectionModel()->selectedRows(0); if (selectedIndexes.size() != 1) return; openItem(selectedIndexes.first()); } void TorrentContentWidget::setModel([[maybe_unused]] QAbstractItemModel *model) { Q_ASSERT_X(false, Q_FUNC_INFO, "Changing the model of TorrentContentWidget is not allowed."); } QModelIndex TorrentContentWidget::currentNameCell() const { const QModelIndex current = currentIndex(); if (!current.isValid()) { Q_ASSERT(false); return {}; } return current.siblingAtColumn(TorrentContentModelItem::COL_NAME); } void TorrentContentWidget::displayColumnHeaderMenu() { QMenu *menu = new QMenu(this); menu->setAttribute(Qt::WA_DeleteOnClose); menu->setToolTipsVisible(true); if (m_columnsVisibilityMode == ColumnsVisibilityMode::Editable) { menu->setTitle(tr("Column visibility")); for (int i = 0; i < TorrentContentModelItem::NB_COL; ++i) { const auto columnName = model()->headerData(i, Qt::Horizontal, Qt::DisplayRole).toString(); QAction *action = menu->addAction(columnName, this, [this, i](bool checked) { setColumnHidden(i, !checked); if (checked && (columnWidth(i) <= 5)) resizeColumnToContents(i); emit stateChanged(); }); action->setCheckable(true); action->setChecked(!isColumnHidden(i)); if (i == TorrentContentModelItem::COL_NAME) action->setEnabled(false); } menu->addSeparator(); } QAction *resizeAction = menu->addAction(tr("Resize columns"), this, [this]() { for (int i = 0, count = header()->count(); i < count; ++i) { if (!isColumnHidden(i)) resizeColumnToContents(i); } emit stateChanged(); }); resizeAction->setToolTip(tr("Resize all non-hidden columns to the size of their contents")); menu->popup(QCursor::pos()); } void TorrentContentWidget::displayContextMenu() { const QModelIndexList selectedRows = selectionModel()->selectedRows(0); if (selectedRows.empty()) return; QMenu *menu = new QMenu(this); menu->setAttribute(Qt::WA_DeleteOnClose); if (selectedRows.size() == 1) { const QModelIndex index = selectedRows[0]; if (!contentHandler()->actualStorageLocation().isEmpty()) { menu->addAction(UIThemeManager::instance()->getIcon(u"folder-documents"_qs), tr("Open") , this, [this, index]() { openItem(index); }); menu->addAction(UIThemeManager::instance()->getIcon(u"directory"_qs), tr("Open containing folder") , this, [this, index]() { openParentFolder(index); }); } menu->addAction(UIThemeManager::instance()->getIcon(u"edit-rename"_qs), tr("Rename...") , this, &TorrentContentWidget::renameSelectedFile); menu->addSeparator(); QMenu *subMenu = menu->addMenu(tr("Priority")); subMenu->addAction(tr("Do not download"), this, [this] { applyPriorities(BitTorrent::DownloadPriority::Ignored); }); subMenu->addAction(tr("Normal"), this, [this] { applyPriorities(BitTorrent::DownloadPriority::Normal); }); subMenu->addAction(tr("High"), this, [this] { applyPriorities(BitTorrent::DownloadPriority::High); }); subMenu->addAction(tr("Maximum"), this, [this] { applyPriorities(BitTorrent::DownloadPriority::Maximum); }); subMenu->addSeparator(); subMenu->addAction(tr("By shown file order"), this, &TorrentContentWidget::applyPrioritiesByOrder); } else { menu->addAction(tr("Do not download"), this, [this] { applyPriorities(BitTorrent::DownloadPriority::Ignored); }); menu->addAction(tr("Normal priority"), this, [this] { applyPriorities(BitTorrent::DownloadPriority::Normal); }); menu->addAction(tr("High priority"), this, [this] { applyPriorities(BitTorrent::DownloadPriority::High); }); menu->addAction(tr("Maximum priority"), this, [this] { applyPriorities(BitTorrent::DownloadPriority::Maximum); }); menu->addSeparator(); menu->addAction(tr("Priority by shown file order"), this, &TorrentContentWidget::applyPrioritiesByOrder); } // The selected torrent might have disappeared during exec() // so we just close menu when an appropriate model is reset connect(model(), &QAbstractItemModel::modelAboutToBeReset, menu, [menu]() { menu->setActiveAction(nullptr); menu->close(); }); menu->popup(QCursor::pos()); } void TorrentContentWidget::openItem(const QModelIndex &index) const { if (!index.isValid()) return; m_model->contentHandler()->flushCache(); // Flush data Utils::Gui::openPath(getFullPath(index)); } void TorrentContentWidget::openParentFolder(const QModelIndex &index) const { const Path path = getFullPath(index); m_model->contentHandler()->flushCache(); // Flush data #ifdef Q_OS_MACOS MacUtils::openFiles({path}); #else Utils::Gui::openFolderSelect(path); #endif } Path TorrentContentWidget::getFullPath(const QModelIndex &index) const { const auto contentHandler = m_model->contentHandler(); if (const int fileIdx = getFileIndex(index); fileIdx >= 0) { const Path fullPath = contentHandler->actualStorageLocation() / contentHandler->actualFilePath(fileIdx); return fullPath; } // folder type const Path fullPath = contentHandler->actualStorageLocation() / getItemPath(index); return fullPath; } void TorrentContentWidget::onItemDoubleClicked(const QModelIndex &index) { const auto contentHandler = m_model->contentHandler(); Q_ASSERT(contentHandler && contentHandler->hasMetadata()); if (Q_UNLIKELY(!contentHandler || !contentHandler->hasMetadata())) return; if (m_doubleClickAction == DoubleClickAction::Rename) renameSelectedFile(); else openItem(index); } void TorrentContentWidget::expandRecursively() { QModelIndex currentIndex; while (model()->rowCount(currentIndex) == 1) { currentIndex = model()->index(0, 0, currentIndex); setExpanded(currentIndex, true); } } void TorrentContentWidget::wheelEvent(QWheelEvent *event) { if (event->modifiers() & Qt::ShiftModifier) { // Shift + scroll = horizontal scroll event->accept(); QWheelEvent scrollHEvent {event->position(), event->globalPosition() , event->pixelDelta(), event->angleDelta().transposed(), event->buttons() , event->modifiers(), event->phase(), event->inverted(), event->source()}; QTreeView::wheelEvent(&scrollHEvent); return; } QTreeView::wheelEvent(event); // event delegated to base class }