1 /*
2  * This file Copyright (C) 2009-2015 Mnemosyne LLC
3  *
4  * It may be used under the GNU GPL versions 2 or 3
5  * or any future license endorsed by Mnemosyne LLC.
6  *
7  */
8 
9 #include <cassert>
10 
11 #include <libtransmission/transmission.h> // priorities
12 
13 #include "FileTreeItem.h"
14 #include "FileTreeModel.h"
15 
16 namespace
17 {
18 
19 class PathIteratorBase
20 {
21 protected:
PathIteratorBase(QString const & path,int slashIndex)22     PathIteratorBase(QString const& path, int slashIndex) :
23         myPath(path),
24         mySlashIndex(slashIndex),
25         myToken()
26     {
27         myToken.reserve(path.size() / 2);
28     }
29 
30 protected:
31     QString const& myPath;
32     int mySlashIndex;
33     QString myToken;
34 
35     static QChar const SlashChar;
36 };
37 
38 QChar const PathIteratorBase::SlashChar = QLatin1Char('/');
39 
40 class ForwardPathIterator : public PathIteratorBase
41 {
42 public:
ForwardPathIterator(QString const & path)43     ForwardPathIterator(QString const& path) :
44         PathIteratorBase(path, path.size() - 1)
45     {
46     }
47 
hasNext() const48     bool hasNext() const
49     {
50         return mySlashIndex > -1;
51     }
52 
next()53     QString const& next()
54     {
55         int newSlashIndex = myPath.lastIndexOf(SlashChar, mySlashIndex);
56         myToken.truncate(0);
57         myToken += myPath.midRef(newSlashIndex + 1, mySlashIndex - newSlashIndex);
58         mySlashIndex = newSlashIndex - 1;
59         return myToken;
60     }
61 };
62 
63 class BackwardPathIterator : public PathIteratorBase
64 {
65 public:
BackwardPathIterator(QString const & path)66     BackwardPathIterator(QString const& path) :
67         PathIteratorBase(path, 0)
68     {
69     }
70 
hasNext() const71     bool hasNext() const
72     {
73         return mySlashIndex < myPath.size();
74     }
75 
next()76     QString const& next()
77     {
78         int newSlashIndex = myPath.indexOf(SlashChar, mySlashIndex);
79 
80         if (newSlashIndex == -1)
81         {
82             newSlashIndex = myPath.size();
83         }
84 
85         myToken.truncate(0);
86         myToken += myPath.midRef(mySlashIndex, newSlashIndex - mySlashIndex);
87         mySlashIndex = newSlashIndex + 1;
88         return myToken;
89     }
90 };
91 
92 } // namespace
93 
FileTreeModel(QObject * parent,bool isEditable)94 FileTreeModel::FileTreeModel(QObject* parent, bool isEditable) :
95     QAbstractItemModel(parent),
96     myIsEditable(isEditable),
97     myRootItem(new FileTreeItem),
98     myIndexCache()
99 {
100 }
101 
~FileTreeModel()102 FileTreeModel::~FileTreeModel()
103 {
104     clear();
105 
106     delete myRootItem;
107 }
108 
setEditable(bool editable)109 void FileTreeModel::setEditable(bool editable)
110 {
111     myIsEditable = editable;
112 }
113 
itemFromIndex(QModelIndex const & index) const114 FileTreeItem* FileTreeModel::itemFromIndex(QModelIndex const& index) const
115 {
116     if (!index.isValid())
117     {
118         return nullptr;
119     }
120 
121     assert(index.model() == this);
122     return static_cast<FileTreeItem*>(index.internalPointer());
123 }
124 
getOrphanIndices(QModelIndexList const & indices) const125 QModelIndexList FileTreeModel::getOrphanIndices(QModelIndexList const& indices) const
126 {
127     QModelIndexList orphanIndices = indices;
128 
129     qSort(orphanIndices);
130 
131     for (QMutableListIterator<QModelIndex> it(orphanIndices); it.hasNext();)
132     {
133         QModelIndex walk = it.next();
134 
135         for (;;)
136         {
137             walk = parent(walk, walk.column());
138 
139             if (!walk.isValid())
140             {
141                 break;
142             }
143 
144             if (qBinaryFind(orphanIndices, walk) != orphanIndices.end())
145             {
146                 it.remove();
147                 break;
148             }
149         }
150     }
151 
152     return orphanIndices;
153 }
154 
data(QModelIndex const & index,int role) const155 QVariant FileTreeModel::data(QModelIndex const& index, int role) const
156 {
157     QVariant value;
158 
159     if (index.isValid())
160     {
161         value = itemFromIndex(index)->data(index.column(), role);
162     }
163 
164     return value;
165 }
166 
flags(QModelIndex const & index) const167 Qt::ItemFlags FileTreeModel::flags(QModelIndex const& index) const
168 {
169     int i(Qt::ItemIsSelectable | Qt::ItemIsEnabled);
170 
171     if (myIsEditable && index.column() == COL_NAME)
172     {
173         i |= Qt::ItemIsEditable;
174     }
175 
176     if (index.column() == COL_WANTED)
177     {
178         i |= Qt::ItemIsUserCheckable | Qt::ItemIsTristate;
179     }
180 
181     return Qt::ItemFlags(i);
182 }
183 
setData(QModelIndex const & index,QVariant const & newname,int role)184 bool FileTreeModel::setData(QModelIndex const& index, QVariant const& newname, int role)
185 {
186     if (role == Qt::EditRole)
187     {
188         FileTreeItem* item = itemFromIndex(index);
189 
190         emit pathEdited(item->path(), newname.toString());
191     }
192 
193     return false; // don't update the view until the session confirms the change
194 }
195 
headerData(int column,Qt::Orientation orientation,int role) const196 QVariant FileTreeModel::headerData(int column, Qt::Orientation orientation, int role) const
197 {
198     QVariant data;
199 
200     if (orientation == Qt::Horizontal && role == Qt::DisplayRole)
201     {
202         switch (column)
203         {
204         case COL_NAME:
205             data.setValue(tr("File"));
206             break;
207 
208         case COL_SIZE:
209             data.setValue(tr("Size"));
210             break;
211 
212         case COL_PROGRESS:
213             data.setValue(tr("Progress"));
214             break;
215 
216         case COL_WANTED:
217             data.setValue(tr("Download"));
218             break;
219 
220         case COL_PRIORITY:
221             data.setValue(tr("Priority"));
222             break;
223 
224         default:
225             break;
226         }
227     }
228 
229     return data;
230 }
231 
index(int row,int column,QModelIndex const & parent) const232 QModelIndex FileTreeModel::index(int row, int column, QModelIndex const& parent) const
233 {
234     QModelIndex i;
235 
236     if (hasIndex(row, column, parent))
237     {
238         FileTreeItem* parentItem;
239 
240         if (!parent.isValid())
241         {
242             parentItem = myRootItem;
243         }
244         else
245         {
246             parentItem = itemFromIndex(parent);
247         }
248 
249         FileTreeItem* childItem = parentItem->child(row);
250 
251         if (childItem != nullptr)
252         {
253             i = createIndex(row, column, childItem);
254         }
255     }
256 
257     return i;
258 }
259 
parent(QModelIndex const & child) const260 QModelIndex FileTreeModel::parent(QModelIndex const& child) const
261 {
262     return parent(child, 0); // QAbstractItemModel::parent() wants col 0
263 }
264 
parent(QModelIndex const & child,int column) const265 QModelIndex FileTreeModel::parent(QModelIndex const& child, int column) const
266 {
267     QModelIndex parent;
268 
269     if (child.isValid())
270     {
271         parent = indexOf(itemFromIndex(child)->parent(), column);
272     }
273 
274     return parent;
275 }
276 
rowCount(QModelIndex const & parent) const277 int FileTreeModel::rowCount(QModelIndex const& parent) const
278 {
279     FileTreeItem* parentItem;
280 
281     if (parent.isValid())
282     {
283         parentItem = itemFromIndex(parent);
284     }
285     else
286     {
287         parentItem = myRootItem;
288     }
289 
290     return parentItem->childCount();
291 }
292 
columnCount(QModelIndex const & parent) const293 int FileTreeModel::columnCount(QModelIndex const& parent) const
294 {
295     Q_UNUSED(parent)
296 
297     return NUM_COLUMNS;
298 }
299 
indexOf(FileTreeItem * item,int column) const300 QModelIndex FileTreeModel::indexOf(FileTreeItem* item, int column) const
301 {
302     if (item == nullptr || item == myRootItem)
303     {
304         return QModelIndex();
305     }
306 
307     return createIndex(item->row(), column, item);
308 }
309 
clearSubtree(QModelIndex const & top)310 void FileTreeModel::clearSubtree(QModelIndex const& top)
311 {
312     size_t i = rowCount(top);
313 
314     while (i > 0)
315     {
316         clearSubtree(index(--i, 0, top));
317     }
318 
319     FileTreeItem* const item = itemFromIndex(top);
320 
321     if (item == nullptr)
322     {
323         return;
324     }
325 
326     if (item->fileIndex() != -1)
327     {
328         myIndexCache.remove(item->fileIndex());
329     }
330 
331     delete item;
332 }
333 
clear()334 void FileTreeModel::clear()
335 {
336     beginResetModel();
337     clearSubtree(QModelIndex());
338     endResetModel();
339 
340     assert(myIndexCache.isEmpty());
341 }
342 
findItemForFileIndex(int fileIndex) const343 FileTreeItem* FileTreeModel::findItemForFileIndex(int fileIndex) const
344 {
345     return myIndexCache.value(fileIndex, nullptr);
346 }
347 
addFile(int fileIndex,QString const & filename,bool wanted,int priority,uint64_t totalSize,uint64_t have,bool updateFields)348 void FileTreeModel::addFile(int fileIndex, QString const& filename, bool wanted, int priority, uint64_t totalSize,
349     uint64_t have, bool updateFields)
350 {
351     FileTreeItem* item;
352 
353     item = findItemForFileIndex(fileIndex);
354 
355     if (item != nullptr) // this file is already in the tree, we've added this
356     {
357         QModelIndex indexWithChangedParents;
358         ForwardPathIterator filenameIt(filename);
359 
360         while (filenameIt.hasNext())
361         {
362             QString const& token = filenameIt.next();
363             std::pair<int, int> const changed = item->update(token, wanted, priority, have, updateFields);
364 
365             if (changed.first >= 0)
366             {
367                 emit dataChanged(indexOf(item, changed.first), indexOf(item, changed.second));
368 
369                 if (!indexWithChangedParents.isValid() && changed.first <= COL_PRIORITY && changed.second >= COL_SIZE)
370                 {
371                     indexWithChangedParents = indexOf(item, 0);
372                 }
373             }
374 
375             item = item->parent();
376         }
377 
378         assert(item == myRootItem);
379 
380         if (indexWithChangedParents.isValid())
381         {
382             emitParentsChanged(indexWithChangedParents, COL_SIZE, COL_PRIORITY);
383         }
384     }
385     else // we haven't build the FileTreeItems for these tokens yet
386     {
387         bool added = false;
388 
389         item = myRootItem;
390         BackwardPathIterator filenameIt(filename);
391 
392         while (filenameIt.hasNext())
393         {
394             QString const& token = filenameIt.next();
395             FileTreeItem* child(item->child(token));
396 
397             if (child == nullptr)
398             {
399                 added = true;
400                 QModelIndex parentIndex(indexOf(item, 0));
401                 int const n(item->childCount());
402 
403                 beginInsertRows(parentIndex, n, n);
404 
405                 if (!filenameIt.hasNext())
406                 {
407                     child = new FileTreeItem(token, fileIndex, totalSize);
408                 }
409                 else
410                 {
411                     child = new FileTreeItem(token);
412                 }
413 
414                 item->appendChild(child);
415                 endInsertRows();
416             }
417 
418             item = child;
419         }
420 
421         if (item != myRootItem)
422         {
423             assert(item->fileIndex() == fileIndex);
424             assert(item->totalSize() == totalSize);
425 
426             myIndexCache[fileIndex] = item;
427 
428             std::pair<int, int> const changed = item->update(item->name(), wanted, priority, have, added || updateFields);
429 
430             if (changed.first >= 0)
431             {
432                 emit dataChanged(indexOf(item, changed.first), indexOf(item, changed.second));
433             }
434         }
435     }
436 }
437 
emitParentsChanged(QModelIndex const & index,int firstColumn,int lastColumn,QSet<QModelIndex> * visitedParentIndices)438 void FileTreeModel::emitParentsChanged(QModelIndex const& index, int firstColumn, int lastColumn,
439     QSet<QModelIndex>* visitedParentIndices)
440 {
441     assert(firstColumn <= lastColumn);
442 
443     QModelIndex walk = index;
444 
445     for (;;)
446     {
447         walk = parent(walk, firstColumn);
448 
449         if (!walk.isValid())
450         {
451             break;
452         }
453 
454         if (visitedParentIndices != nullptr)
455         {
456             if (visitedParentIndices->contains(walk))
457             {
458                 break;
459             }
460 
461             visitedParentIndices->insert(walk);
462         }
463 
464         emit dataChanged(walk, walk.sibling(walk.row(), lastColumn));
465     }
466 }
467 
emitSubtreeChanged(QModelIndex const & index,int firstColumn,int lastColumn)468 void FileTreeModel::emitSubtreeChanged(QModelIndex const& index, int firstColumn, int lastColumn)
469 {
470     assert(firstColumn <= lastColumn);
471 
472     int const childCount = rowCount(index);
473 
474     if (childCount == 0)
475     {
476         return;
477     }
478 
479     // tell everyone that this item changed
480     emit dataChanged(index.child(0, firstColumn), index.child(childCount - 1, lastColumn));
481 
482     // walk the subitems
483     for (int i = 0; i < childCount; ++i)
484     {
485         emitSubtreeChanged(index.child(i, 0), firstColumn, lastColumn);
486     }
487 }
488 
twiddleWanted(QModelIndexList const & indices)489 void FileTreeModel::twiddleWanted(QModelIndexList const& indices)
490 {
491     QMap<bool, QModelIndexList> wantedIndices;
492 
493     for (QModelIndex const& i : getOrphanIndices(indices))
494     {
495         FileTreeItem const* const item = itemFromIndex(i);
496         wantedIndices[item->isSubtreeWanted() != Qt::Checked] << i;
497     }
498 
499     for (int i = 0; i <= 1; ++i)
500     {
501         if (wantedIndices.contains(i))
502         {
503             setWanted(wantedIndices[i], i != 0);
504         }
505     }
506 }
507 
twiddlePriority(QModelIndexList const & indices)508 void FileTreeModel::twiddlePriority(QModelIndexList const& indices)
509 {
510     QMap<int, QModelIndexList> priorityIndices;
511 
512     for (QModelIndex const& i : getOrphanIndices(indices))
513     {
514         FileTreeItem const* const item = itemFromIndex(i);
515         int priority = item->priority();
516 
517         // ... -> normal -> high -> low -> normal -> ...; mixed -> normal
518         if (priority == FileTreeItem::NORMAL)
519         {
520             priority = TR_PRI_HIGH;
521         }
522         else if (priority == FileTreeItem::HIGH)
523         {
524             priority = TR_PRI_LOW;
525         }
526         else
527         {
528             priority = TR_PRI_NORMAL;
529         }
530 
531         priorityIndices[priority] << i;
532     }
533 
534     for (int i = TR_PRI_LOW; i <= TR_PRI_HIGH; ++i)
535     {
536         if (priorityIndices.contains(i))
537         {
538             setPriority(priorityIndices[i], i);
539         }
540     }
541 }
542 
setWanted(QModelIndexList const & indices,bool wanted)543 void FileTreeModel::setWanted(QModelIndexList const& indices, bool wanted)
544 {
545     if (indices.isEmpty())
546     {
547         return;
548     }
549 
550     QModelIndexList const orphanIndices = getOrphanIndices(indices);
551 
552     QSet<int> fileIds;
553 
554     for (QModelIndex const& i : orphanIndices)
555     {
556         FileTreeItem* const item = itemFromIndex(i);
557         item->setSubtreeWanted(wanted, fileIds);
558 
559         emit dataChanged(i, i);
560         emitSubtreeChanged(i, COL_WANTED, COL_WANTED);
561     }
562 
563     // emit parent changes separately to avoid multiple updates for same items
564     QSet<QModelIndex> parentIndices;
565 
566     for (QModelIndex const& i : orphanIndices)
567     {
568         emitParentsChanged(i, COL_SIZE, COL_WANTED, &parentIndices);
569     }
570 
571     if (!fileIds.isEmpty())
572     {
573         emit wantedChanged(fileIds, wanted);
574     }
575 }
576 
setPriority(QModelIndexList const & indices,int priority)577 void FileTreeModel::setPriority(QModelIndexList const& indices, int priority)
578 {
579     if (indices.isEmpty())
580     {
581         return;
582     }
583 
584     QModelIndexList const orphanIndices = getOrphanIndices(indices);
585 
586     QSet<int> fileIds;
587 
588     for (QModelIndex const& i : orphanIndices)
589     {
590         FileTreeItem* const item = itemFromIndex(i);
591         item->setSubtreePriority(priority, fileIds);
592 
593         emit dataChanged(i, i);
594         emitSubtreeChanged(i, COL_PRIORITY, COL_PRIORITY);
595     }
596 
597     // emit parent changes separately to avoid multiple updates for same items
598     QSet<QModelIndex> parentIndices;
599 
600     for (QModelIndex const& i : orphanIndices)
601     {
602         emitParentsChanged(i, COL_PRIORITY, COL_PRIORITY, &parentIndices);
603     }
604 
605     if (!fileIds.isEmpty())
606     {
607         emit priorityChanged(fileIds, priority);
608     }
609 }
610 
openFile(QModelIndex const & index)611 bool FileTreeModel::openFile(QModelIndex const& index)
612 {
613     if (!index.isValid())
614     {
615         return false;
616     }
617 
618     FileTreeItem* const item = itemFromIndex(index);
619 
620     if (item->fileIndex() < 0 || !item->isComplete())
621     {
622         return false;
623     }
624 
625     emit openRequested(item->path());
626     return true;
627 }
628