transmission/qt/FileTreeView.cc

447 lines
13 KiB
C++

/*
* This file Copyright (C) 2009-2015 Mnemosyne LLC
*
* It may be used under the GNU GPL versions 2 or 3
* or any future license endorsed by Mnemosyne LLC.
*
*/
#include <algorithm>
#include <cassert>
#include <QHeaderView>
#include <QMenu>
#include <QQueue>
#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"
#define PRIORITY_KEY "priority"
FileTreeView::FileTreeView(QWidget* parent, bool isEditable) :
QTreeView(parent),
myModel(new FileTreeModel(this, isEditable)),
myProxy(new QSortFilterProxyModel(this)),
myDelegate(new FileTreeDelegate(this))
{
myProxy->setSourceModel(myModel);
myProxy->setSortRole(FileTreeModel::SortRole);
myProxy->setSortCaseSensitivity(Qt::CaseInsensitive);
setModel(myProxy);
setItemDelegate(myDelegate);
sortByColumn(FileTreeModel::COL_NAME, Qt::AscendingOrder);
connect(this, SIGNAL(clicked(QModelIndex)), this, SLOT(onClicked(QModelIndex)));
connect(myModel, SIGNAL(priorityChanged(QSet<int>, int)), this, SIGNAL(priorityChanged(QSet<int>, int)));
connect(myModel, SIGNAL(wantedChanged(QSet<int>, bool)), this, SIGNAL(wantedChanged(QSet<int>, bool)));
connect(myModel, SIGNAL(pathEdited(QString, QString)), this, SIGNAL(pathEdited(QString, QString)));
connect(myModel, SIGNAL(openRequested(QString)), this, SIGNAL(openRequested(QString)));
}
void FileTreeView::onClicked(QModelIndex const& proxyIndex)
{
QModelIndex const modelIndex = myProxy->mapToSource(proxyIndex);
if (modelIndex.column() == FileTreeModel::COL_WANTED)
{
myModel->twiddleWanted(QModelIndexList() << modelIndex);
}
else if (modelIndex.column() == FileTreeModel::COL_PRIORITY)
{
myModel->twiddlePriority(QModelIndexList() << modelIndex);
}
}
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 minWidth = 0;
QStringList itemTexts;
switch (column)
{
case FileTreeModel::COL_SIZE:
for (int s = Formatter::B; s <= Formatter::TB; ++s)
{
itemTexts << QLatin1String("999.9 ") + Formatter::unitStr(Formatter::MEM, static_cast<Formatter::Size>(s));
}
break;
case FileTreeModel::COL_PROGRESS:
itemTexts << QLatin1String(" 100% ");
break;
case FileTreeModel::COL_WANTED:
minWidth = 20;
break;
case FileTreeModel::COL_PRIORITY:
itemTexts << FileTreeItem::tr("Low") << FileTreeItem::tr("Normal") << FileTreeItem::tr("High") <<
FileTreeItem::tr("Mixed");
break;
}
int itemWidth = 0;
for (QString const& itemText : itemTexts)
{
itemWidth = std::max(itemWidth, Utils::measureViewItem(this, itemText));
}
QString const headerText = myModel->headerData(column, Qt::Horizontal).toString();
int headerWidth = Utils::measureHeaderItem(this->header(), headerText);
int const width = std::max(minWidth, std::max(itemWidth, headerWidth));
setColumnWidth(column, width);
left -= width;
}
setColumnWidth(FileTreeModel::COL_NAME, std::max(left, 0));
}
void FileTreeView::keyPressEvent(QKeyEvent* event)
{
if (state() != EditingState)
{
if (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)
{
myModel->twiddleWanted(selectedSourceRows());
return;
}
if (modifiers == Qt::ShiftModifier)
{
myModel->twiddlePriority(selectedSourceRows());
return;
}
}
}
QTreeView::keyPressEvent(event);
}
void FileTreeView::mouseDoubleClickEvent(QMouseEvent* event)
{
QModelIndex const index = currentIndex();
if (!index.isValid() || index.column() == FileTreeModel::COL_WANTED || index.column() == FileTreeModel::COL_PRIORITY)
{
return;
}
if (openSelectedItem())
{
return;
}
QTreeView::mouseDoubleClickEvent(event);
}
void FileTreeView::contextMenuEvent(QContextMenuEvent* event)
{
QModelIndex const rootIndex = myModel->index(0, 0);
if (!rootIndex.isValid())
{
return;
}
if (myContextMenu == nullptr)
{
initContextMenu();
}
myContextMenu->popup(event->globalPos());
}
void FileTreeView::update(FileList const& files, bool updateFields)
{
bool const modelWasEmpty = myProxy->rowCount() == 0;
for (TorrentFile const& file : files)
{
myModel->addFile(file.index, file.filename, file.wanted, file.priority, file.size, file.have, updateFields);
}
if (modelWasEmpty)
{
// expand up until the item with more than one expandable child
for (QModelIndex index = myProxy->index(0, 0); index.isValid();)
{
QModelIndex const oldIndex = index;
expand(oldIndex);
index = QModelIndex();
for (int i = 0, count = myProxy->rowCount(oldIndex); i < count; ++i)
{
QModelIndex const newIndex = myProxy->index(i, 0, oldIndex);
if (myProxy->rowCount(newIndex) == 0)
{
continue;
}
if (index.isValid())
{
index = QModelIndex();
break;
}
index = newIndex;
}
}
}
myProxy->sort(header()->sortIndicatorSection(), header()->sortIndicatorOrder());
}
void FileTreeView::clear()
{
myModel->clear();
}
void FileTreeView::setEditable(bool editable)
{
myModel->setEditable(editable);
}
bool FileTreeView::edit(QModelIndex const& index, EditTrigger trigger, QEvent* event)
{
if (selectionModel()->selectedRows().size() != 1)
{
return false;
}
QModelIndex const nameIndex = index.sibling(index.row(), FileTreeModel::COL_NAME);
if (editTriggers().testFlag(trigger))
{
selectionModel()->setCurrentIndex(nameIndex, QItemSelectionModel::NoUpdate);
}
return QTreeView::edit(nameIndex, trigger, event);
}
void FileTreeView::checkSelectedItems()
{
myModel->setWanted(selectedSourceRows(), true);
}
void FileTreeView::uncheckSelectedItems()
{
myModel->setWanted(selectedSourceRows(), false);
}
void FileTreeView::onlyCheckSelectedItems()
{
QModelIndex const rootIndex = myModel->index(0, 0);
if (!rootIndex.isValid())
{
return;
}
QModelIndexList wantedIndices = selectedSourceRows();
myModel->setWanted(wantedIndices, true);
std::sort(wantedIndices.begin(), wantedIndices.end());
QSet<QModelIndex> wantedIndicesParents;
for (QModelIndex const& i : wantedIndices)
{
for (QModelIndex p = i.parent(); p.isValid(); p = p.parent())
{
wantedIndicesParents.insert(p);
}
}
QQueue<QModelIndex> parentsQueue;
parentsQueue.enqueue(rootIndex);
QModelIndexList unwantedIndices;
while (!parentsQueue.isEmpty())
{
QModelIndex const parentIndex = parentsQueue.dequeue();
if (std::binary_search(wantedIndices.begin(), wantedIndices.end(), parentIndex))
{
continue;
}
for (int i = 0, count = myModel->rowCount(parentIndex); i < count; ++i)
{
QModelIndex const childIndex = parentIndex.child(i, 0);
int const childCheckState = childIndex.data(FileTreeModel::WantedRole).toInt();
if (childCheckState == Qt::Unchecked || std::binary_search(wantedIndices.begin(), wantedIndices.end(), childIndex))
{
continue;
}
if (childCheckState == Qt::Checked && childIndex.data(FileTreeModel::FileIndexRole).toInt() >= 0)
{
unwantedIndices << childIndex;
}
else
{
if (!wantedIndicesParents.contains(childIndex))
{
unwantedIndices << childIndex;
}
else
{
parentsQueue.enqueue(childIndex);
}
}
}
}
myModel->setWanted(unwantedIndices, false);
}
void FileTreeView::setSelectedItemsPriority()
{
auto* action = qobject_cast<QAction*>(sender());
assert(action != nullptr);
myModel->setPriority(selectedSourceRows(), action->property(PRIORITY_KEY).toInt());
}
bool FileTreeView::openSelectedItem()
{
return myModel->openFile(myProxy->mapToSource(currentIndex()));
}
void FileTreeView::renameSelectedItem()
{
QTreeView::edit(currentIndex());
}
void FileTreeView::refreshContextMenuActionsSensitivity()
{
assert(myContextMenu != nullptr);
QModelIndexList const selectedRows = selectionModel()->selectedRows();
Qt::CheckState const checkState = getCumulativeCheckState(selectedRows);
bool const haveSelection = !selectedRows.isEmpty();
bool const haveSingleSelection = selectedRows.size() == 1;
bool const haveUnchecked = checkState == Qt::Unchecked || checkState == Qt::PartiallyChecked;
bool const haveChecked = checkState == Qt::Checked || checkState == Qt::PartiallyChecked;
myCheckSelectedAction->setEnabled(haveUnchecked);
myUncheckSelectedAction->setEnabled(haveChecked);
myOnlyCheckSelectedAction->setEnabled(haveSelection);
myPriorityMenu->setEnabled(haveSelection);
myOpenAction->setEnabled(haveSingleSelection && selectedRows.first().data(FileTreeModel::FileIndexRole).toInt() >= 0 &&
selectedRows.first().data(FileTreeModel::CompleteRole).toBool());
myRenameAction->setEnabled(haveSingleSelection);
}
void FileTreeView::initContextMenu()
{
myContextMenu = new QMenu(this);
myCheckSelectedAction = myContextMenu->addAction(tr("Check Selected"), this, SLOT(checkSelectedItems()));
myUncheckSelectedAction = myContextMenu->addAction(tr("Uncheck Selected"), this, SLOT(uncheckSelectedItems()));
myOnlyCheckSelectedAction = myContextMenu->addAction(tr("Only Check Selected"), this, SLOT(onlyCheckSelectedItems()));
myContextMenu->addSeparator();
myPriorityMenu = myContextMenu->addMenu(tr("Priority"));
myHighPriorityAction = myPriorityMenu->addAction(FileTreeItem::tr("High"), this, SLOT(setSelectedItemsPriority()));
myNormalPriorityAction = myPriorityMenu->addAction(FileTreeItem::tr("Normal"), this, SLOT(setSelectedItemsPriority()));
myLowPriorityAction = myPriorityMenu->addAction(FileTreeItem::tr("Low"), this, SLOT(setSelectedItemsPriority()));
myHighPriorityAction->setProperty(PRIORITY_KEY, TR_PRI_HIGH);
myNormalPriorityAction->setProperty(PRIORITY_KEY, TR_PRI_NORMAL);
myLowPriorityAction->setProperty(PRIORITY_KEY, TR_PRI_LOW);
myContextMenu->addSeparator();
myOpenAction = myContextMenu->addAction(tr("Open"), this, SLOT(openSelectedItem()));
myRenameAction = myContextMenu->addAction(tr("Rename..."), this, SLOT(renameSelectedItem()));
connect(myContextMenu, SIGNAL(aboutToShow()), SLOT(refreshContextMenuActionsSensitivity()));
}
QModelIndexList FileTreeView::selectedSourceRows(int column) const
{
QModelIndexList indices;
for (QModelIndex const& i : selectionModel()->selectedRows(column))
{
indices << myProxy->mapToSource(i);
}
return indices;
}
Qt::CheckState FileTreeView::getCumulativeCheckState(QModelIndexList const& indices)
{
bool haveChecked = false, haveUnchecked = false;
for (QModelIndex const& i : indices)
{
switch (i.data(FileTreeModel::WantedRole).toInt())
{
case Qt::Checked:
haveChecked = true;
break;
case Qt::Unchecked:
haveUnchecked = true;
break;
case Qt::PartiallyChecked:
return Qt::PartiallyChecked;
}
if (haveChecked && haveUnchecked)
{
return Qt::PartiallyChecked;
}
}
return haveChecked ? Qt::Checked : Qt::Unchecked;
}