Vladimir Golovnev
2 years ago
committed by
GitHub
23 changed files with 1161 additions and 1096 deletions
@ -0,0 +1,62 @@ |
|||||||
|
/*
|
||||||
|
* Bittorrent Client using Qt and libtorrent. |
||||||
|
* Copyright (C) 2022 Vladimir Golovnev <glassez@yandex.ru> |
||||||
|
* |
||||||
|
* 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. |
||||||
|
*/ |
||||||
|
|
||||||
|
#pragma once |
||||||
|
|
||||||
|
#include <QObject> |
||||||
|
|
||||||
|
#include "base/pathfwd.h" |
||||||
|
#include "abstractfilestorage.h" |
||||||
|
#include "downloadpriority.h" |
||||||
|
|
||||||
|
namespace BitTorrent |
||||||
|
{ |
||||||
|
class TorrentContentHandler : public QObject, public AbstractFileStorage |
||||||
|
{ |
||||||
|
public: |
||||||
|
using QObject::QObject; |
||||||
|
|
||||||
|
virtual bool hasMetadata() const = 0; |
||||||
|
virtual Path actualStorageLocation() const = 0; |
||||||
|
virtual Path actualFilePath(int fileIndex) const = 0; |
||||||
|
virtual QVector<DownloadPriority> filePriorities() const = 0; |
||||||
|
virtual QVector<qreal> filesProgress() const = 0; |
||||||
|
virtual void fetchFilesProgress(std::function<void (QVector<qreal>)> resultHandler) const = 0; |
||||||
|
/**
|
||||||
|
* @brief fraction of file pieces that are available at least from one peer |
||||||
|
* |
||||||
|
* This is not the same as torrrent availability, it is just a fraction of pieces |
||||||
|
* that can be downloaded right now. It varies between 0 to 1. |
||||||
|
*/ |
||||||
|
virtual QVector<qreal> availableFileFractions() const = 0; |
||||||
|
virtual void fetchAvailableFileFractions(std::function<void (QVector<qreal>)> resultHandler) const = 0; |
||||||
|
|
||||||
|
virtual void prioritizeFiles(const QVector<DownloadPriority> &priorities) = 0; |
||||||
|
virtual void flushCache() const = 0; |
||||||
|
}; |
||||||
|
} |
@ -1,165 +0,0 @@ |
|||||||
/*
|
|
||||||
* Bittorrent Client using Qt and libtorrent. |
|
||||||
* Copyright (C) 2014 Ivan Sorokin <vanyacpp@gmail.com> |
|
||||||
* |
|
||||||
* 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 "torrentcontenttreeview.h" |
|
||||||
|
|
||||||
#include <QDir> |
|
||||||
#include <QHeaderView> |
|
||||||
#include <QKeyEvent> |
|
||||||
#include <QLineEdit> |
|
||||||
#include <QMessageBox> |
|
||||||
#include <QModelIndexList> |
|
||||||
#include <QThread> |
|
||||||
#include <QWheelEvent> |
|
||||||
|
|
||||||
#include "base/bittorrent/abstractfilestorage.h" |
|
||||||
#include "base/bittorrent/common.h" |
|
||||||
#include "base/bittorrent/session.h" |
|
||||||
#include "base/bittorrent/torrent.h" |
|
||||||
#include "base/bittorrent/torrentinfo.h" |
|
||||||
#include "base/exceptions.h" |
|
||||||
#include "base/global.h" |
|
||||||
#include "base/path.h" |
|
||||||
#include "base/utils/fs.h" |
|
||||||
#include "autoexpandabledialog.h" |
|
||||||
#include "raisedmessagebox.h" |
|
||||||
#include "torrentcontentfiltermodel.h" |
|
||||||
#include "torrentcontentmodelitem.h" |
|
||||||
|
|
||||||
namespace |
|
||||||
{ |
|
||||||
Path getFullPath(const QModelIndex &idx) |
|
||||||
{ |
|
||||||
Path path; |
|
||||||
for (QModelIndex i = idx; i.isValid(); i = i.parent()) |
|
||||||
path = Path(i.data().toString()) / path; |
|
||||||
return path; |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
TorrentContentTreeView::TorrentContentTreeView(QWidget *parent) |
|
||||||
: QTreeView(parent) |
|
||||||
{ |
|
||||||
setExpandsOnDoubleClick(false); |
|
||||||
header()->setFirstSectionMovable(true); |
|
||||||
} |
|
||||||
|
|
||||||
void TorrentContentTreeView::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<Qt::CheckState>(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 TorrentContentTreeView::renameSelectedFile(BitTorrent::AbstractFileStorage &fileStorage) |
|
||||||
{ |
|
||||||
const QModelIndexList selectedIndexes = selectionModel()->selectedRows(0); |
|
||||||
if (selectedIndexes.size() != 1) return; |
|
||||||
|
|
||||||
const QPersistentModelIndex modelIndex = selectedIndexes.first(); |
|
||||||
if (!modelIndex.isValid()) return; |
|
||||||
|
|
||||||
auto model = dynamic_cast<TorrentContentFilterModel *>(TorrentContentTreeView::model()); |
|
||||||
if (!model) return; |
|
||||||
|
|
||||||
const bool isFile = (model->itemType(modelIndex) == TorrentContentModelItem::FileType); |
|
||||||
|
|
||||||
// Ask for new name
|
|
||||||
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; |
|
||||||
|
|
||||||
const QString oldName = modelIndex.data().toString(); |
|
||||||
if (newName == oldName) |
|
||||||
return; // Name did not change
|
|
||||||
|
|
||||||
const Path parentPath = getFullPath(modelIndex.parent()); |
|
||||||
const Path oldPath = parentPath / Path(oldName); |
|
||||||
const Path newPath = parentPath / Path(newName); |
|
||||||
|
|
||||||
try |
|
||||||
{ |
|
||||||
if (isFile) |
|
||||||
fileStorage.renameFile(oldPath, newPath); |
|
||||||
else |
|
||||||
fileStorage.renameFolder(oldPath, newPath); |
|
||||||
|
|
||||||
model->setData(modelIndex, newName); |
|
||||||
} |
|
||||||
catch (const RuntimeError &error) |
|
||||||
{ |
|
||||||
RaisedMessageBox::warning(this, tr("Rename error"), error.message(), QMessageBox::Ok); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
QModelIndex TorrentContentTreeView::currentNameCell() const |
|
||||||
{ |
|
||||||
const QModelIndex current = currentIndex(); |
|
||||||
if (!current.isValid()) |
|
||||||
{ |
|
||||||
Q_ASSERT(false); |
|
||||||
return {}; |
|
||||||
} |
|
||||||
|
|
||||||
return current.siblingAtColumn(TorrentContentModelItem::COL_NAME); |
|
||||||
} |
|
||||||
|
|
||||||
void TorrentContentTreeView::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
|
|
||||||
} |
|
@ -0,0 +1,489 @@ |
|||||||
|
/*
|
||||||
|
* Bittorrent Client using Qt and libtorrent. |
||||||
|
* Copyright (C) 2022 Vladimir Golovnev <glassez@yandex.ru> |
||||||
|
* Copyright (C) 2014 Ivan Sorokin <vanyacpp@gmail.com> |
||||||
|
* |
||||||
|
* 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 <QDir> |
||||||
|
#include <QHeaderView> |
||||||
|
#include <QKeyEvent> |
||||||
|
#include <QLineEdit> |
||||||
|
#include <QMenu> |
||||||
|
#include <QMessageBox> |
||||||
|
#include <QModelIndexList> |
||||||
|
#include <QShortcut> |
||||||
|
#include <QThread> |
||||||
|
#include <QWheelEvent> |
||||||
|
|
||||||
|
#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<const QModelIndex &>(&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<Qt::CheckState>(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<int>(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<qsizetype>((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<int>(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
|
||||||
|
} |
@ -0,0 +1,121 @@ |
|||||||
|
/*
|
||||||
|
* Bittorrent Client using Qt and libtorrent. |
||||||
|
* Copyright (C) 2022 Vladimir Golovnev <glassez@yandex.ru> |
||||||
|
* Copyright (C) 2014 Ivan Sorokin <vanyacpp@gmail.com> |
||||||
|
* |
||||||
|
* 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. |
||||||
|
*/ |
||||||
|
|
||||||
|
#pragma once |
||||||
|
|
||||||
|
#include <QTreeView> |
||||||
|
|
||||||
|
#include "base/bittorrent/downloadpriority.h" |
||||||
|
#include "base/pathfwd.h" |
||||||
|
|
||||||
|
namespace BitTorrent |
||||||
|
{ |
||||||
|
class Torrent; |
||||||
|
class TorrentContentHandler; |
||||||
|
class TorrentInfo; |
||||||
|
} |
||||||
|
|
||||||
|
class TorrentContentFilterModel; |
||||||
|
class TorrentContentModel; |
||||||
|
|
||||||
|
class TorrentContentWidget final : public QTreeView |
||||||
|
{ |
||||||
|
Q_OBJECT |
||||||
|
Q_DISABLE_COPY_MOVE(TorrentContentWidget) |
||||||
|
|
||||||
|
public: |
||||||
|
enum Column |
||||||
|
{ |
||||||
|
Name, |
||||||
|
Size, |
||||||
|
Progress, |
||||||
|
Priority, |
||||||
|
Remaining, |
||||||
|
Availability |
||||||
|
}; |
||||||
|
|
||||||
|
enum class DoubleClickAction |
||||||
|
{ |
||||||
|
Open, |
||||||
|
Rename |
||||||
|
}; |
||||||
|
|
||||||
|
enum class ColumnsVisibilityMode |
||||||
|
{ |
||||||
|
Editable, |
||||||
|
Locked |
||||||
|
}; |
||||||
|
|
||||||
|
explicit TorrentContentWidget(QWidget *parent = nullptr); |
||||||
|
|
||||||
|
void setContentHandler(BitTorrent::TorrentContentHandler *contentHandler); |
||||||
|
BitTorrent::TorrentContentHandler *contentHandler() const; |
||||||
|
void refresh(); |
||||||
|
|
||||||
|
DoubleClickAction doubleClickAction() const; |
||||||
|
void setDoubleClickAction(DoubleClickAction action); |
||||||
|
|
||||||
|
ColumnsVisibilityMode columnsVisibilityMode() const; |
||||||
|
void setColumnsVisibilityMode(ColumnsVisibilityMode mode); |
||||||
|
|
||||||
|
int getFileIndex(const QModelIndex &index) const; |
||||||
|
Path getItemPath(const QModelIndex &index) const; |
||||||
|
|
||||||
|
void setFilterPattern(const QString &patternText); |
||||||
|
|
||||||
|
void checkAll(); |
||||||
|
void checkNone(); |
||||||
|
|
||||||
|
signals: |
||||||
|
void stateChanged(); |
||||||
|
|
||||||
|
private: |
||||||
|
void setModel(QAbstractItemModel *model) override; |
||||||
|
void keyPressEvent(QKeyEvent *event) override; |
||||||
|
void wheelEvent(QWheelEvent *event) override; |
||||||
|
QModelIndex currentNameCell() const; |
||||||
|
void displayColumnHeaderMenu(); |
||||||
|
void displayContextMenu(); |
||||||
|
void openItem(const QModelIndex &index) const; |
||||||
|
void openParentFolder(const QModelIndex &index) const; |
||||||
|
void openSelectedFile(); |
||||||
|
void renameSelectedFile(); |
||||||
|
void applyPriorities(BitTorrent::DownloadPriority priority); |
||||||
|
void applyPrioritiesByOrder(); |
||||||
|
Path getFullPath(const QModelIndex &index) const; |
||||||
|
void onItemDoubleClicked(const QModelIndex &index); |
||||||
|
// Expand single-item folders recursively.
|
||||||
|
// This will trigger sorting and filtering so do it after all relevant data is loaded.
|
||||||
|
void expandRecursively(); |
||||||
|
|
||||||
|
TorrentContentModel *m_model; |
||||||
|
TorrentContentFilterModel *m_filterModel; |
||||||
|
DoubleClickAction m_doubleClickAction = DoubleClickAction::Rename; |
||||||
|
ColumnsVisibilityMode m_columnsVisibilityMode = ColumnsVisibilityMode::Editable; |
||||||
|
}; |
Loading…
Reference in new issue