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