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"
33 #include <QHeaderView>
37 #include <QMessageBox>
38 #include <QModelIndexList>
41 #include <QWheelEvent>
43 #include "base/bittorrent/torrentcontenthandler.h"
44 #include "base/path.h"
45 #include "base/utils/string.h"
46 #include "autoexpandabledialog.h"
47 #include "raisedmessagebox.h"
48 #include "torrentcontentfiltermodel.h"
49 #include "torrentcontentitemdelegate.h"
50 #include "torrentcontentmodel.h"
51 #include "torrentcontentmodelitem.h"
52 #include "uithememanager.h"
56 #include "gui/macutilities.h"
61 QList
<QPersistentModelIndex
> toPersistentIndexes(const QModelIndexList
&indexes
)
63 QList
<QPersistentModelIndex
> persistentIndexes
;
64 persistentIndexes
.reserve(indexes
.size());
65 for (const QModelIndex
&index
: indexes
)
66 persistentIndexes
.emplaceBack(index
);
68 return persistentIndexes
;
72 TorrentContentWidget::TorrentContentWidget(QWidget
*parent
)
75 setExpandsOnDoubleClick(false);
76 setSortingEnabled(true);
77 setUniformRowHeights(true);
78 header()->setSortIndicator(0, Qt::AscendingOrder
);
79 header()->setFirstSectionMovable(true);
80 header()->setContextMenuPolicy(Qt::CustomContextMenu
);
82 m_model
= new TorrentContentModel(this);
83 connect(m_model
, &TorrentContentModel::renameFailed
, this, [this](const QString
&errorMessage
)
85 RaisedMessageBox::warning(this, tr("Rename error"), errorMessage
, QMessageBox::Ok
);
88 m_filterModel
= new TorrentContentFilterModel(this);
89 m_filterModel
->setSourceModel(m_model
);
90 QTreeView::setModel(m_filterModel
);
92 auto *itemDelegate
= new TorrentContentItemDelegate(this);
93 setItemDelegate(itemDelegate
);
95 connect(this, &QAbstractItemView::clicked
, this, qOverload
<const QModelIndex
&>(&QAbstractItemView::edit
));
96 connect(this, &QAbstractItemView::doubleClicked
, this, &TorrentContentWidget::onItemDoubleClicked
);
97 connect(this, &QWidget::customContextMenuRequested
, this, &TorrentContentWidget::displayContextMenu
);
98 connect(header(), &QWidget::customContextMenuRequested
, this, &TorrentContentWidget::displayColumnHeaderMenu
);
99 connect(header(), &QHeaderView::sectionMoved
, this, &TorrentContentWidget::stateChanged
);
100 connect(header(), &QHeaderView::sectionResized
, this, &TorrentContentWidget::stateChanged
);
101 connect(header(), &QHeaderView::sortIndicatorChanged
, this, &TorrentContentWidget::stateChanged
);
103 const auto *renameFileHotkey
= new QShortcut(Qt::Key_F2
, this, nullptr, nullptr, Qt::WidgetShortcut
);
104 connect(renameFileHotkey
, &QShortcut::activated
, this, &TorrentContentWidget::renameSelectedFile
);
106 connect(model(), &QAbstractItemModel::modelReset
, this, &TorrentContentWidget::expandRecursively
);
109 void TorrentContentWidget::setContentHandler(BitTorrent::TorrentContentHandler
*contentHandler
)
111 m_model
->setContentHandler(contentHandler
);
118 BitTorrent::TorrentContentHandler
*TorrentContentWidget::contentHandler() const
120 return m_model
->contentHandler();
123 void TorrentContentWidget::refresh()
125 setUpdatesEnabled(false);
127 setUpdatesEnabled(true);
130 bool TorrentContentWidget::openByEnterKey() const
132 return m_openFileHotkeyEnter
;
135 void TorrentContentWidget::setOpenByEnterKey(const bool value
)
137 if (value
== openByEnterKey())
142 m_openFileHotkeyReturn
= new QShortcut(Qt::Key_Return
, this, nullptr, nullptr, Qt::WidgetShortcut
);
143 connect(m_openFileHotkeyReturn
, &QShortcut::activated
, this, &TorrentContentWidget::openSelectedFile
);
144 m_openFileHotkeyEnter
= new QShortcut(Qt::Key_Enter
, this, nullptr, nullptr, Qt::WidgetShortcut
);
145 connect(m_openFileHotkeyEnter
, &QShortcut::activated
, this, &TorrentContentWidget::openSelectedFile
);
149 delete m_openFileHotkeyEnter
;
150 m_openFileHotkeyEnter
= nullptr;
151 delete m_openFileHotkeyReturn
;
152 m_openFileHotkeyReturn
= nullptr;
156 TorrentContentWidget::DoubleClickAction
TorrentContentWidget::doubleClickAction() const
158 return m_doubleClickAction
;
161 void TorrentContentWidget::setDoubleClickAction(DoubleClickAction action
)
163 m_doubleClickAction
= action
;
166 TorrentContentWidget::ColumnsVisibilityMode
TorrentContentWidget::columnsVisibilityMode() const
168 return m_columnsVisibilityMode
;
171 void TorrentContentWidget::setColumnsVisibilityMode(ColumnsVisibilityMode mode
)
173 m_columnsVisibilityMode
= mode
;
176 int TorrentContentWidget::getFileIndex(const QModelIndex
&index
) const
178 return m_filterModel
->getFileIndex(index
);
181 Path
TorrentContentWidget::getItemPath(const QModelIndex
&index
) const
184 for (QModelIndex i
= index
; i
.isValid(); i
= i
.parent())
185 path
= Path(i
.data().toString()) / path
;
189 void TorrentContentWidget::setFilterPattern(const QString
&patternText
, const FilterPatternFormat format
)
191 if (format
== FilterPatternFormat::PlainText
)
193 m_filterModel
->setFilterFixedString(patternText
);
194 m_filterModel
->setFilterCaseSensitivity(Qt::CaseInsensitive
);
198 const QString pattern
= ((format
== FilterPatternFormat::Regex
)
199 ? patternText
: Utils::String::wildcardToRegexPattern(patternText
));
200 m_filterModel
->setFilterRegularExpression(QRegularExpression(pattern
, QRegularExpression::CaseInsensitiveOption
));
203 if (patternText
.isEmpty())
206 expand(m_filterModel
->index(0, 0));
214 void TorrentContentWidget::checkAll()
216 for (int i
= 0; i
< model()->rowCount(); ++i
)
217 model()->setData(model()->index(i
, TorrentContentModelItem::COL_NAME
), Qt::Checked
, Qt::CheckStateRole
);
220 void TorrentContentWidget::checkNone()
222 for (int i
= 0; i
< model()->rowCount(); ++i
)
223 model()->setData(model()->index(i
, TorrentContentModelItem::COL_NAME
), Qt::Unchecked
, Qt::CheckStateRole
);
226 void TorrentContentWidget::keyPressEvent(QKeyEvent
*event
)
228 if ((event
->key() != Qt::Key_Space
) && (event
->key() != Qt::Key_Select
))
230 QTreeView::keyPressEvent(event
);
236 const QVariant value
= currentNameCell().data(Qt::CheckStateRole
);
237 if (!value
.isValid())
243 const Qt::CheckState state
= (static_cast<Qt::CheckState
>(value
.toInt()) == Qt::Checked
)
244 ? Qt::Unchecked
: Qt::Checked
;
245 const QList
<QPersistentModelIndex
> selection
= toPersistentIndexes(selectionModel()->selectedRows(TorrentContentModelItem::COL_NAME
));
247 for (const QPersistentModelIndex
&index
: selection
)
248 model()->setData(index
, state
, Qt::CheckStateRole
);
251 void TorrentContentWidget::renameSelectedFile()
253 const QModelIndexList selectedIndexes
= selectionModel()->selectedRows(0);
254 if (selectedIndexes
.size() != 1)
257 const QPersistentModelIndex modelIndex
= selectedIndexes
.first();
258 if (!modelIndex
.isValid())
262 const bool isFile
= (m_filterModel
->itemType(modelIndex
) == TorrentContentModelItem::FileType
);
264 QString newName
= AutoExpandableDialog::getText(this, tr("Renaming"), tr("New name:"), QLineEdit::Normal
265 , modelIndex
.data().toString(), &ok
, isFile
).trimmed();
266 if (!ok
|| !modelIndex
.isValid())
269 model()->setData(modelIndex
, newName
);
272 void TorrentContentWidget::applyPriorities(const BitTorrent::DownloadPriority priority
)
274 const QList
<QPersistentModelIndex
> selectedRows
= toPersistentIndexes(selectionModel()->selectedRows(Priority
));
275 for (const QPersistentModelIndex
&index
: selectedRows
)
277 model()->setData(index
, static_cast<int>(priority
));
281 void TorrentContentWidget::applyPrioritiesByOrder()
283 // Equally distribute the selected items into groups and for each group assign
284 // a download priority that will apply to each item. The number of groups depends on how
285 // many "download priority" are available to be assigned
287 const QList
<QPersistentModelIndex
> selectedRows
= toPersistentIndexes(selectionModel()->selectedRows(Priority
));
289 const qsizetype priorityGroups
= 3;
290 const auto priorityGroupSize
= std::max
<qsizetype
>((selectedRows
.length() / priorityGroups
), 1);
292 for (qsizetype i
= 0; i
< selectedRows
.length(); ++i
)
294 auto priority
= BitTorrent::DownloadPriority::Ignored
;
295 switch (i
/ priorityGroupSize
)
298 priority
= BitTorrent::DownloadPriority::Maximum
;
301 priority
= BitTorrent::DownloadPriority::High
;
305 priority
= BitTorrent::DownloadPriority::Normal
;
309 const QPersistentModelIndex
&index
= selectedRows
[i
];
310 model()->setData(index
, static_cast<int>(priority
));
314 void TorrentContentWidget::openSelectedFile()
316 const QModelIndexList selectedIndexes
= selectionModel()->selectedRows(0);
317 if (selectedIndexes
.size() != 1)
319 openItem(selectedIndexes
.first());
322 void TorrentContentWidget::setModel([[maybe_unused
]] QAbstractItemModel
*model
)
324 Q_ASSERT_X(false, Q_FUNC_INFO
, "Changing the model of TorrentContentWidget is not allowed.");
327 QModelIndex
TorrentContentWidget::currentNameCell() const
329 const QModelIndex current
= currentIndex();
330 if (!current
.isValid())
336 return current
.siblingAtColumn(TorrentContentModelItem::COL_NAME
);
339 void TorrentContentWidget::displayColumnHeaderMenu()
341 QMenu
*menu
= new QMenu(this);
342 menu
->setAttribute(Qt::WA_DeleteOnClose
);
343 menu
->setToolTipsVisible(true);
345 if (m_columnsVisibilityMode
== ColumnsVisibilityMode::Editable
)
347 menu
->setTitle(tr("Column visibility"));
348 for (int i
= 0; i
< TorrentContentModelItem::NB_COL
; ++i
)
350 const auto columnName
= model()->headerData(i
, Qt::Horizontal
, Qt::DisplayRole
).toString();
351 QAction
*action
= menu
->addAction(columnName
, this, [this, i
](bool checked
)
353 setColumnHidden(i
, !checked
);
355 if (checked
&& (columnWidth(i
) <= 5))
356 resizeColumnToContents(i
);
360 action
->setCheckable(true);
361 action
->setChecked(!isColumnHidden(i
));
363 if (i
== TorrentContentModelItem::COL_NAME
)
364 action
->setEnabled(false);
367 menu
->addSeparator();
370 QAction
*resizeAction
= menu
->addAction(tr("Resize columns"), this, [this]()
372 for (int i
= 0, count
= header()->count(); i
< count
; ++i
)
374 if (!isColumnHidden(i
))
375 resizeColumnToContents(i
);
380 resizeAction
->setToolTip(tr("Resize all non-hidden columns to the size of their contents"));
382 menu
->popup(QCursor::pos());
385 void TorrentContentWidget::displayContextMenu()
387 const QModelIndexList selectedRows
= selectionModel()->selectedRows(0);
388 if (selectedRows
.empty())
391 QMenu
*menu
= new QMenu(this);
392 menu
->setAttribute(Qt::WA_DeleteOnClose
);
394 if (selectedRows
.size() == 1)
396 const QModelIndex index
= selectedRows
[0];
398 if (!contentHandler()->actualStorageLocation().isEmpty())
400 menu
->addAction(UIThemeManager::instance()->getIcon(u
"folder-documents"_s
), tr("Open")
401 , this, [this, index
]() { openItem(index
); });
402 menu
->addAction(UIThemeManager::instance()->getIcon(u
"directory"_s
), tr("Open containing folder")
403 , this, [this, index
]() { openParentFolder(index
); });
405 menu
->addAction(UIThemeManager::instance()->getIcon(u
"edit-rename"_s
), tr("Rename...")
406 , this, &TorrentContentWidget::renameSelectedFile
);
407 menu
->addSeparator();
409 QMenu
*subMenu
= menu
->addMenu(tr("Priority"));
411 subMenu
->addAction(tr("Do not download"), this, [this]
413 applyPriorities(BitTorrent::DownloadPriority::Ignored
);
415 subMenu
->addAction(tr("Normal"), this, [this]
417 applyPriorities(BitTorrent::DownloadPriority::Normal
);
419 subMenu
->addAction(tr("High"), this, [this]
421 applyPriorities(BitTorrent::DownloadPriority::High
);
423 subMenu
->addAction(tr("Maximum"), this, [this]
425 applyPriorities(BitTorrent::DownloadPriority::Maximum
);
427 subMenu
->addSeparator();
428 subMenu
->addAction(tr("By shown file order"), this, &TorrentContentWidget::applyPrioritiesByOrder
);
432 menu
->addAction(tr("Do not download"), this, [this]
434 applyPriorities(BitTorrent::DownloadPriority::Ignored
);
436 menu
->addAction(tr("Normal priority"), this, [this]
438 applyPriorities(BitTorrent::DownloadPriority::Normal
);
440 menu
->addAction(tr("High priority"), this, [this]
442 applyPriorities(BitTorrent::DownloadPriority::High
);
444 menu
->addAction(tr("Maximum priority"), this, [this]
446 applyPriorities(BitTorrent::DownloadPriority::Maximum
);
448 menu
->addSeparator();
449 menu
->addAction(tr("Priority by shown file order"), this, &TorrentContentWidget::applyPrioritiesByOrder
);
452 // The selected torrent might have disappeared during exec()
453 // so we just close menu when an appropriate model is reset
454 connect(model(), &QAbstractItemModel::modelAboutToBeReset
, menu
, [menu
]()
456 menu
->setActiveAction(nullptr);
460 menu
->popup(QCursor::pos());
463 void TorrentContentWidget::openItem(const QModelIndex
&index
) const
465 if (!index
.isValid())
468 m_model
->contentHandler()->flushCache(); // Flush data
469 Utils::Gui::openPath(getFullPath(index
));
472 void TorrentContentWidget::openParentFolder(const QModelIndex
&index
) const
474 const Path path
= getFullPath(index
);
475 m_model
->contentHandler()->flushCache(); // Flush data
477 MacUtils::openFiles({path
});
479 Utils::Gui::openFolderSelect(path
);
483 Path
TorrentContentWidget::getFullPath(const QModelIndex
&index
) const
485 const auto *contentHandler
= m_model
->contentHandler();
486 if (const int fileIdx
= getFileIndex(index
); fileIdx
>= 0)
488 const Path fullPath
= contentHandler
->actualStorageLocation() / contentHandler
->actualFilePath(fileIdx
);
493 const Path fullPath
= contentHandler
->actualStorageLocation() / getItemPath(index
);
497 void TorrentContentWidget::onItemDoubleClicked(const QModelIndex
&index
)
499 const auto *contentHandler
= m_model
->contentHandler();
500 Q_ASSERT(contentHandler
&& contentHandler
->hasMetadata());
502 if (!contentHandler
|| !contentHandler
->hasMetadata()) [[unlikely
]]
505 if (m_doubleClickAction
== DoubleClickAction::Rename
)
506 renameSelectedFile();
511 void TorrentContentWidget::expandRecursively()
513 QModelIndex currentIndex
;
514 while (model()->rowCount(currentIndex
) == 1)
516 currentIndex
= model()->index(0, 0, currentIndex
);
517 setExpanded(currentIndex
, true);
521 void TorrentContentWidget::wheelEvent(QWheelEvent
*event
)
523 if (event
->modifiers() & Qt::ShiftModifier
)
525 // Shift + scroll = horizontal scroll
527 QWheelEvent scrollHEvent
{event
->position(), event
->globalPosition()
528 , event
->pixelDelta(), event
->angleDelta().transposed(), event
->buttons()
529 , event
->modifiers(), event
->phase(), event
->inverted(), event
->source()};
530 QTreeView::wheelEvent(&scrollHEvent
);
534 QTreeView::wheelEvent(event
); // event delegated to base class