1 /*
2  * Copyright (C) 2012 - 2015  Hong Jen Yee (PCMan) <pcman.tw@gmail.com>
3  *
4  * This library is free software; you can redistribute it and/or
5  * modify it under the terms of the GNU Lesser General Public
6  * License as published by the Free Software Foundation; either
7  * version 2.1 of the License, or (at your option) any later version.
8  *
9  * This library is distributed in the hope that it will be useful,
10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
12  * Lesser General Public License for more details.
13  *
14  * You should have received a copy of the GNU Lesser General Public
15  * License along with this library; if not, write to the Free Software
16  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
17  *
18  */
19 
20 
21 #include "foldermodel.h"
22 #include <iostream>
23 #include <algorithm>
24 #include <QtAlgorithms>
25 #include <QVector>
26 #include <qmimedata.h>
27 #include <QMimeData>
28 #include <QByteArray>
29 #include <QPixmap>
30 #include <QPainter>
31 #include <QTimer>
32 #include <QDateTime>
33 #include <QString>
34 #include <QApplication>
35 #include <QClipboard>
36 #include "utilities.h"
37 #include "fileoperation.h"
38 
39 namespace Fm {
40 
FolderModel()41 FolderModel::FolderModel():
42     hasPendingThumbnailHandler_{false},
43     showFullNames_{false},
44     isLoaded_{false},
45     hasCutfile_{false} {
46     connect(QApplication::clipboard(), &QClipboard::dataChanged, this, &FolderModel::onClipboardDataChange);
47 }
48 
~FolderModel()49 FolderModel::~FolderModel() {
50     // if the thumbnail requests list is not empty, cancel them
51     for(auto job: pendingThumbnailJobs_) {
52         job->cancel();
53     }
54 }
55 
setFolder(const std::shared_ptr<Fm::Folder> & new_folder)56 void FolderModel::setFolder(const std::shared_ptr<Fm::Folder>& new_folder) {
57     if(folder_) {
58         removeAll();        // remove old items
59     }
60     if(new_folder) {
61         folder_ = new_folder;
62         connect(folder_.get(), &Fm::Folder::startLoading, this, &FolderModel::onStartLoading);
63         connect(folder_.get(), &Fm::Folder::finishLoading, this, &FolderModel::onFinishLoading);
64         connect(folder_.get(), &Fm::Folder::filesAdded, this, &FolderModel::onFilesAdded);
65         connect(folder_.get(), &Fm::Folder::filesChanged, this, &FolderModel::onFilesChanged);
66         connect(folder_.get(), &Fm::Folder::filesRemoved, this, &FolderModel::onFilesRemoved);
67         // handle the case if the folder is already loaded
68         if(folder_->isLoaded()) {
69             isLoaded_ = true;
70             insertFiles(0, folder_->files());
71             onClipboardDataChange(); // files may have been cut
72         }
73     }
74 }
75 
onStartLoading()76 void FolderModel::onStartLoading() {
77     isLoaded_ = false;
78     hasCutfile_ = false;
79     // remove all items
80     removeAll();
81 }
82 
onFinishLoading()83 void FolderModel::onFinishLoading() {
84     isLoaded_ = true;
85     // files may have been cut before (e.g., by another app)
86     onClipboardDataChange();
87 }
88 
onFilesAdded(const Fm::FileInfoList & files)89 void FolderModel::onFilesAdded(const Fm::FileInfoList& files) {
90     int n_files = files.size();
91     beginInsertRows(QModelIndex(), items.count(), items.count() + n_files - 1);
92     for(auto& info : files) {
93         FolderModelItem item(info);
94 
95         // cut files may be removed and added again
96         if(isLoaded_ && cutFilesHashSet_.count(info->path().hash()) != 0) {
97             item.isCut = true;
98             hasCutfile_ = true;
99         }
100 
101         items.append(item);
102     }
103     endInsertRows();
104 
105     if(isLoaded_) {
106         Q_EMIT filesAdded(files);
107     }
108 }
109 
onFilesChanged(std::vector<Fm::FileInfoPair> & files)110 void FolderModel::onFilesChanged(std::vector<Fm::FileInfoPair>& files) {
111     for(auto& change : files) {
112         int row;
113         auto& oldInfo = change.first;
114         auto& newInfo = change.second;
115         QList<FolderModelItem>::iterator it = findItemByFileInfo(oldInfo.get(), &row);
116         if(it != items.end()) {
117             FolderModelItem& item = *it;
118             // try to update the item
119             item.info = newInfo;
120             item.thumbnails.clear();
121             QModelIndex index = createIndex(row, 0, &item);
122             Q_EMIT dataChanged(index, index);
123             if(oldInfo->size() != newInfo->size()) {
124                 Q_EMIT fileSizeChanged(index);
125             }
126         }
127     }
128 }
129 
onFilesRemoved(const Fm::FileInfoList & files)130 void FolderModel::onFilesRemoved(const Fm::FileInfoList& files) {
131     for(auto& info : files) {
132         int row;
133         QList<FolderModelItem>::iterator it = findItemByName(info->name().c_str(), &row);
134         if(it != items.end()) {
135             beginRemoveRows(QModelIndex(), row, row);
136             items.erase(it);
137             endRemoveRows();
138         }
139     }
140 }
141 
loadPendingThumbnails()142 void FolderModel::loadPendingThumbnails() {
143     hasPendingThumbnailHandler_ = false;
144     for(auto& item: thumbnailData_) {
145         if(!item.pendingThumbnails_.empty()) {
146             auto job = new Fm::ThumbnailJob(std::move(item.pendingThumbnails_), item.size_);
147             pendingThumbnailJobs_.push_back(job);
148             job->setAutoDelete(true);
149             connect(job, &Fm::ThumbnailJob::thumbnailLoaded, this, &FolderModel::onThumbnailLoaded, Qt::BlockingQueuedConnection);
150             connect(job, &Fm::ThumbnailJob::finished, this, &FolderModel::onThumbnailJobFinished, Qt::BlockingQueuedConnection);
151             Fm::ThumbnailJob::threadPool()->start(job);
152         }
153     }
154 }
155 
queueLoadThumbnail(const std::shared_ptr<const Fm::FileInfo> & file,int size)156 void FolderModel::queueLoadThumbnail(const std::shared_ptr<const Fm::FileInfo>& file, int size) {
157     auto it = std::find_if(thumbnailData_.begin(), thumbnailData_.end(), [size](ThumbnailData& item){return item.size_ == size;});
158     if(it != thumbnailData_.end()) {
159         it->pendingThumbnails_.push_back(file);
160         if(!hasPendingThumbnailHandler_) {
161             QTimer::singleShot(0, this, &FolderModel::loadPendingThumbnails);
162             hasPendingThumbnailHandler_ = true;
163         }
164     }
165 }
166 
insertFiles(int row,const Fm::FileInfoList & files)167 void FolderModel::insertFiles(int row, const Fm::FileInfoList& files) {
168     int n_files = files.size();
169     beginInsertRows(QModelIndex(), row, row + n_files - 1);
170     for(auto& info : files) {
171         FolderModelItem item(info);
172         items.append(item);
173     }
174     endInsertRows();
175 }
176 
updateCutFilesSet()177 void FolderModel::updateCutFilesSet() {
178     // NOTE: To process large numbers of files as fast as possible,
179     // we analyze the clipboard data directly, without using "utilities.h",
180     // because otherwise, we should iterate through path lists multiple times.
181 
182     if(folder_ == nullptr) {
183         return;
184     }
185     cutFilesHashSet_.clear();
186 
187     bool cutPathFound = false;
188     const QClipboard* clipboard = QApplication::clipboard();
189     const QMimeData* data = clipboard->mimeData();
190 
191     // Gnome, LXDE, XFCE (see utilities.cpp -> pasteFilesFromClipboard)
192     if(data->hasFormat(QStringLiteral("x-special/gnome-copied-files"))) {
193         QByteArray gnomeData = data->data(QStringLiteral("x-special/gnome-copied-files"));
194         char* pdata = gnomeData.data();
195         char* eol = strchr(pdata, '\n');
196         if(eol) {
197             *eol = '\0';
198             if(strcmp(pdata, "cut") == 0) {
199                 char** uris = g_strsplit_set(eol + 1, "\r\n", -1);
200                 for(char** uri = uris; *uri; ++uri) {
201                     if(**uri != '\0') {
202                         cutPathFound = true; // although it may not be in this folder
203                         auto path = Fm::FilePath::fromUri(*uri);
204                         if(path.parent() == folder_->path()) {
205                             cutFilesHashSet_.insert(path.hash());
206                         }
207                     }
208                 }
209                 g_strfreev(uris);
210             }
211         }
212     }
213     // KDE
214     if(!cutPathFound && data->hasUrls()) {
215         QByteArray cut = data->data(QStringLiteral("application/x-kde-cutselection"));
216         if(!cut.isEmpty() && QChar::fromLatin1(cut.at(0)) == QLatin1Char('1')) {
217             QList<QUrl> urls = data->urls();
218             for(auto it = urls.cbegin(); it != urls.cend(); ++it) {
219                 auto path = Fm::FilePath::fromUri(it->toString().toUtf8().constData());
220                 if(path.parent() == folder_->path()) {
221                     cutFilesHashSet_.insert(path.hash());
222                 }
223             }
224         }
225     }
226 }
227 
onClipboardDataChange()228 void FolderModel::onClipboardDataChange() {
229     if(folder_ && isLoaded_) {
230         updateCutFilesSet();
231         if(!cutFilesHashSet_.empty()) {
232              // (some) files are cut here; so the items need to be updated
233             hasCutfile_ = false;
234             QList<FolderModelItem>::iterator it = items.begin();
235             while(it != items.end()) {
236                 FolderModelItem& item = *it;
237                 if(cutFilesHashSet_.count(it->info->path().hash()) != 0) {
238                     item.isCut = true;
239                     hasCutfile_ = true;
240                 }
241                 else {
242                     item.isCut = false;
243                 }
244                 ++it;
245             }
246             Q_EMIT dataChanged(index(0, 0), index(rowCount() - 1, 0));
247         }
248         else if(hasCutfile_) {
249             // this folder contained a cut file before but not anymore;
250             // so its items need to be updated
251             hasCutfile_ = false;
252             QList<FolderModelItem>::iterator it = items.begin();
253             while(it != items.end()) {
254                 FolderModelItem& item = *it;
255                 item.isCut = false;
256                 ++it;
257             }
258             Q_EMIT dataChanged(index(0, 0), index(rowCount() - 1, 0));
259         }
260     }
261 }
262 
removeAll()263 void FolderModel::removeAll() {
264     if(items.empty()) {
265         return;
266     }
267     beginRemoveRows(QModelIndex(), 0, items.size() - 1);
268     items.clear();
269     endRemoveRows();
270 }
271 
rowCount(const QModelIndex & parent) const272 int FolderModel::rowCount(const QModelIndex& parent) const {
273     if(parent.isValid()) {
274         return 0;
275     }
276     return items.size();
277 }
278 
columnCount(const QModelIndex & parent=QModelIndex ()) const279 int FolderModel::columnCount(const QModelIndex& parent = QModelIndex()) const {
280     if(parent.isValid()) {
281         return 0;
282     }
283     return NumOfColumns;
284 }
285 
itemFromIndex(const QModelIndex & index) const286 FolderModelItem* FolderModel::itemFromIndex(const QModelIndex& index) const {
287     return reinterpret_cast<FolderModelItem*>(index.internalPointer());
288 }
289 
fileInfoFromIndex(const QModelIndex & index) const290 std::shared_ptr<const Fm::FileInfo> FolderModel::fileInfoFromIndex(const QModelIndex& index) const {
291     FolderModelItem* item = itemFromIndex(index);
292     return item ? item->info : nullptr;
293 }
294 
makeTooltip(FolderModelItem * item) const295 QString FolderModel::makeTooltip(FolderModelItem* item) const {
296     // name (bold)
297     QString tip = QStringLiteral("<p><b>") + (showFullNames_ ? QString::fromStdString(item->name()) : item->displayName()).toHtmlEscaped() + QStringLiteral("</b></p>");
298 
299     // parent dir
300     auto info = item->info;
301     auto parent_path = info->path().parent();
302     auto parent_str = parent_path ? parent_path.displayName() : nullptr;
303     if(parent_str) {
304         tip += QStringLiteral("<p><i>") + tr("Location:") + QStringLiteral("</i> ") + QString::fromUtf8(parent_str.get()).toHtmlEscaped() + QStringLiteral("</p>");
305     }
306 
307     // file type
308     tip += QStringLiteral("<i>") + tr("File type:") + QStringLiteral("</i> ") + QString::fromUtf8(info->mimeType()->desc());
309 
310     // file size
311     const QString dSize = item->displaySize();
312     if(!dSize.isEmpty()) { // it's empty for directories
313         tip += QStringLiteral("<br><i>") + tr("File size:") + QStringLiteral("</i> ") + dSize;
314     }
315 
316     // times
317     tip += QStringLiteral("<br><i>") + tr("Last modified:") + QStringLiteral("</i> ") + item->displayMtime()
318            + QStringLiteral("<br><i>") + tr("Last accessed:") + QStringLiteral("</i> ")
319                                        + (info->atime() == 0 ? tr("N/A")
320                                                              : QDateTime::fromMSecsSinceEpoch(info->atime() * 1000).toString(Qt::SystemLocaleShortDate))
321            + QStringLiteral("<br><i>") + tr("Created:") + QStringLiteral("</i> ") + item->displayCrtime();
322 
323     // owner
324     const QString owner = item->ownerName();
325     if(!owner.isEmpty()) { // can be empty
326         tip += QStringLiteral("<br><i>") + tr("Owner:") + QStringLiteral("</i> ") + owner
327                + QStringLiteral("<br><i>") + tr("Group:") + QStringLiteral("</i> ") + item->ownerGroup();
328     }
329 
330     return tip;
331 }
332 
data(const QModelIndex & index,int role) const333 QVariant FolderModel::data(const QModelIndex& index, int role/* = Qt::DisplayRole*/) const {
334     if(!index.isValid() || index.row() > items.size() || index.column() >= NumOfColumns) {
335         return QVariant();
336     }
337     FolderModelItem* item = itemFromIndex(index);
338     auto info = item->info;
339     bool isCut = folder_ && item->isCut;
340 
341     switch(role) {
342     case Qt::ToolTipRole:
343         return QVariant(makeTooltip(item));
344     case Qt::DisplayRole:  {
345         switch(index.column()) {
346         case ColumnFileName:
347             return (showFullNames_ && !item->name().empty() ? QString::fromStdString(item->name())
348                                                             : item->displayName());
349         case ColumnFileType:
350             return QString::fromUtf8(info->mimeType()->desc());
351         case ColumnFileMTime:
352             return item->displayMtime();
353         case ColumnFileCrTime:
354             return item->displayCrtime();
355         case ColumnFileDTime:
356             return item->displayDtime();
357         case ColumnFileSize:
358             return item->displaySize();
359         case ColumnFileOwner:
360             return item->ownerName();
361         case ColumnFileGroup:
362             return item->ownerGroup();
363         }
364         break;
365     }
366     case Qt::DecorationRole: {
367         if(index.column() == 0) {
368             return QVariant(item->icon());
369         }
370         break;
371     }
372     case Qt::EditRole: {
373         if(index.column() == 0) {
374             return QString::fromStdString(info->name());
375         }
376         break;
377     }
378     case FileInfoRole:
379         return QVariant::fromValue(info);
380     case FileIsDirRole:
381         return QVariant(info->isDir());
382     case FileIsCutRole:
383         return isCut;
384     }
385     return QVariant();
386 }
387 
headerData(int section,Qt::Orientation orientation,int role) const388 QVariant FolderModel::headerData(int section, Qt::Orientation orientation, int role/* = Qt::DisplayRole*/) const {
389     if(role == Qt::DisplayRole) {
390         if(orientation == Qt::Horizontal) {
391             QString title;
392             switch(section) {
393             case ColumnFileName:
394                 title = tr("Name");
395                 break;
396             case ColumnFileType:
397                 title = tr("Type");
398                 break;
399             case ColumnFileSize:
400                 title = tr("Size");
401                 break;
402             case ColumnFileMTime:
403                 title = tr("Modified");
404                 break;
405             case ColumnFileCrTime:
406                 title = tr("Created");
407                 break;
408             case ColumnFileDTime:
409                 title = tr("Deleted");
410                 break;
411             case ColumnFileOwner:
412                 title = tr("Owner");
413                 break;
414             case ColumnFileGroup:
415                 title = tr("Group");
416                 break;
417             }
418             return QVariant(title);
419         }
420     }
421     return QVariant();
422 }
423 
index(int row,int column,const QModelIndex &) const424 QModelIndex FolderModel::index(int row, int column, const QModelIndex& /*parent*/) const {
425     if(row < 0 || row >= items.size() || column < 0 || column >= NumOfColumns) {
426         return QModelIndex();
427     }
428     const FolderModelItem& item = items.at(row);
429     return createIndex(row, column, (void*)&item);
430 }
431 
parent(const QModelIndex &) const432 QModelIndex FolderModel::parent(const QModelIndex& /*index*/) const {
433     return QModelIndex();
434 }
435 
flags(const QModelIndex & index) const436 Qt::ItemFlags FolderModel::flags(const QModelIndex& index) const {
437     // FIXME: should not return same flags unconditionally for all columns
438     Qt::ItemFlags flags;
439     if(index.isValid()) {
440         flags = Qt::ItemIsEnabled | Qt::ItemIsSelectable;
441         if(index.column() == ColumnFileName) {
442             flags |= (Qt::ItemIsDragEnabled | Qt::ItemIsDropEnabled
443                       | Qt::ItemIsEditable); // inline renaming);
444         }
445     }
446     else {
447         flags = Qt::ItemIsDropEnabled;
448     }
449     return flags;
450 }
451 
fileInfoFromPath(const Fm::FilePath & path) const452 std::shared_ptr<const Fm::FileInfo> FolderModel::fileInfoFromPath(const Fm::FilePath& path) const {
453     QList<FolderModelItem>::const_iterator it = items.begin();
454     while(it != items.end()) {
455         const FolderModelItem& item = *it;
456         if(item.info->path() == path) {
457             return item.info;
458         }
459         ++it;
460     }
461     return nullptr;
462 }
463 
464 // FIXME: this is very inefficient and should be replaced with a
465 // more reasonable implementation later.
findItemByName(const char * name,int * row)466 QList<FolderModelItem>::iterator FolderModel::findItemByName(const char* name, int* row) {
467     QList<FolderModelItem>::iterator it = items.begin();
468     int i = 0;
469     while(it != items.end()) {
470         FolderModelItem& item = *it;
471         if(item.info->name() == name) {
472             *row = i;
473             return it;
474         }
475         ++it;
476         ++i;
477     }
478     return items.end();
479 }
480 
findItemByFileInfo(const Fm::FileInfo * info,int * row)481 QList< FolderModelItem >::iterator FolderModel::findItemByFileInfo(const Fm::FileInfo* info, int* row) {
482     QList<FolderModelItem>::iterator it = items.begin();
483     int i = 0;
484     while(it != items.end()) {
485         FolderModelItem& item = *it;
486         if(item.info.get() == info) {
487             *row = i;
488             return it;
489         }
490         ++it;
491         ++i;
492     }
493     return items.end();
494 }
495 
mimeTypes() const496 QStringList FolderModel::mimeTypes() const {
497     //qDebug("FolderModel::mimeTypes");
498     QStringList types = QAbstractItemModel::mimeTypes();
499     // now types contains "application/x-qabstractitemmodeldatalist"
500 
501     // add support for freedesktop Xdnd direct save (XDS) protocol.
502     // https://www.freedesktop.org/wiki/Specifications/XDS/#index4h2
503     // the real implementation is in FolderView::childDropEvent().
504     types << QStringLiteral("XdndDirectSave0");
505     types << QStringLiteral("text/uri-list");
506     types << QStringLiteral("libfm/files"); // see FolderModel::mimeData() below
507     return types;
508 }
509 
mimeData(const QModelIndexList & indexes) const510 QMimeData* FolderModel::mimeData(const QModelIndexList& indexes) const {
511     QMimeData* data = QAbstractItemModel::mimeData(indexes);
512     //qDebug("FolderModel::mimeData");
513     // build two uri lists, one for internal DND
514     // and the other for DNDing to external apps
515     QByteArray urilist, libfmUrilist;
516     urilist.reserve(4096);
517     libfmUrilist.reserve(4096);
518 
519     for(const auto& index : indexes) {
520         FolderModelItem* item = itemFromIndex(index);
521         if(item && item->info) {
522             auto path = item->info->path();
523             if(path.isValid()) {
524                 // get the list that will be used by internal DND
525                 auto uri = path.uri();
526                 libfmUrilist.append(uri.get());
527                 libfmUrilist.append('\n');
528 
529                 // also, get the list that will be used when DNDing to external apps,
530                 // using local paths as far as possible (for DNDing from remote folders)
531                 if(auto localPath = path.localPath()) {
532                     QUrl url = QUrl::fromLocalFile(QString::fromUtf8(localPath.get()));
533                     urilist.append(url.toEncoded());
534                 }
535                 else {
536                     urilist.append(uri.get());
537                 }
538                 urilist.append('\n');
539             }
540         }
541     }
542     data->setData(QStringLiteral("text/uri-list"), urilist);
543     // NOTE: The mimetype "text/uri-list" changes the list in QMimeData::setData() to get URLs
544     // but some protocols (like MTP) may need the original list to query file info.
545     data->setData(QStringLiteral("libfm/files"), libfmUrilist);
546 
547     return data;
548 }
549 
dropMimeData(const QMimeData * data,Qt::DropAction action,int row,int column,const QModelIndex & parent)550 bool FolderModel::dropMimeData(const QMimeData* data, Qt::DropAction action, int row, int column, const QModelIndex& parent) {
551     //qDebug("FolderModel::dropMimeData");
552     if(!folder_ || !data) {
553         return false;
554     }
555     Fm::FilePath destPath;
556     if(parent.isValid()) { // drop on an item
557         std::shared_ptr<const Fm::FileInfo> info;
558         if(row == -1 && column == -1) {
559             info = fileInfoFromIndex(parent);
560         }
561         else {
562             QModelIndex itemIndex = index(row, column, parent);
563             info = fileInfoFromIndex(itemIndex);
564         }
565         if(info) {
566             if (info->isDir()) {
567                 destPath = info->path();
568             }
569             else {
570                 destPath = path(); // don't drop on file
571             }
572         }
573         else {
574             return false;
575         }
576     }
577     else { // drop on blank area of the folder
578         destPath = path();
579     }
580 
581     Fm::FilePathList srcPaths;
582     // try to get paths from the original data
583     if(data->hasFormat(QStringLiteral("libfm/files"))) {
584         QByteArray _data = data->data(QStringLiteral("libfm/files"));
585         srcPaths = pathListFromUriList(_data.data());
586     }
587     if(srcPaths.empty() && data->hasUrls()) {
588         srcPaths = Fm::pathListFromQUrls(data->urls());
589     }
590 
591     // FIXME: should we put this in dropEvent handler of FolderView instead?
592     if(!srcPaths.empty()) {
593         //qDebug("drop action: %d", action);
594         switch(action) {
595         case Qt::CopyAction:
596             FileOperation::copyFiles(srcPaths, destPath);
597             break;
598         case Qt::MoveAction:
599             FileOperation::moveFiles(srcPaths, destPath);
600             break;
601         case Qt::LinkAction:
602             FileOperation::symlinkFiles(srcPaths, destPath);
603         /* Falls through. */
604         default:
605             return false;
606         }
607         return true;
608     }
609     else if(data->hasFormat(QStringLiteral("application/x-qabstractitemmodeldatalist"))) {
610         return true;
611     }
612     return QAbstractListModel::dropMimeData(data, action, row, column, parent);
613 }
614 
supportedDropActions() const615 Qt::DropActions FolderModel::supportedDropActions() const {
616     //qDebug("FolderModel::supportedDropActions");
617     return Qt::CopyAction | Qt::MoveAction | Qt::LinkAction;
618 }
619 
620 // ask the model to load thumbnails of the specified size
cacheThumbnails(const int size)621 void FolderModel::cacheThumbnails(const int size) {
622     auto it = std::find_if(thumbnailData_.begin(), thumbnailData_.end(), [size](ThumbnailData& item){return item.size_ == size;});
623     if(it != thumbnailData_.cend()) {
624         ++it->refCount_;
625     }
626     else {
627         thumbnailData_.push_front(ThumbnailData(size));
628     }
629 }
630 
631 // ask the model to free cached thumbnails of the specified size
releaseThumbnails(int size)632 void FolderModel::releaseThumbnails(int size) {
633     auto prev = thumbnailData_.before_begin();
634     for(auto it = thumbnailData_.begin(); it != thumbnailData_.end(); ++it) {
635         if(it->size_ == size) {
636             --it->refCount_;
637             if(it->refCount_ == 0) {
638                 thumbnailData_.erase_after(prev);
639             }
640 
641             // remove all cached thumbnails of the specified size
642             QList<FolderModelItem>::iterator itemIt;
643             for(itemIt = items.begin(); itemIt != items.end(); ++itemIt) {
644                 FolderModelItem& item = *itemIt;
645                 item.removeThumbnail(size);
646             }
647             break;
648         }
649         prev = it;
650     }
651 }
652 
onThumbnailJobFinished()653 void FolderModel::onThumbnailJobFinished() {
654     Fm::ThumbnailJob* job = static_cast<Fm::ThumbnailJob*>(sender());
655     auto it = std::find(pendingThumbnailJobs_.cbegin(), pendingThumbnailJobs_.cend(), job);
656     if(it != pendingThumbnailJobs_.end()) {
657         pendingThumbnailJobs_.erase(it);
658     }
659 }
660 
onThumbnailLoaded(const std::shared_ptr<const Fm::FileInfo> & file,int size,const QImage & image)661 void FolderModel::onThumbnailLoaded(const std::shared_ptr<const Fm::FileInfo>& file, int size, const QImage& image) {
662     // find the model item this thumbnail belongs to
663     int row;
664     QList<FolderModelItem>::iterator it = findItemByFileInfo(file.get(), &row);
665     if(it != items.end()) {
666         // the file is found in our model
667         FolderModelItem& item = *it;
668         QModelIndex index = createIndex(row, 0, (void*)&item);
669         // store the image in the folder model item.
670         FolderModelItem::Thumbnail* thumbnail = item.findThumbnail(size);
671         thumbnail->image = image;
672         // qDebug("thumbnail loaded for: %s, size: %d", item.displayName.toUtf8().constData(), size);
673         if(image.isNull()) {
674             thumbnail->status = FolderModelItem::ThumbnailFailed;
675         }
676         else {
677             thumbnail->status = FolderModelItem::ThumbnailLoaded;
678             thumbnail->image = image;
679 
680             // tell the world that we have the thumbnail loaded
681             Q_EMIT thumbnailLoaded(index, size);
682         }
683     }
684 }
685 
686 // get a thumbnail of size at the index
687 // if a thumbnail is not yet loaded, this will initiate loading of the thumbnail.
thumbnailFromIndex(const QModelIndex & index,int size)688 QImage FolderModel::thumbnailFromIndex(const QModelIndex& index, int size) {
689     FolderModelItem* item = itemFromIndex(index);
690     if(item) {
691         FolderModelItem::Thumbnail* thumbnail = item->findThumbnail(size);
692         // qDebug("FolderModel::thumbnailFromIndex: %d, %s", thumbnail->status, item->displayName.toUtf8().data());
693         switch(thumbnail->status) {
694         case FolderModelItem::ThumbnailNotChecked: {
695             // load the thumbnail
696             queueLoadThumbnail(item->info, size);
697             thumbnail->status = FolderModelItem::ThumbnailLoading;
698             break;
699         }
700         case FolderModelItem::ThumbnailLoaded:
701             return thumbnail->image;
702         default:
703             ;
704         }
705     }
706     return QImage();
707 }
708 
709 
710 } // namespace Fm
711