418 lines
12 KiB
C++
418 lines
12 KiB
C++
// This file Copyright © 2009-2023 Mnemosyne LLC.
|
|
// It may be used under GPLv2 (SPDX: GPL-2.0-only), GPLv3 (SPDX: GPL-3.0-only),
|
|
// or any future license endorsed by Mnemosyne LLC.
|
|
// License text can be found in the licenses/ folder.
|
|
|
|
#include <algorithm>
|
|
#include <cassert>
|
|
#include <queue>
|
|
|
|
#include <QHeaderView>
|
|
#include <QMenu>
|
|
#include <QResizeEvent>
|
|
#include <QSortFilterProxyModel>
|
|
|
|
#include <libtransmission/transmission.h> // priorities
|
|
|
|
#include "FileTreeDelegate.h"
|
|
#include "FileTreeItem.h"
|
|
#include "FileTreeModel.h"
|
|
#include "FileTreeView.h"
|
|
#include "Formatter.h"
|
|
#include "Utils.h"
|
|
|
|
namespace
|
|
{
|
|
|
|
char const* const PriorityKey = "priority";
|
|
|
|
}
|
|
|
|
FileTreeView::FileTreeView(QWidget* parent, bool is_editable)
|
|
: QTreeView{ parent }
|
|
, model_{ new FileTreeModel(this, is_editable) }
|
|
, proxy_{ new QSortFilterProxyModel(this) }
|
|
, delegate_{ new FileTreeDelegate(this) }
|
|
{
|
|
proxy_->setSourceModel(model_);
|
|
proxy_->setSortRole(FileTreeModel::SortRole);
|
|
proxy_->setSortCaseSensitivity(Qt::CaseInsensitive);
|
|
|
|
setModel(proxy_);
|
|
setItemDelegate(delegate_);
|
|
sortByColumn(FileTreeModel::COL_NAME, Qt::AscendingOrder);
|
|
|
|
connect(this, &QAbstractItemView::clicked, this, &FileTreeView::onClicked);
|
|
|
|
connect(model_, &FileTreeModel::openRequested, this, &FileTreeView::openRequested);
|
|
connect(model_, &FileTreeModel::pathEdited, this, &FileTreeView::pathEdited);
|
|
connect(model_, &FileTreeModel::priorityChanged, this, &FileTreeView::priorityChanged);
|
|
connect(model_, &FileTreeModel::wantedChanged, this, &FileTreeView::wantedChanged);
|
|
}
|
|
|
|
void FileTreeView::onClicked(QModelIndex const& proxy_index)
|
|
{
|
|
QModelIndex const model_index = proxy_->mapToSource(proxy_index);
|
|
|
|
if (model_index.column() == FileTreeModel::COL_WANTED)
|
|
{
|
|
model_->twiddleWanted(QModelIndexList() << model_index);
|
|
}
|
|
else if (model_index.column() == FileTreeModel::COL_PRIORITY)
|
|
{
|
|
model_->twiddlePriority(QModelIndexList() << model_index);
|
|
}
|
|
}
|
|
|
|
void FileTreeView::resizeEvent(QResizeEvent* event)
|
|
{
|
|
QTreeView::resizeEvent(event);
|
|
|
|
// this is kind of a hack to get the last four columns be the
|
|
// right size, and to have the filename column use whatever
|
|
// space is left over...
|
|
|
|
int left = event->size().width() - 1;
|
|
|
|
for (int column = 0; column < FileTreeModel::NUM_COLUMNS; ++column)
|
|
{
|
|
if (column == FileTreeModel::COL_NAME)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
int min_width = 0;
|
|
|
|
QStringList item_texts;
|
|
|
|
switch (column)
|
|
{
|
|
case FileTreeModel::COL_SIZE:
|
|
for (int s = Formatter::get().B; s <= Formatter::get().TB; ++s)
|
|
{
|
|
item_texts
|
|
<< (QStringLiteral("999.9 ") + Formatter::get().unitStr(Formatter::MEM, static_cast<Formatter::Size>(s)));
|
|
}
|
|
|
|
break;
|
|
|
|
case FileTreeModel::COL_PROGRESS:
|
|
item_texts << QStringLiteral(" 100% ");
|
|
break;
|
|
|
|
case FileTreeModel::COL_WANTED:
|
|
min_width = 20;
|
|
break;
|
|
|
|
case FileTreeModel::COL_PRIORITY:
|
|
item_texts << FileTreeItem::tr("Low") << FileTreeItem::tr("Normal") << FileTreeItem::tr("High")
|
|
<< FileTreeItem::tr("Mixed");
|
|
break;
|
|
}
|
|
|
|
int item_width = 0;
|
|
|
|
for (QString const& item_text : item_texts)
|
|
{
|
|
item_width = std::max(item_width, Utils::measureViewItem(this, item_text));
|
|
}
|
|
|
|
QString const header_text = model_->headerData(column, Qt::Horizontal).toString();
|
|
int const header_width = Utils::measureHeaderItem(this->header(), header_text);
|
|
|
|
int const width = std::max(min_width, std::max(item_width, header_width));
|
|
setColumnWidth(column, width);
|
|
|
|
left -= width;
|
|
}
|
|
|
|
setColumnWidth(FileTreeModel::COL_NAME, std::max(left, 0));
|
|
}
|
|
|
|
void FileTreeView::keyPressEvent(QKeyEvent* event)
|
|
{
|
|
if ((state() != EditingState) && (event->key() == Qt::Key_Space))
|
|
{
|
|
// handle using the keyboard to toggle the
|
|
// wanted/unwanted state or the file priority
|
|
|
|
Qt::KeyboardModifiers const modifiers = event->modifiers();
|
|
|
|
if (modifiers == Qt::NoModifier)
|
|
{
|
|
model_->twiddleWanted(selectedSourceRows());
|
|
return;
|
|
}
|
|
|
|
if (modifiers == Qt::ShiftModifier)
|
|
{
|
|
model_->twiddlePriority(selectedSourceRows());
|
|
return;
|
|
}
|
|
}
|
|
|
|
QTreeView::keyPressEvent(event);
|
|
}
|
|
|
|
void FileTreeView::mouseDoubleClickEvent(QMouseEvent* event)
|
|
{
|
|
if (auto const index = currentIndex();
|
|
!index.isValid() || index.column() == FileTreeModel::COL_WANTED || index.column() == FileTreeModel::COL_PRIORITY)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (openSelectedItem())
|
|
{
|
|
return;
|
|
}
|
|
|
|
QTreeView::mouseDoubleClickEvent(event);
|
|
}
|
|
|
|
void FileTreeView::contextMenuEvent(QContextMenuEvent* event)
|
|
{
|
|
if (auto const root_index = model_->index(0, 0); !root_index.isValid())
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (context_menu_ == nullptr)
|
|
{
|
|
initContextMenu();
|
|
}
|
|
|
|
context_menu_->popup(event->globalPos());
|
|
}
|
|
|
|
void FileTreeView::update(FileList const& files, bool update_fields)
|
|
{
|
|
bool const model_was_empty = proxy_->rowCount() == 0;
|
|
|
|
for (TorrentFile const& file : files)
|
|
{
|
|
model_->addFile(file.index, file.filename, file.wanted, file.priority, file.size, file.have, update_fields);
|
|
}
|
|
|
|
if (model_was_empty)
|
|
{
|
|
expand(proxy_->index(0, 0));
|
|
}
|
|
|
|
proxy_->sort(header()->sortIndicatorSection(), header()->sortIndicatorOrder());
|
|
}
|
|
|
|
void FileTreeView::clear()
|
|
{
|
|
model_->clear();
|
|
}
|
|
|
|
void FileTreeView::setEditable(bool editable)
|
|
{
|
|
model_->setEditable(editable);
|
|
}
|
|
|
|
bool FileTreeView::edit(QModelIndex const& index, EditTrigger trigger, QEvent* event)
|
|
{
|
|
if (selectionModel()->selectedRows().size() != 1)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
QModelIndex const name_index = index.sibling(index.row(), FileTreeModel::COL_NAME);
|
|
|
|
if (editTriggers().testFlag(trigger))
|
|
{
|
|
selectionModel()->setCurrentIndex(name_index, QItemSelectionModel::NoUpdate);
|
|
}
|
|
|
|
return QTreeView::edit(name_index, trigger, event);
|
|
}
|
|
|
|
void FileTreeView::checkSelectedItems()
|
|
{
|
|
model_->setWanted(selectedSourceRows(), true);
|
|
}
|
|
|
|
void FileTreeView::uncheckSelectedItems()
|
|
{
|
|
model_->setWanted(selectedSourceRows(), false);
|
|
}
|
|
|
|
void FileTreeView::onlyCheckSelectedItems()
|
|
{
|
|
QModelIndex const root_index = model_->index(0, 0);
|
|
|
|
if (!root_index.isValid())
|
|
{
|
|
return;
|
|
}
|
|
|
|
QModelIndexList wanted_indices = selectedSourceRows();
|
|
model_->setWanted(wanted_indices, true);
|
|
|
|
std::sort(wanted_indices.begin(), wanted_indices.end());
|
|
|
|
QSet<QModelIndex> wanted_indices_parents;
|
|
|
|
for (QModelIndex const& i : wanted_indices)
|
|
{
|
|
for (QModelIndex p = i.parent(); p.isValid(); p = p.parent())
|
|
{
|
|
wanted_indices_parents.insert(p);
|
|
}
|
|
}
|
|
|
|
auto parents_queue = std::queue<QModelIndex>{};
|
|
parents_queue.emplace(root_index);
|
|
QModelIndexList unwanted_indices;
|
|
|
|
while (!std::empty(parents_queue))
|
|
{
|
|
auto const parent_index = parents_queue.front();
|
|
parents_queue.pop();
|
|
|
|
if (std::binary_search(wanted_indices.begin(), wanted_indices.end(), parent_index))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
auto const* parent_model = parent_index.model();
|
|
|
|
for (int i = 0, count = model_->rowCount(parent_index); i < count; ++i)
|
|
{
|
|
QModelIndex const child_index = parent_model->index(i, 0, parent_index);
|
|
int const child_check_state = child_index.data(FileTreeModel::WantedRole).toInt();
|
|
|
|
if (child_check_state == Qt::Unchecked ||
|
|
std::binary_search(wanted_indices.begin(), wanted_indices.end(), child_index))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (child_check_state == Qt::Checked && child_index.data(FileTreeModel::FileIndexRole).toInt() >= 0)
|
|
{
|
|
unwanted_indices << child_index;
|
|
}
|
|
else if (!wanted_indices_parents.contains(child_index))
|
|
{
|
|
unwanted_indices << child_index;
|
|
}
|
|
else
|
|
{
|
|
parents_queue.emplace(child_index);
|
|
}
|
|
}
|
|
}
|
|
|
|
model_->setWanted(unwanted_indices, false);
|
|
}
|
|
|
|
void FileTreeView::setSelectedItemsPriority()
|
|
{
|
|
auto const* action = qobject_cast<QAction const*>(sender());
|
|
assert(action != nullptr);
|
|
model_->setPriority(selectedSourceRows(), action->property(PriorityKey).toInt());
|
|
}
|
|
|
|
bool FileTreeView::openSelectedItem()
|
|
{
|
|
return model_->openFile(proxy_->mapToSource(currentIndex()));
|
|
}
|
|
|
|
void FileTreeView::renameSelectedItem()
|
|
{
|
|
QTreeView::edit(currentIndex());
|
|
}
|
|
|
|
void FileTreeView::refreshContextMenuActionsSensitivity()
|
|
{
|
|
assert(context_menu_ != nullptr);
|
|
|
|
QModelIndexList const selected_rows = selectionModel()->selectedRows();
|
|
Qt::CheckState const check_state = getCumulativeCheckState(selected_rows);
|
|
|
|
bool const have_selection = !selected_rows.isEmpty();
|
|
bool const have_single_selection = selected_rows.size() == 1;
|
|
bool const have_unchecked = check_state == Qt::Unchecked || check_state == Qt::PartiallyChecked;
|
|
bool const have_checked = check_state == Qt::Checked || check_state == Qt::PartiallyChecked;
|
|
|
|
check_selected_action_->setEnabled(have_unchecked);
|
|
uncheck_selected_action_->setEnabled(have_checked);
|
|
only_check_selected_action_->setEnabled(have_selection);
|
|
priority_menu_->setEnabled(have_selection);
|
|
open_action_->setEnabled(
|
|
have_single_selection && selected_rows.first().data(FileTreeModel::FileIndexRole).toInt() >= 0 &&
|
|
selected_rows.first().data(FileTreeModel::CompleteRole).toBool());
|
|
rename_action_->setEnabled(have_single_selection);
|
|
}
|
|
|
|
void FileTreeView::initContextMenu()
|
|
{
|
|
context_menu_ = new QMenu{ this };
|
|
|
|
check_selected_action_ = context_menu_->addAction(tr("Check Selected"), this, SLOT(checkSelectedItems()));
|
|
uncheck_selected_action_ = context_menu_->addAction(tr("Uncheck Selected"), this, SLOT(uncheckSelectedItems()));
|
|
only_check_selected_action_ = context_menu_->addAction(tr("Only Check Selected"), this, SLOT(onlyCheckSelectedItems()));
|
|
|
|
context_menu_->addSeparator();
|
|
|
|
priority_menu_ = context_menu_->addMenu(tr("Priority"));
|
|
high_priority_action_ = priority_menu_->addAction(FileTreeItem::tr("High"), this, SLOT(setSelectedItemsPriority()));
|
|
normal_priority_action_ = priority_menu_->addAction(FileTreeItem::tr("Normal"), this, SLOT(setSelectedItemsPriority()));
|
|
low_priority_action_ = priority_menu_->addAction(FileTreeItem::tr("Low"), this, SLOT(setSelectedItemsPriority()));
|
|
|
|
high_priority_action_->setProperty(PriorityKey, TR_PRI_HIGH);
|
|
normal_priority_action_->setProperty(PriorityKey, TR_PRI_NORMAL);
|
|
low_priority_action_->setProperty(PriorityKey, TR_PRI_LOW);
|
|
|
|
context_menu_->addSeparator();
|
|
|
|
open_action_ = context_menu_->addAction(tr("Open"), this, SLOT(openSelectedItem()));
|
|
rename_action_ = context_menu_->addAction(tr("Rename…"), this, SLOT(renameSelectedItem()));
|
|
|
|
connect(context_menu_, &QMenu::aboutToShow, this, &FileTreeView::refreshContextMenuActionsSensitivity);
|
|
}
|
|
|
|
QModelIndexList FileTreeView::selectedSourceRows(int column) const
|
|
{
|
|
QModelIndexList indices;
|
|
|
|
for (QModelIndex const& i : selectionModel()->selectedRows(column))
|
|
{
|
|
indices << proxy_->mapToSource(i);
|
|
}
|
|
|
|
return indices;
|
|
}
|
|
|
|
Qt::CheckState FileTreeView::getCumulativeCheckState(QModelIndexList const& indices)
|
|
{
|
|
bool have_checked = false;
|
|
bool have_unchecked = false;
|
|
|
|
for (QModelIndex const& i : indices)
|
|
{
|
|
switch (i.data(FileTreeModel::WantedRole).toInt())
|
|
{
|
|
case Qt::Checked:
|
|
have_checked = true;
|
|
break;
|
|
|
|
case Qt::Unchecked:
|
|
have_unchecked = true;
|
|
break;
|
|
|
|
case Qt::PartiallyChecked:
|
|
return Qt::PartiallyChecked;
|
|
}
|
|
|
|
if (have_checked && have_unchecked)
|
|
{
|
|
return Qt::PartiallyChecked;
|
|
}
|
|
}
|
|
|
|
return have_checked ? Qt::Checked : Qt::Unchecked;
|
|
}
|