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
);
93 connect(model(), &QAbstractItemModel::modelReset
, this, &TorrentContentWidget::expandRecursively
);
96 void TorrentContentWidget::setContentHandler(BitTorrent::TorrentContentHandler
*contentHandler
)
98 m_model
->setContentHandler(contentHandler
);
105 BitTorrent::TorrentContentHandler
*TorrentContentWidget::contentHandler() const
107 return m_model
->contentHandler();
110 void TorrentContentWidget::refresh()
112 setUpdatesEnabled(false);
114 setUpdatesEnabled(true);
117 bool TorrentContentWidget::openByEnterKey() const
119 return m_openFileHotkeyEnter
;
122 void TorrentContentWidget::setOpenByEnterKey(const bool value
)
124 if (value
== openByEnterKey())
129 m_openFileHotkeyReturn
= new QShortcut(Qt::Key_Return
, this, nullptr, nullptr, Qt::WidgetShortcut
);
130 connect(m_openFileHotkeyReturn
, &QShortcut::activated
, this, &TorrentContentWidget::openSelectedFile
);
131 m_openFileHotkeyEnter
= new QShortcut(Qt::Key_Enter
, this, nullptr, nullptr, Qt::WidgetShortcut
);
132 connect(m_openFileHotkeyEnter
, &QShortcut::activated
, this, &TorrentContentWidget::openSelectedFile
);
136 delete m_openFileHotkeyEnter
;
137 m_openFileHotkeyEnter
= nullptr;
138 delete m_openFileHotkeyReturn
;
139 m_openFileHotkeyReturn
= nullptr;
143 TorrentContentWidget::DoubleClickAction
TorrentContentWidget::doubleClickAction() const
145 return m_doubleClickAction
;
148 void TorrentContentWidget::setDoubleClickAction(DoubleClickAction action
)
150 m_doubleClickAction
= action
;
153 TorrentContentWidget::ColumnsVisibilityMode
TorrentContentWidget::columnsVisibilityMode() const
155 return m_columnsVisibilityMode
;
158 void TorrentContentWidget::setColumnsVisibilityMode(ColumnsVisibilityMode mode
)
160 m_columnsVisibilityMode
= mode
;
163 int TorrentContentWidget::getFileIndex(const QModelIndex
&index
) const
165 return m_filterModel
->getFileIndex(index
);
168 Path
TorrentContentWidget::getItemPath(const QModelIndex
&index
) const
171 for (QModelIndex i
= index
; i
.isValid(); i
= i
.parent())
172 path
= Path(i
.data().toString()) / path
;
176 void TorrentContentWidget::setFilterPattern(const QString
&patternText
)
178 const QString pattern
= Utils::String::wildcardToRegexPattern(patternText
);
179 m_filterModel
->setFilterRegularExpression(QRegularExpression(pattern
, QRegularExpression::CaseInsensitiveOption
));
180 if (patternText
.isEmpty())
183 expand(m_filterModel
->index(0, 0));
191 void TorrentContentWidget::checkAll()
193 for (int i
= 0; i
< model()->rowCount(); ++i
)
194 model()->setData(model()->index(i
, TorrentContentModelItem::COL_NAME
), Qt::Checked
, Qt::CheckStateRole
);
197 void TorrentContentWidget::checkNone()
199 for (int i
= 0; i
< model()->rowCount(); ++i
)
200 model()->setData(model()->index(i
, TorrentContentModelItem::COL_NAME
), Qt::Unchecked
, Qt::CheckStateRole
);
203 void TorrentContentWidget::keyPressEvent(QKeyEvent
*event
)
205 if ((event
->key() != Qt::Key_Space
) && (event
->key() != Qt::Key_Select
))
207 QTreeView::keyPressEvent(event
);
213 const QVariant value
= currentNameCell().data(Qt::CheckStateRole
);
214 if (!value
.isValid())
220 const Qt::CheckState state
= (static_cast<Qt::CheckState
>(value
.toInt()) == Qt::Checked
)
221 ? Qt::Unchecked
: Qt::Checked
;
222 const QModelIndexList selection
= selectionModel()->selectedRows(TorrentContentModelItem::COL_NAME
);
224 for (const QModelIndex
&index
: selection
)
225 model()->setData(index
, state
, Qt::CheckStateRole
);
228 void TorrentContentWidget::renameSelectedFile()
230 const QModelIndexList selectedIndexes
= selectionModel()->selectedRows(0);
231 if (selectedIndexes
.size() != 1)
234 const QPersistentModelIndex modelIndex
= selectedIndexes
.first();
235 if (!modelIndex
.isValid())
239 const bool isFile
= (m_filterModel
->itemType(modelIndex
) == TorrentContentModelItem::FileType
);
241 QString newName
= AutoExpandableDialog::getText(this, tr("Renaming"), tr("New name:"), QLineEdit::Normal
242 , modelIndex
.data().toString(), &ok
, isFile
).trimmed();
243 if (!ok
|| !modelIndex
.isValid())
246 model()->setData(modelIndex
, newName
);
249 void TorrentContentWidget::applyPriorities(const BitTorrent::DownloadPriority priority
)
251 const QModelIndexList selectedRows
= selectionModel()->selectedRows(0);
252 for (const QModelIndex
&index
: selectedRows
)
254 model()->setData(index
.sibling(index
.row(), Priority
), static_cast<int>(priority
));
258 void TorrentContentWidget::applyPrioritiesByOrder()
260 // Equally distribute the selected items into groups and for each group assign
261 // a download priority that will apply to each item. The number of groups depends on how
262 // many "download priority" are available to be assigned
264 const QModelIndexList selectedRows
= selectionModel()->selectedRows(0);
266 const qsizetype priorityGroups
= 3;
267 const auto priorityGroupSize
= std::max
<qsizetype
>((selectedRows
.length() / priorityGroups
), 1);
269 for (qsizetype i
= 0; i
< selectedRows
.length(); ++i
)
271 auto priority
= BitTorrent::DownloadPriority::Ignored
;
272 switch (i
/ priorityGroupSize
)
275 priority
= BitTorrent::DownloadPriority::Maximum
;
278 priority
= BitTorrent::DownloadPriority::High
;
282 priority
= BitTorrent::DownloadPriority::Normal
;
286 const QModelIndex
&index
= selectedRows
[i
];
287 model()->setData(index
.sibling(index
.row(), Priority
), static_cast<int>(priority
));
291 void TorrentContentWidget::openSelectedFile()
293 const QModelIndexList selectedIndexes
= selectionModel()->selectedRows(0);
294 if (selectedIndexes
.size() != 1)
296 openItem(selectedIndexes
.first());
299 void TorrentContentWidget::setModel([[maybe_unused
]] QAbstractItemModel
*model
)
301 Q_ASSERT_X(false, Q_FUNC_INFO
, "Changing the model of TorrentContentWidget is not allowed.");
304 QModelIndex
TorrentContentWidget::currentNameCell() const
306 const QModelIndex current
= currentIndex();
307 if (!current
.isValid())
313 return current
.siblingAtColumn(TorrentContentModelItem::COL_NAME
);
316 void TorrentContentWidget::displayColumnHeaderMenu()
318 QMenu
*menu
= new QMenu(this);
319 menu
->setAttribute(Qt::WA_DeleteOnClose
);
320 menu
->setToolTipsVisible(true);
322 if (m_columnsVisibilityMode
== ColumnsVisibilityMode::Editable
)
324 menu
->setTitle(tr("Column visibility"));
325 for (int i
= 0; i
< TorrentContentModelItem::NB_COL
; ++i
)
327 const auto columnName
= model()->headerData(i
, Qt::Horizontal
, Qt::DisplayRole
).toString();
328 QAction
*action
= menu
->addAction(columnName
, this, [this, i
](bool checked
)
330 setColumnHidden(i
, !checked
);
332 if (checked
&& (columnWidth(i
) <= 5))
333 resizeColumnToContents(i
);
337 action
->setCheckable(true);
338 action
->setChecked(!isColumnHidden(i
));
340 if (i
== TorrentContentModelItem::COL_NAME
)
341 action
->setEnabled(false);
344 menu
->addSeparator();
347 QAction
*resizeAction
= menu
->addAction(tr("Resize columns"), this, [this]()
349 for (int i
= 0, count
= header()->count(); i
< count
; ++i
)
351 if (!isColumnHidden(i
))
352 resizeColumnToContents(i
);
357 resizeAction
->setToolTip(tr("Resize all non-hidden columns to the size of their contents"));
359 menu
->popup(QCursor::pos());
362 void TorrentContentWidget::displayContextMenu()
364 const QModelIndexList selectedRows
= selectionModel()->selectedRows(0);
365 if (selectedRows
.empty())
368 QMenu
*menu
= new QMenu(this);
369 menu
->setAttribute(Qt::WA_DeleteOnClose
);
371 if (selectedRows
.size() == 1)
373 const QModelIndex index
= selectedRows
[0];
375 if (!contentHandler()->actualStorageLocation().isEmpty())
377 menu
->addAction(UIThemeManager::instance()->getIcon(u
"folder-documents"_s
), tr("Open")
378 , this, [this, index
]() { openItem(index
); });
379 menu
->addAction(UIThemeManager::instance()->getIcon(u
"directory"_s
), tr("Open containing folder")
380 , this, [this, index
]() { openParentFolder(index
); });
382 menu
->addAction(UIThemeManager::instance()->getIcon(u
"edit-rename"_s
), tr("Rename...")
383 , this, &TorrentContentWidget::renameSelectedFile
);
384 menu
->addSeparator();
386 QMenu
*subMenu
= menu
->addMenu(tr("Priority"));
388 subMenu
->addAction(tr("Do not download"), this, [this]
390 applyPriorities(BitTorrent::DownloadPriority::Ignored
);
392 subMenu
->addAction(tr("Normal"), this, [this]
394 applyPriorities(BitTorrent::DownloadPriority::Normal
);
396 subMenu
->addAction(tr("High"), this, [this]
398 applyPriorities(BitTorrent::DownloadPriority::High
);
400 subMenu
->addAction(tr("Maximum"), this, [this]
402 applyPriorities(BitTorrent::DownloadPriority::Maximum
);
404 subMenu
->addSeparator();
405 subMenu
->addAction(tr("By shown file order"), this, &TorrentContentWidget::applyPrioritiesByOrder
);
409 menu
->addAction(tr("Do not download"), this, [this]
411 applyPriorities(BitTorrent::DownloadPriority::Ignored
);
413 menu
->addAction(tr("Normal priority"), this, [this]
415 applyPriorities(BitTorrent::DownloadPriority::Normal
);
417 menu
->addAction(tr("High priority"), this, [this]
419 applyPriorities(BitTorrent::DownloadPriority::High
);
421 menu
->addAction(tr("Maximum priority"), this, [this]
423 applyPriorities(BitTorrent::DownloadPriority::Maximum
);
425 menu
->addSeparator();
426 menu
->addAction(tr("Priority by shown file order"), this, &TorrentContentWidget::applyPrioritiesByOrder
);
429 // The selected torrent might have disappeared during exec()
430 // so we just close menu when an appropriate model is reset
431 connect(model(), &QAbstractItemModel::modelAboutToBeReset
, menu
, [menu
]()
433 menu
->setActiveAction(nullptr);
437 menu
->popup(QCursor::pos());
440 void TorrentContentWidget::openItem(const QModelIndex
&index
) const
442 if (!index
.isValid())
445 m_model
->contentHandler()->flushCache(); // Flush data
446 Utils::Gui::openPath(getFullPath(index
));
449 void TorrentContentWidget::openParentFolder(const QModelIndex
&index
) const
451 const Path path
= getFullPath(index
);
452 m_model
->contentHandler()->flushCache(); // Flush data
454 MacUtils::openFiles({path
});
456 Utils::Gui::openFolderSelect(path
);
460 Path
TorrentContentWidget::getFullPath(const QModelIndex
&index
) const
462 const auto *contentHandler
= m_model
->contentHandler();
463 if (const int fileIdx
= getFileIndex(index
); fileIdx
>= 0)
465 const Path fullPath
= contentHandler
->actualStorageLocation() / contentHandler
->actualFilePath(fileIdx
);
470 const Path fullPath
= contentHandler
->actualStorageLocation() / getItemPath(index
);
474 void TorrentContentWidget::onItemDoubleClicked(const QModelIndex
&index
)
476 const auto *contentHandler
= m_model
->contentHandler();
477 Q_ASSERT(contentHandler
&& contentHandler
->hasMetadata());
479 if (!contentHandler
|| !contentHandler
->hasMetadata()) [[unlikely
]]
482 if (m_doubleClickAction
== DoubleClickAction::Rename
)
483 renameSelectedFile();
488 void TorrentContentWidget::expandRecursively()
490 QModelIndex currentIndex
;
491 while (model()->rowCount(currentIndex
) == 1)
493 currentIndex
= model()->index(0, 0, currentIndex
);
494 setExpanded(currentIndex
, true);
498 void TorrentContentWidget::wheelEvent(QWheelEvent
*event
)
500 if (event
->modifiers() & Qt::ShiftModifier
)
502 // Shift + scroll = horizontal scroll
504 QWheelEvent scrollHEvent
{event
->position(), event
->globalPosition()
505 , event
->pixelDelta(), event
->angleDelta().transposed(), event
->buttons()
506 , event
->modifiers(), event
->phase(), event
->inverted(), event
->source()};
507 QTreeView::wheelEvent(&scrollHEvent
);
511 QTreeView::wheelEvent(event
); // event delegated to base class