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>
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"
55 #include "gui/macutilities.h"
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
)
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
);
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
);
119 BitTorrent::TorrentContentHandler
*TorrentContentWidget::contentHandler() const
121 return m_model
->contentHandler();
124 void TorrentContentWidget::refresh()
126 setUpdatesEnabled(false);
128 setUpdatesEnabled(true);
131 bool TorrentContentWidget::openByEnterKey() const
133 return m_openFileHotkeyEnter
;
136 void TorrentContentWidget::setOpenByEnterKey(const bool value
)
138 if (value
== openByEnterKey())
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
);
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
185 for (QModelIndex i
= index
; i
.isValid(); i
= i
.parent())
186 path
= Path(i
.data().toString()) / 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
);
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())
207 expand(m_filterModel
->index(0, 0));
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
);
237 const QVariant value
= currentNameCell().data(Qt::CheckStateRole
);
238 if (!value
.isValid())
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)
258 const QPersistentModelIndex modelIndex
= selectedIndexes
.first();
259 if (!modelIndex
.isValid())
263 const bool isFile
= (m_filterModel
->itemType(modelIndex
) == TorrentContentModelItem::FileType
);
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())
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
)
299 priority
= BitTorrent::DownloadPriority::Maximum
;
302 priority
= BitTorrent::DownloadPriority::High
;
306 priority
= BitTorrent::DownloadPriority::Normal
;
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)
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())
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
);
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
);
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())
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
);
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);
461 menu
->popup(QCursor::pos());
464 void TorrentContentWidget::openItem(const QModelIndex
&index
) const
466 if (!index
.isValid())
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
478 MacUtils::openFiles({path
});
480 Utils::Gui::openFolderSelect(path
);
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
);
494 const Path fullPath
= contentHandler
->actualStorageLocation() / getItemPath(index
);
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
]]
506 if (m_doubleClickAction
== DoubleClickAction::Rename
)
507 renameSelectedFile();
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
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
);
535 QTreeView::wheelEvent(event
); // event delegated to base class