2 * Bittorrent Client using Qt and libtorrent.
3 * Copyright (C) 2022 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"
59 TorrentContentWidget::TorrentContentWidget(QWidget
*parent
)
62 setExpandsOnDoubleClick(false);
63 setSortingEnabled(true);
64 setUniformRowHeights(true);
65 header()->setSortIndicator(0, Qt::AscendingOrder
);
66 header()->setFirstSectionMovable(true);
67 header()->setContextMenuPolicy(Qt::CustomContextMenu
);
69 m_model
= new TorrentContentModel(this);
70 connect(m_model
, &TorrentContentModel::renameFailed
, this, [this](const QString
&errorMessage
)
72 RaisedMessageBox::warning(this, tr("Rename error"), errorMessage
, QMessageBox::Ok
);
75 m_filterModel
= new TorrentContentFilterModel(this);
76 m_filterModel
->setSourceModel(m_model
);
77 QTreeView::setModel(m_filterModel
);
79 auto *itemDelegate
= new TorrentContentItemDelegate(this);
80 setItemDelegate(itemDelegate
);
82 connect(this, &QAbstractItemView::clicked
, this, qOverload
<const QModelIndex
&>(&QAbstractItemView::edit
));
83 connect(this, &QAbstractItemView::doubleClicked
, this, &TorrentContentWidget::onItemDoubleClicked
);
84 connect(this, &QWidget::customContextMenuRequested
, this, &TorrentContentWidget::displayContextMenu
);
85 connect(header(), &QWidget::customContextMenuRequested
, this, &TorrentContentWidget::displayColumnHeaderMenu
);
86 connect(header(), &QHeaderView::sectionMoved
, this, &TorrentContentWidget::stateChanged
);
87 connect(header(), &QHeaderView::sectionResized
, this, &TorrentContentWidget::stateChanged
);
88 connect(header(), &QHeaderView::sortIndicatorChanged
, this, &TorrentContentWidget::stateChanged
);
90 const auto *renameFileHotkey
= new QShortcut(Qt::Key_F2
, this, nullptr, nullptr, Qt::WidgetShortcut
);
91 connect(renameFileHotkey
, &QShortcut::activated
, this, &TorrentContentWidget::renameSelectedFile
);
92 const auto *openFileHotkeyReturn
= new QShortcut(Qt::Key_Return
, this, nullptr, nullptr, Qt::WidgetShortcut
);
93 connect(openFileHotkeyReturn
, &QShortcut::activated
, this, &TorrentContentWidget::openSelectedFile
);
94 const auto *openFileHotkeyEnter
= new QShortcut(Qt::Key_Enter
, this, nullptr, nullptr, Qt::WidgetShortcut
);
95 connect(openFileHotkeyEnter
, &QShortcut::activated
, this, &TorrentContentWidget::openSelectedFile
);
97 connect(model(), &QAbstractItemModel::modelReset
, this, &TorrentContentWidget::expandRecursively
);
100 void TorrentContentWidget::setContentHandler(BitTorrent::TorrentContentHandler
*contentHandler
)
102 m_model
->setContentHandler(contentHandler
);
109 BitTorrent::TorrentContentHandler
*TorrentContentWidget::contentHandler() const
111 return m_model
->contentHandler();
114 void TorrentContentWidget::refresh()
116 setUpdatesEnabled(false);
118 setUpdatesEnabled(true);
121 TorrentContentWidget::DoubleClickAction
TorrentContentWidget::doubleClickAction() const
123 return m_doubleClickAction
;
126 void TorrentContentWidget::setDoubleClickAction(DoubleClickAction action
)
128 m_doubleClickAction
= action
;
131 TorrentContentWidget::ColumnsVisibilityMode
TorrentContentWidget::columnsVisibilityMode() const
133 return m_columnsVisibilityMode
;
136 void TorrentContentWidget::setColumnsVisibilityMode(ColumnsVisibilityMode mode
)
138 m_columnsVisibilityMode
= mode
;
141 int TorrentContentWidget::getFileIndex(const QModelIndex
&index
) const
143 return m_filterModel
->getFileIndex(index
);
146 Path
TorrentContentWidget::getItemPath(const QModelIndex
&index
) const
149 for (QModelIndex i
= index
; i
.isValid(); i
= i
.parent())
150 path
= Path(i
.data().toString()) / path
;
154 void TorrentContentWidget::setFilterPattern(const QString
&patternText
)
156 const QString pattern
= Utils::String::wildcardToRegexPattern(patternText
);
157 m_filterModel
->setFilterRegularExpression(QRegularExpression(pattern
, QRegularExpression::CaseInsensitiveOption
));
158 if (patternText
.isEmpty())
161 expand(m_filterModel
->index(0, 0));
169 void TorrentContentWidget::checkAll()
171 for (int i
= 0; i
< model()->rowCount(); ++i
)
172 model()->setData(model()->index(i
, TorrentContentModelItem::COL_NAME
), Qt::Checked
, Qt::CheckStateRole
);
175 void TorrentContentWidget::checkNone()
177 for (int i
= 0; i
< model()->rowCount(); ++i
)
178 model()->setData(model()->index(i
, TorrentContentModelItem::COL_NAME
), Qt::Unchecked
, Qt::CheckStateRole
);
181 void TorrentContentWidget::keyPressEvent(QKeyEvent
*event
)
183 if ((event
->key() != Qt::Key_Space
) && (event
->key() != Qt::Key_Select
))
185 QTreeView::keyPressEvent(event
);
191 const QVariant value
= currentNameCell().data(Qt::CheckStateRole
);
192 if (!value
.isValid())
198 const Qt::CheckState state
= (static_cast<Qt::CheckState
>(value
.toInt()) == Qt::Checked
)
199 ? Qt::Unchecked
: Qt::Checked
;
200 const QModelIndexList selection
= selectionModel()->selectedRows(TorrentContentModelItem::COL_NAME
);
202 for (const QModelIndex
&index
: selection
)
203 model()->setData(index
, state
, Qt::CheckStateRole
);
206 void TorrentContentWidget::renameSelectedFile()
208 const QModelIndexList selectedIndexes
= selectionModel()->selectedRows(0);
209 if (selectedIndexes
.size() != 1)
212 const QPersistentModelIndex modelIndex
= selectedIndexes
.first();
213 if (!modelIndex
.isValid())
217 const bool isFile
= (m_filterModel
->itemType(modelIndex
) == TorrentContentModelItem::FileType
);
219 QString newName
= AutoExpandableDialog::getText(this, tr("Renaming"), tr("New name:"), QLineEdit::Normal
220 , modelIndex
.data().toString(), &ok
, isFile
).trimmed();
221 if (!ok
|| !modelIndex
.isValid())
224 model()->setData(modelIndex
, newName
);
227 void TorrentContentWidget::applyPriorities(const BitTorrent::DownloadPriority priority
)
229 const QModelIndexList selectedRows
= selectionModel()->selectedRows(0);
230 for (const QModelIndex
&index
: selectedRows
)
232 model()->setData(index
.sibling(index
.row(), Priority
), static_cast<int>(priority
));
236 void TorrentContentWidget::applyPrioritiesByOrder()
238 // Equally distribute the selected items into groups and for each group assign
239 // a download priority that will apply to each item. The number of groups depends on how
240 // many "download priority" are available to be assigned
242 const QModelIndexList selectedRows
= selectionModel()->selectedRows(0);
244 const qsizetype priorityGroups
= 3;
245 const auto priorityGroupSize
= std::max
<qsizetype
>((selectedRows
.length() / priorityGroups
), 1);
247 for (qsizetype i
= 0; i
< selectedRows
.length(); ++i
)
249 auto priority
= BitTorrent::DownloadPriority::Ignored
;
250 switch (i
/ priorityGroupSize
)
253 priority
= BitTorrent::DownloadPriority::Maximum
;
256 priority
= BitTorrent::DownloadPriority::High
;
260 priority
= BitTorrent::DownloadPriority::Normal
;
264 const QModelIndex
&index
= selectedRows
[i
];
265 model()->setData(index
.sibling(index
.row(), Priority
), static_cast<int>(priority
));
269 void TorrentContentWidget::openSelectedFile()
271 const QModelIndexList selectedIndexes
= selectionModel()->selectedRows(0);
272 if (selectedIndexes
.size() != 1)
274 openItem(selectedIndexes
.first());
277 void TorrentContentWidget::setModel([[maybe_unused
]] QAbstractItemModel
*model
)
279 Q_ASSERT_X(false, Q_FUNC_INFO
, "Changing the model of TorrentContentWidget is not allowed.");
282 QModelIndex
TorrentContentWidget::currentNameCell() const
284 const QModelIndex current
= currentIndex();
285 if (!current
.isValid())
291 return current
.siblingAtColumn(TorrentContentModelItem::COL_NAME
);
294 void TorrentContentWidget::displayColumnHeaderMenu()
296 QMenu
*menu
= new QMenu(this);
297 menu
->setAttribute(Qt::WA_DeleteOnClose
);
298 menu
->setToolTipsVisible(true);
300 if (m_columnsVisibilityMode
== ColumnsVisibilityMode::Editable
)
302 menu
->setTitle(tr("Column visibility"));
303 for (int i
= 0; i
< TorrentContentModelItem::NB_COL
; ++i
)
305 const auto columnName
= model()->headerData(i
, Qt::Horizontal
, Qt::DisplayRole
).toString();
306 QAction
*action
= menu
->addAction(columnName
, this, [this, i
](bool checked
)
308 setColumnHidden(i
, !checked
);
310 if (checked
&& (columnWidth(i
) <= 5))
311 resizeColumnToContents(i
);
315 action
->setCheckable(true);
316 action
->setChecked(!isColumnHidden(i
));
318 if (i
== TorrentContentModelItem::COL_NAME
)
319 action
->setEnabled(false);
322 menu
->addSeparator();
325 QAction
*resizeAction
= menu
->addAction(tr("Resize columns"), this, [this]()
327 for (int i
= 0, count
= header()->count(); i
< count
; ++i
)
329 if (!isColumnHidden(i
))
330 resizeColumnToContents(i
);
335 resizeAction
->setToolTip(tr("Resize all non-hidden columns to the size of their contents"));
337 menu
->popup(QCursor::pos());
340 void TorrentContentWidget::displayContextMenu()
342 const QModelIndexList selectedRows
= selectionModel()->selectedRows(0);
343 if (selectedRows
.empty())
346 QMenu
*menu
= new QMenu(this);
347 menu
->setAttribute(Qt::WA_DeleteOnClose
);
349 if (selectedRows
.size() == 1)
351 const QModelIndex index
= selectedRows
[0];
353 if (!contentHandler()->actualStorageLocation().isEmpty())
355 menu
->addAction(UIThemeManager::instance()->getIcon(u
"folder-documents"_s
), tr("Open")
356 , this, [this, index
]() { openItem(index
); });
357 menu
->addAction(UIThemeManager::instance()->getIcon(u
"directory"_s
), tr("Open containing folder")
358 , this, [this, index
]() { openParentFolder(index
); });
360 menu
->addAction(UIThemeManager::instance()->getIcon(u
"edit-rename"_s
), tr("Rename...")
361 , this, &TorrentContentWidget::renameSelectedFile
);
362 menu
->addSeparator();
364 QMenu
*subMenu
= menu
->addMenu(tr("Priority"));
366 subMenu
->addAction(tr("Do not download"), this, [this]
368 applyPriorities(BitTorrent::DownloadPriority::Ignored
);
370 subMenu
->addAction(tr("Normal"), this, [this]
372 applyPriorities(BitTorrent::DownloadPriority::Normal
);
374 subMenu
->addAction(tr("High"), this, [this]
376 applyPriorities(BitTorrent::DownloadPriority::High
);
378 subMenu
->addAction(tr("Maximum"), this, [this]
380 applyPriorities(BitTorrent::DownloadPriority::Maximum
);
382 subMenu
->addSeparator();
383 subMenu
->addAction(tr("By shown file order"), this, &TorrentContentWidget::applyPrioritiesByOrder
);
387 menu
->addAction(tr("Do not download"), this, [this]
389 applyPriorities(BitTorrent::DownloadPriority::Ignored
);
391 menu
->addAction(tr("Normal priority"), this, [this]
393 applyPriorities(BitTorrent::DownloadPriority::Normal
);
395 menu
->addAction(tr("High priority"), this, [this]
397 applyPriorities(BitTorrent::DownloadPriority::High
);
399 menu
->addAction(tr("Maximum priority"), this, [this]
401 applyPriorities(BitTorrent::DownloadPriority::Maximum
);
403 menu
->addSeparator();
404 menu
->addAction(tr("Priority by shown file order"), this, &TorrentContentWidget::applyPrioritiesByOrder
);
407 // The selected torrent might have disappeared during exec()
408 // so we just close menu when an appropriate model is reset
409 connect(model(), &QAbstractItemModel::modelAboutToBeReset
, menu
, [menu
]()
411 menu
->setActiveAction(nullptr);
415 menu
->popup(QCursor::pos());
418 void TorrentContentWidget::openItem(const QModelIndex
&index
) const
420 if (!index
.isValid())
423 m_model
->contentHandler()->flushCache(); // Flush data
424 Utils::Gui::openPath(getFullPath(index
));
427 void TorrentContentWidget::openParentFolder(const QModelIndex
&index
) const
429 const Path path
= getFullPath(index
);
430 m_model
->contentHandler()->flushCache(); // Flush data
432 MacUtils::openFiles({path
});
434 Utils::Gui::openFolderSelect(path
);
438 Path
TorrentContentWidget::getFullPath(const QModelIndex
&index
) const
440 const auto *contentHandler
= m_model
->contentHandler();
441 if (const int fileIdx
= getFileIndex(index
); fileIdx
>= 0)
443 const Path fullPath
= contentHandler
->actualStorageLocation() / contentHandler
->actualFilePath(fileIdx
);
448 const Path fullPath
= contentHandler
->actualStorageLocation() / getItemPath(index
);
452 void TorrentContentWidget::onItemDoubleClicked(const QModelIndex
&index
)
454 const auto *contentHandler
= m_model
->contentHandler();
455 Q_ASSERT(contentHandler
&& contentHandler
->hasMetadata());
457 if (!contentHandler
|| !contentHandler
->hasMetadata()) [[unlikely
]]
460 if (m_doubleClickAction
== DoubleClickAction::Rename
)
461 renameSelectedFile();
466 void TorrentContentWidget::expandRecursively()
468 QModelIndex currentIndex
;
469 while (model()->rowCount(currentIndex
) == 1)
471 currentIndex
= model()->index(0, 0, currentIndex
);
472 setExpanded(currentIndex
, true);
476 void TorrentContentWidget::wheelEvent(QWheelEvent
*event
)
478 if (event
->modifiers() & Qt::ShiftModifier
)
480 // Shift + scroll = horizontal scroll
482 QWheelEvent scrollHEvent
{event
->position(), event
->globalPosition()
483 , event
->pixelDelta(), event
->angleDelta().transposed(), event
->buttons()
484 , event
->modifiers(), event
->phase(), event
->inverted(), event
->source()};
485 QTreeView::wheelEvent(&scrollHEvent
);
489 QTreeView::wheelEvent(event
); // event delegated to base class