627 lines
14 KiB
C++
627 lines
14 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 <libtransmission/transmission.h> // priorities
|
|
|
|
#include "FileTreeItem.h"
|
|
#include "FileTreeModel.h"
|
|
|
|
namespace
|
|
{
|
|
|
|
class PathIteratorBase
|
|
{
|
|
protected:
|
|
PathIteratorBase(QString const& path, int slashIndex) :
|
|
myPath(path),
|
|
mySlashIndex(slashIndex),
|
|
myToken()
|
|
{
|
|
myToken.reserve(path.size() / 2);
|
|
}
|
|
|
|
QString const& myPath;
|
|
int mySlashIndex;
|
|
QString myToken;
|
|
|
|
static QChar const SlashChar;
|
|
};
|
|
|
|
QChar const PathIteratorBase::SlashChar = QLatin1Char('/');
|
|
|
|
class ForwardPathIterator : public PathIteratorBase
|
|
{
|
|
public:
|
|
ForwardPathIterator(QString const& path) :
|
|
PathIteratorBase(path, path.size() - 1)
|
|
{
|
|
}
|
|
|
|
bool hasNext() const
|
|
{
|
|
return mySlashIndex > -1;
|
|
}
|
|
|
|
QString const& next()
|
|
{
|
|
int newSlashIndex = myPath.lastIndexOf(SlashChar, mySlashIndex);
|
|
myToken.truncate(0);
|
|
myToken += myPath.midRef(newSlashIndex + 1, mySlashIndex - newSlashIndex);
|
|
mySlashIndex = newSlashIndex - 1;
|
|
return myToken;
|
|
}
|
|
};
|
|
|
|
class BackwardPathIterator : public PathIteratorBase
|
|
{
|
|
public:
|
|
BackwardPathIterator(QString const& path) :
|
|
PathIteratorBase(path, 0)
|
|
{
|
|
}
|
|
|
|
bool hasNext() const
|
|
{
|
|
return mySlashIndex < myPath.size();
|
|
}
|
|
|
|
QString const& next()
|
|
{
|
|
int newSlashIndex = myPath.indexOf(SlashChar, mySlashIndex);
|
|
|
|
if (newSlashIndex == -1)
|
|
{
|
|
newSlashIndex = myPath.size();
|
|
}
|
|
|
|
myToken.truncate(0);
|
|
myToken += myPath.midRef(mySlashIndex, newSlashIndex - mySlashIndex);
|
|
mySlashIndex = newSlashIndex + 1;
|
|
return myToken;
|
|
}
|
|
};
|
|
|
|
} // namespace
|
|
|
|
FileTreeModel::FileTreeModel(QObject* parent, bool isEditable) :
|
|
QAbstractItemModel(parent),
|
|
myIsEditable(isEditable),
|
|
myRootItem(new FileTreeItem)
|
|
{
|
|
}
|
|
|
|
FileTreeModel::~FileTreeModel()
|
|
{
|
|
clear();
|
|
|
|
delete myRootItem;
|
|
}
|
|
|
|
void FileTreeModel::setEditable(bool editable)
|
|
{
|
|
myIsEditable = editable;
|
|
}
|
|
|
|
FileTreeItem* FileTreeModel::itemFromIndex(QModelIndex const& index) const
|
|
{
|
|
if (!index.isValid())
|
|
{
|
|
return nullptr;
|
|
}
|
|
|
|
assert(index.model() == this);
|
|
return static_cast<FileTreeItem*>(index.internalPointer());
|
|
}
|
|
|
|
QModelIndexList FileTreeModel::getOrphanIndices(QModelIndexList const& indices) const
|
|
{
|
|
QModelIndexList orphanIndices = indices;
|
|
|
|
std::sort(orphanIndices.begin(), orphanIndices.end());
|
|
|
|
for (QMutableListIterator<QModelIndex> it(orphanIndices); it.hasNext();)
|
|
{
|
|
QModelIndex walk = it.next();
|
|
|
|
for (;;)
|
|
{
|
|
walk = parent(walk, walk.column());
|
|
|
|
if (!walk.isValid())
|
|
{
|
|
break;
|
|
}
|
|
|
|
if (std::binary_search(orphanIndices.begin(), orphanIndices.end(), walk))
|
|
{
|
|
it.remove();
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
return orphanIndices;
|
|
}
|
|
|
|
QVariant FileTreeModel::data(QModelIndex const& index, int role) const
|
|
{
|
|
QVariant value;
|
|
|
|
if (index.isValid())
|
|
{
|
|
value = itemFromIndex(index)->data(index.column(), role);
|
|
}
|
|
|
|
return value;
|
|
}
|
|
|
|
Qt::ItemFlags FileTreeModel::flags(QModelIndex const& index) const
|
|
{
|
|
int i(Qt::ItemIsSelectable | Qt::ItemIsEnabled);
|
|
|
|
if (myIsEditable && index.column() == COL_NAME)
|
|
{
|
|
i |= Qt::ItemIsEditable;
|
|
}
|
|
|
|
if (index.column() == COL_WANTED)
|
|
{
|
|
i |= Qt::ItemIsUserCheckable | Qt::ItemIsTristate;
|
|
}
|
|
|
|
return Qt::ItemFlags(i);
|
|
}
|
|
|
|
bool FileTreeModel::setData(QModelIndex const& index, QVariant const& newname, int role)
|
|
{
|
|
if (role == Qt::EditRole)
|
|
{
|
|
FileTreeItem* item = itemFromIndex(index);
|
|
|
|
emit pathEdited(item->path(), newname.toString());
|
|
}
|
|
|
|
return false; // don't update the view until the session confirms the change
|
|
}
|
|
|
|
QVariant FileTreeModel::headerData(int column, Qt::Orientation orientation, int role) const
|
|
{
|
|
QVariant data;
|
|
|
|
if (orientation == Qt::Horizontal && role == Qt::DisplayRole)
|
|
{
|
|
switch (column)
|
|
{
|
|
case COL_NAME:
|
|
data.setValue(tr("File"));
|
|
break;
|
|
|
|
case COL_SIZE:
|
|
data.setValue(tr("Size"));
|
|
break;
|
|
|
|
case COL_PROGRESS:
|
|
data.setValue(tr("Progress"));
|
|
break;
|
|
|
|
case COL_WANTED:
|
|
data.setValue(tr("Download"));
|
|
break;
|
|
|
|
case COL_PRIORITY:
|
|
data.setValue(tr("Priority"));
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
return data;
|
|
}
|
|
|
|
QModelIndex FileTreeModel::index(int row, int column, QModelIndex const& parent) const
|
|
{
|
|
QModelIndex i;
|
|
|
|
if (hasIndex(row, column, parent))
|
|
{
|
|
FileTreeItem* parentItem;
|
|
|
|
if (!parent.isValid())
|
|
{
|
|
parentItem = myRootItem;
|
|
}
|
|
else
|
|
{
|
|
parentItem = itemFromIndex(parent);
|
|
}
|
|
|
|
FileTreeItem* childItem = parentItem->child(row);
|
|
|
|
if (childItem != nullptr)
|
|
{
|
|
i = createIndex(row, column, childItem);
|
|
}
|
|
}
|
|
|
|
return i;
|
|
}
|
|
|
|
QModelIndex FileTreeModel::parent(QModelIndex const& child) const
|
|
{
|
|
return parent(child, 0); // QAbstractItemModel::parent() wants col 0
|
|
}
|
|
|
|
QModelIndex FileTreeModel::parent(QModelIndex const& child, int column) const
|
|
{
|
|
QModelIndex parent;
|
|
|
|
if (child.isValid())
|
|
{
|
|
parent = indexOf(itemFromIndex(child)->parent(), column);
|
|
}
|
|
|
|
return parent;
|
|
}
|
|
|
|
int FileTreeModel::rowCount(QModelIndex const& parent) const
|
|
{
|
|
FileTreeItem* parentItem;
|
|
|
|
if (parent.isValid())
|
|
{
|
|
parentItem = itemFromIndex(parent);
|
|
}
|
|
else
|
|
{
|
|
parentItem = myRootItem;
|
|
}
|
|
|
|
return parentItem->childCount();
|
|
}
|
|
|
|
int FileTreeModel::columnCount(QModelIndex const& parent) const
|
|
{
|
|
Q_UNUSED(parent)
|
|
|
|
return NUM_COLUMNS;
|
|
}
|
|
|
|
QModelIndex FileTreeModel::indexOf(FileTreeItem* item, int column) const
|
|
{
|
|
if (item == nullptr || item == myRootItem)
|
|
{
|
|
return QModelIndex();
|
|
}
|
|
|
|
return createIndex(item->row(), column, item);
|
|
}
|
|
|
|
void FileTreeModel::clearSubtree(QModelIndex const& top)
|
|
{
|
|
size_t i = rowCount(top);
|
|
|
|
while (i > 0)
|
|
{
|
|
clearSubtree(index(--i, 0, top));
|
|
}
|
|
|
|
FileTreeItem* const item = itemFromIndex(top);
|
|
|
|
if (item == nullptr)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (item->fileIndex() != -1)
|
|
{
|
|
myIndexCache.remove(item->fileIndex());
|
|
}
|
|
|
|
delete item;
|
|
}
|
|
|
|
void FileTreeModel::clear()
|
|
{
|
|
beginResetModel();
|
|
clearSubtree(QModelIndex());
|
|
endResetModel();
|
|
|
|
assert(myIndexCache.isEmpty());
|
|
}
|
|
|
|
FileTreeItem* FileTreeModel::findItemForFileIndex(int fileIndex) const
|
|
{
|
|
return myIndexCache.value(fileIndex, nullptr);
|
|
}
|
|
|
|
void FileTreeModel::addFile(int fileIndex, QString const& filename, bool wanted, int priority, uint64_t totalSize,
|
|
uint64_t have, bool updateFields)
|
|
{
|
|
FileTreeItem* item;
|
|
|
|
item = findItemForFileIndex(fileIndex);
|
|
|
|
if (item != nullptr) // this file is already in the tree, we've added this
|
|
{
|
|
QModelIndex indexWithChangedParents;
|
|
ForwardPathIterator filenameIt(filename);
|
|
|
|
while (filenameIt.hasNext())
|
|
{
|
|
QString const& token = filenameIt.next();
|
|
std::pair<int, int> const changed = item->update(token, wanted, priority, have, updateFields);
|
|
|
|
if (changed.first >= 0)
|
|
{
|
|
emit dataChanged(indexOf(item, changed.first), indexOf(item, changed.second));
|
|
|
|
if (!indexWithChangedParents.isValid() && changed.first <= COL_PRIORITY && changed.second >= COL_SIZE)
|
|
{
|
|
indexWithChangedParents = indexOf(item, 0);
|
|
}
|
|
}
|
|
|
|
item = item->parent();
|
|
}
|
|
|
|
assert(item == myRootItem);
|
|
|
|
if (indexWithChangedParents.isValid())
|
|
{
|
|
emitParentsChanged(indexWithChangedParents, COL_SIZE, COL_PRIORITY);
|
|
}
|
|
}
|
|
else // we haven't build the FileTreeItems for these tokens yet
|
|
{
|
|
bool added = false;
|
|
|
|
item = myRootItem;
|
|
BackwardPathIterator filenameIt(filename);
|
|
|
|
while (filenameIt.hasNext())
|
|
{
|
|
QString const& token = filenameIt.next();
|
|
FileTreeItem* child(item->child(token));
|
|
|
|
if (child == nullptr)
|
|
{
|
|
added = true;
|
|
QModelIndex parentIndex(indexOf(item, 0));
|
|
int const n(item->childCount());
|
|
|
|
beginInsertRows(parentIndex, n, n);
|
|
|
|
if (!filenameIt.hasNext())
|
|
{
|
|
child = new FileTreeItem(token, fileIndex, totalSize);
|
|
}
|
|
else
|
|
{
|
|
child = new FileTreeItem(token);
|
|
}
|
|
|
|
item->appendChild(child);
|
|
endInsertRows();
|
|
}
|
|
|
|
item = child;
|
|
}
|
|
|
|
if (item != myRootItem)
|
|
{
|
|
assert(item->fileIndex() == fileIndex);
|
|
assert(item->totalSize() == totalSize);
|
|
|
|
myIndexCache[fileIndex] = item;
|
|
|
|
std::pair<int, int> const changed = item->update(item->name(), wanted, priority, have, added || updateFields);
|
|
|
|
if (changed.first >= 0)
|
|
{
|
|
emit dataChanged(indexOf(item, changed.first), indexOf(item, changed.second));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void FileTreeModel::emitParentsChanged(QModelIndex const& index, int firstColumn, int lastColumn,
|
|
QSet<QModelIndex>* visitedParentIndices)
|
|
{
|
|
assert(firstColumn <= lastColumn);
|
|
|
|
QModelIndex walk = index;
|
|
|
|
for (;;)
|
|
{
|
|
walk = parent(walk, firstColumn);
|
|
|
|
if (!walk.isValid())
|
|
{
|
|
break;
|
|
}
|
|
|
|
if (visitedParentIndices != nullptr)
|
|
{
|
|
if (visitedParentIndices->contains(walk))
|
|
{
|
|
break;
|
|
}
|
|
|
|
visitedParentIndices->insert(walk);
|
|
}
|
|
|
|
emit dataChanged(walk, walk.sibling(walk.row(), lastColumn));
|
|
}
|
|
}
|
|
|
|
void FileTreeModel::emitSubtreeChanged(QModelIndex const& index, int firstColumn, int lastColumn)
|
|
{
|
|
assert(firstColumn <= lastColumn);
|
|
|
|
int const childCount = rowCount(index);
|
|
|
|
if (childCount == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// tell everyone that this item changed
|
|
emit dataChanged(index.child(0, firstColumn), index.child(childCount - 1, lastColumn));
|
|
|
|
// walk the subitems
|
|
for (int i = 0; i < childCount; ++i)
|
|
{
|
|
emitSubtreeChanged(index.child(i, 0), firstColumn, lastColumn);
|
|
}
|
|
}
|
|
|
|
void FileTreeModel::twiddleWanted(QModelIndexList const& indices)
|
|
{
|
|
QMap<bool, QModelIndexList> wantedIndices;
|
|
|
|
for (QModelIndex const& i : getOrphanIndices(indices))
|
|
{
|
|
FileTreeItem const* const item = itemFromIndex(i);
|
|
wantedIndices[item->isSubtreeWanted() != Qt::Checked] << i;
|
|
}
|
|
|
|
for (int i = 0; i <= 1; ++i)
|
|
{
|
|
if (wantedIndices.contains(i))
|
|
{
|
|
setWanted(wantedIndices[i], i != 0);
|
|
}
|
|
}
|
|
}
|
|
|
|
void FileTreeModel::twiddlePriority(QModelIndexList const& indices)
|
|
{
|
|
QMap<int, QModelIndexList> priorityIndices;
|
|
|
|
for (QModelIndex const& i : getOrphanIndices(indices))
|
|
{
|
|
FileTreeItem const* const item = itemFromIndex(i);
|
|
int priority = item->priority();
|
|
|
|
// ... -> normal -> high -> low -> normal -> ...; mixed -> normal
|
|
if (priority == FileTreeItem::NORMAL)
|
|
{
|
|
priority = TR_PRI_HIGH;
|
|
}
|
|
else if (priority == FileTreeItem::HIGH)
|
|
{
|
|
priority = TR_PRI_LOW;
|
|
}
|
|
else
|
|
{
|
|
priority = TR_PRI_NORMAL;
|
|
}
|
|
|
|
priorityIndices[priority] << i;
|
|
}
|
|
|
|
for (int i = TR_PRI_LOW; i <= TR_PRI_HIGH; ++i)
|
|
{
|
|
if (priorityIndices.contains(i))
|
|
{
|
|
setPriority(priorityIndices[i], i);
|
|
}
|
|
}
|
|
}
|
|
|
|
void FileTreeModel::setWanted(QModelIndexList const& indices, bool wanted)
|
|
{
|
|
if (indices.isEmpty())
|
|
{
|
|
return;
|
|
}
|
|
|
|
QModelIndexList const orphanIndices = getOrphanIndices(indices);
|
|
|
|
QSet<int> fileIds;
|
|
|
|
for (QModelIndex const& i : orphanIndices)
|
|
{
|
|
FileTreeItem* const item = itemFromIndex(i);
|
|
item->setSubtreeWanted(wanted, fileIds);
|
|
|
|
emit dataChanged(i, i);
|
|
emitSubtreeChanged(i, COL_WANTED, COL_WANTED);
|
|
}
|
|
|
|
// emit parent changes separately to avoid multiple updates for same items
|
|
QSet<QModelIndex> parentIndices;
|
|
|
|
for (QModelIndex const& i : orphanIndices)
|
|
{
|
|
emitParentsChanged(i, COL_SIZE, COL_WANTED, &parentIndices);
|
|
}
|
|
|
|
if (!fileIds.isEmpty())
|
|
{
|
|
emit wantedChanged(fileIds, wanted);
|
|
}
|
|
}
|
|
|
|
void FileTreeModel::setPriority(QModelIndexList const& indices, int priority)
|
|
{
|
|
if (indices.isEmpty())
|
|
{
|
|
return;
|
|
}
|
|
|
|
QModelIndexList const orphanIndices = getOrphanIndices(indices);
|
|
|
|
QSet<int> fileIds;
|
|
|
|
for (QModelIndex const& i : orphanIndices)
|
|
{
|
|
FileTreeItem* const item = itemFromIndex(i);
|
|
item->setSubtreePriority(priority, fileIds);
|
|
|
|
emit dataChanged(i, i);
|
|
emitSubtreeChanged(i, COL_PRIORITY, COL_PRIORITY);
|
|
}
|
|
|
|
// emit parent changes separately to avoid multiple updates for same items
|
|
QSet<QModelIndex> parentIndices;
|
|
|
|
for (QModelIndex const& i : orphanIndices)
|
|
{
|
|
emitParentsChanged(i, COL_PRIORITY, COL_PRIORITY, &parentIndices);
|
|
}
|
|
|
|
if (!fileIds.isEmpty())
|
|
{
|
|
emit priorityChanged(fileIds, priority);
|
|
}
|
|
}
|
|
|
|
bool FileTreeModel::openFile(QModelIndex const& index)
|
|
{
|
|
if (!index.isValid())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
FileTreeItem* const item = itemFromIndex(index);
|
|
|
|
if (item->fileIndex() < 0 || !item->isComplete())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
emit openRequested(item->path());
|
|
return true;
|
|
}
|