mirror of
https://github.com/transmission/transmission.git
synced 2025-12-20 02:18:42 +00:00
There're places where manual intervention is still required as uncrustify is not ideal (unfortunately), but at least one may rely on it to do the right thing most of the time (e.g. when sending in a patch). The style itself is quite different from what we had before but making it uniform across all the codebase is the key. I also hope that it'll make the code more readable (YMMV) and less sensitive to further changes.
628 lines
14 KiB
C++
628 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 <cassert>
|
|
|
|
#include <libtransmission/transmission.h> // priorities
|
|
|
|
#include "FileTreeItem.h"
|
|
#include "FileTreeModel.h"
|
|
|
|
namespace
|
|
{
|
|
|
|
class PathIteratorBase
|
|
{
|
|
protected:
|
|
PathIteratorBase(const QString& path, int slashIndex) :
|
|
myPath(path),
|
|
mySlashIndex(slashIndex),
|
|
myToken()
|
|
{
|
|
myToken.reserve(path.size() / 2);
|
|
}
|
|
|
|
protected:
|
|
const QString& myPath;
|
|
int mySlashIndex;
|
|
QString myToken;
|
|
|
|
static const QChar SlashChar;
|
|
};
|
|
|
|
const QChar PathIteratorBase::SlashChar = QLatin1Char('/');
|
|
|
|
class ForwardPathIterator : public PathIteratorBase
|
|
{
|
|
public:
|
|
ForwardPathIterator(const QString& path) :
|
|
PathIteratorBase(path, path.size() - 1)
|
|
{
|
|
}
|
|
|
|
bool hasNext() const
|
|
{
|
|
return mySlashIndex > 0;
|
|
}
|
|
|
|
const QString& 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(const QString& path) :
|
|
PathIteratorBase(path, 0)
|
|
{
|
|
}
|
|
|
|
bool hasNext() const
|
|
{
|
|
return mySlashIndex < myPath.size();
|
|
}
|
|
|
|
const QString& 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),
|
|
myIndexCache()
|
|
{
|
|
}
|
|
|
|
FileTreeModel::~FileTreeModel()
|
|
{
|
|
clear();
|
|
|
|
delete myRootItem;
|
|
}
|
|
|
|
void FileTreeModel::setEditable(bool editable)
|
|
{
|
|
myIsEditable = editable;
|
|
}
|
|
|
|
FileTreeItem* FileTreeModel::itemFromIndex(const QModelIndex& index) const
|
|
{
|
|
if (!index.isValid())
|
|
{
|
|
return nullptr;
|
|
}
|
|
|
|
assert(index.model() == this);
|
|
return static_cast<FileTreeItem*>(index.internalPointer());
|
|
}
|
|
|
|
QModelIndexList FileTreeModel::getOrphanIndices(const QModelIndexList& indices) const
|
|
{
|
|
QModelIndexList orphanIndices = indices;
|
|
|
|
qSort(orphanIndices);
|
|
|
|
for (QMutableListIterator<QModelIndex> it(orphanIndices); it.hasNext();)
|
|
{
|
|
QModelIndex walk = it.next();
|
|
|
|
for (;;)
|
|
{
|
|
walk = parent(walk, walk.column());
|
|
|
|
if (!walk.isValid())
|
|
{
|
|
break;
|
|
}
|
|
|
|
if (qBinaryFind(orphanIndices, walk) != orphanIndices.end())
|
|
{
|
|
it.remove();
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
return orphanIndices;
|
|
}
|
|
|
|
QVariant FileTreeModel::data(const QModelIndex& index, int role) const
|
|
{
|
|
QVariant value;
|
|
|
|
if (index.isValid())
|
|
{
|
|
value = itemFromIndex(index)->data(index.column(), role);
|
|
}
|
|
|
|
return value;
|
|
}
|
|
|
|
Qt::ItemFlags FileTreeModel::flags(const QModelIndex& 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(const QModelIndex& index, const QVariant& 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, const QModelIndex& 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)
|
|
{
|
|
i = createIndex(row, column, childItem);
|
|
}
|
|
}
|
|
|
|
return i;
|
|
}
|
|
|
|
QModelIndex FileTreeModel::parent(const QModelIndex& child) const
|
|
{
|
|
return parent(child, 0); // QAbstractItemModel::parent() wants col 0
|
|
}
|
|
|
|
QModelIndex FileTreeModel::parent(const QModelIndex& child, int column) const
|
|
{
|
|
QModelIndex parent;
|
|
|
|
if (child.isValid())
|
|
{
|
|
parent = indexOf(itemFromIndex(child)->parent(), column);
|
|
}
|
|
|
|
return parent;
|
|
}
|
|
|
|
int FileTreeModel::rowCount(const QModelIndex& parent) const
|
|
{
|
|
FileTreeItem* parentItem;
|
|
|
|
if (parent.isValid())
|
|
{
|
|
parentItem = itemFromIndex(parent);
|
|
}
|
|
else
|
|
{
|
|
parentItem = myRootItem;
|
|
}
|
|
|
|
return parentItem->childCount();
|
|
}
|
|
|
|
int FileTreeModel::columnCount(const QModelIndex& parent) const
|
|
{
|
|
Q_UNUSED(parent);
|
|
|
|
return NUM_COLUMNS;
|
|
}
|
|
|
|
QModelIndex FileTreeModel::indexOf(FileTreeItem* item, int column) const
|
|
{
|
|
if (!item || item == myRootItem)
|
|
{
|
|
return QModelIndex();
|
|
}
|
|
|
|
return createIndex(item->row(), column, item);
|
|
}
|
|
|
|
void FileTreeModel::clearSubtree(const QModelIndex& top)
|
|
{
|
|
size_t i = rowCount(top);
|
|
|
|
while (i > 0)
|
|
{
|
|
clearSubtree(index(--i, 0, top));
|
|
}
|
|
|
|
FileTreeItem* const item = itemFromIndex(top);
|
|
|
|
if (item == 0)
|
|
{
|
|
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, 0);
|
|
}
|
|
|
|
void FileTreeModel::addFile(int fileIndex, const QString& filename, bool wanted, int priority, uint64_t totalSize,
|
|
uint64_t have, bool updateFields)
|
|
{
|
|
FileTreeItem* item;
|
|
|
|
item = findItemForFileIndex(fileIndex);
|
|
|
|
if (item) // this file is already in the tree, we've added this
|
|
{
|
|
QModelIndex indexWithChangedParents;
|
|
ForwardPathIterator filenameIt(filename);
|
|
|
|
while (filenameIt.hasNext())
|
|
{
|
|
const QString& token = filenameIt.next();
|
|
const std::pair<int, int> 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())
|
|
{
|
|
const QString& token = filenameIt.next();
|
|
FileTreeItem* child(item->child(token));
|
|
|
|
if (!child)
|
|
{
|
|
added = true;
|
|
QModelIndex parentIndex(indexOf(item, 0));
|
|
const int 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;
|
|
|
|
const std::pair<int, int> 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(const QModelIndex& 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(const QModelIndex& index, int firstColumn, int lastColumn)
|
|
{
|
|
assert(firstColumn <= lastColumn);
|
|
|
|
const int childCount = rowCount(index);
|
|
|
|
if (!childCount)
|
|
{
|
|
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(const QModelIndexList& indices)
|
|
{
|
|
QMap<bool, QModelIndexList> wantedIndices;
|
|
|
|
for (const QModelIndex& i : getOrphanIndices(indices))
|
|
{
|
|
const FileTreeItem* 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(const QModelIndexList& indices)
|
|
{
|
|
QMap<int, QModelIndexList> priorityIndices;
|
|
|
|
for (const QModelIndex& i : getOrphanIndices(indices))
|
|
{
|
|
const FileTreeItem* 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(const QModelIndexList& indices, bool wanted)
|
|
{
|
|
if (indices.isEmpty())
|
|
{
|
|
return;
|
|
}
|
|
|
|
const QModelIndexList orphanIndices = getOrphanIndices(indices);
|
|
|
|
QSet<int> fileIds;
|
|
|
|
for (const QModelIndex& 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 (const QModelIndex& i : orphanIndices)
|
|
{
|
|
emitParentsChanged(i, COL_SIZE, COL_WANTED, &parentIndices);
|
|
}
|
|
|
|
if (!fileIds.isEmpty())
|
|
{
|
|
emit wantedChanged(fileIds, wanted);
|
|
}
|
|
}
|
|
|
|
void FileTreeModel::setPriority(const QModelIndexList& indices, int priority)
|
|
{
|
|
if (indices.isEmpty())
|
|
{
|
|
return;
|
|
}
|
|
|
|
const QModelIndexList orphanIndices = getOrphanIndices(indices);
|
|
|
|
QSet<int> fileIds;
|
|
|
|
for (const QModelIndex& 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 (const QModelIndex& i : orphanIndices)
|
|
{
|
|
emitParentsChanged(i, COL_PRIORITY, COL_PRIORITY, &parentIndices);
|
|
}
|
|
|
|
if (!fileIds.isEmpty())
|
|
{
|
|
emit priorityChanged(fileIds, priority);
|
|
}
|
|
}
|
|
|
|
bool FileTreeModel::openFile(const QModelIndex& 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;
|
|
}
|