From a218d7b7f6a9e30671be72b756104302637eb33d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20Mr=C3=A1zek?= Date: Sat, 5 Jul 2014 13:26:37 +0200 Subject: Improve screenshot view/model. Changes to screenshots are tracked. Thumbnails are generated in a thread pool. --- gui/pages/ScreenshotsPage.cpp | 228 ++++++++++++++++++++++++++++++++++++------ 1 file changed, 199 insertions(+), 29 deletions(-) (limited to 'gui/pages') diff --git a/gui/pages/ScreenshotsPage.cpp b/gui/pages/ScreenshotsPage.cpp index 051bc12d..f3ec0c1d 100644 --- a/gui/pages/ScreenshotsPage.cpp +++ b/gui/pages/ScreenshotsPage.cpp @@ -3,11 +3,14 @@ #include #include +#include +#include #include #include #include #include #include +#include #include @@ -18,9 +21,156 @@ #include "logic/screenshots/ImgurAlbumCreation.h" #include "logic/tasks/SequentialTask.h" +template +class RWStorage +{ +public: + void add(K key, V value) + { + QWriteLocker l(&lock); + cache[key] = value; + stale_entries.remove(key); + } + V get(K key) + { + QReadLocker l(&lock); + if(cache.contains(key)) + { + return cache[key]; + } + else return V(); + } + bool get(K key, V& value) + { + QReadLocker l(&lock); + if(cache.contains(key)) + { + value = cache[key]; + return true; + } + else return false; + } + bool has(K key) + { + QReadLocker l(&lock); + return cache.contains(key); + } + bool stale(K key) + { + QReadLocker l(&lock); + if(!cache.contains(key)) + return true; + return stale_entries.contains(key); + } + void setStale(K key) + { + QReadLocker l(&lock); + if(cache.contains(key)) + { + stale_entries.insert(key); + } + } + void clear() + { + QWriteLocker l(&lock); + cache.clear(); + } +private: + QReadWriteLock lock; + QMap cache; + QSet stale_entries; +}; + +typedef RWStorage SharedIconCache; +typedef std::shared_ptr SharedIconCachePtr; + +class ThumbnailingResult : public QObject +{ + Q_OBJECT +public Q_SLOTS: + inline void emitResultsReady(const QString &path) + { + emit resultsReady(path); + } + inline void emitResultsFailed(const QString &path) + { + emit resultsFailed(path); + } +Q_SIGNALS: + void resultsReady(const QString &path); + void resultsFailed(const QString &path); +}; + +class ThumbnailRunnable: public QRunnable +{ +public: + ThumbnailRunnable (QString path, SharedIconCachePtr cache) + { + m_path = path; + m_cache = cache; + } + void run() + { + QFileInfo info(m_path); + if(info.isDir()) + return; + if((info.suffix().compare("png", Qt::CaseInsensitive) != 0)) + return; + int tries = 5; + while(tries) + { + if(!m_cache->stale(m_path)) + return; + QImage image(m_path); + if (image.isNull()) + { + QThread::msleep(500); + tries--; + continue; + } + QImage small; + if(image.width() > image.height()) + small = image.scaledToWidth(512).scaledToWidth(256, Qt::SmoothTransformation); + else + small = image.scaledToHeight(512).scaledToHeight(256, Qt::SmoothTransformation); + auto smallSize = small.size(); + QPoint offset((256-small.width())/2, (256-small.height())/2); + QImage square(QSize(256,256), QImage::Format_ARGB32); + square.fill(Qt::transparent); + + QPainter painter(&square); + painter.drawImage(offset, small); + painter.end(); + + QIcon icon(QPixmap::fromImage(square)); + m_cache->add(m_path, icon); + m_resultEmitter.emitResultsReady(m_path); + return; + } + m_resultEmitter.emitResultsFailed(m_path); + } + QString m_path; + SharedIconCachePtr m_cache; + ThumbnailingResult m_resultEmitter; +}; + +// this is about as elegant and well written as a bag of bricks with scribbles done by insane asylum patients. class FilterModel : public QIdentityProxyModel { + Q_OBJECT public: + explicit FilterModel(QObject *parent = 0):QIdentityProxyModel(parent) + { + m_thumbnailingPool.setMaxThreadCount(4); + m_thumbnailCache = std::make_shared(); + m_thumbnailCache->add("placeholder", QIcon::fromTheme("screenshot-placeholder")); + connect(&watcher, SIGNAL(fileChanged(QString)), SLOT(fileChanged(QString))); + // FIXME: the watched file set is not updated when files are removed + } + virtual ~FilterModel() + { + m_thumbnailingPool.waitForDone(500); + } virtual QVariant data(const QModelIndex &proxyIndex, int role = Qt::DisplayRole) const { auto model = sourceModel(); @@ -35,38 +185,23 @@ public: { QVariant result = sourceModel()->data(mapToSource(proxyIndex), QFileSystemModel::FilePathRole); QString filePath = result.toString(); - if(thumbnailCache.contains(filePath)) + QIcon temp; + if(!watched.contains(filePath)) { - return thumbnailCache[filePath]; + ((QFileSystemWatcher &)watcher).addPath(filePath); + ((QSet &)watched).insert(filePath); } - bool failed = false; - QFileInfo info(filePath); - failed |= info.isDir(); - failed |= (info.suffix().compare("png", Qt::CaseInsensitive) != 0); - // WARNING: really an IF! this is purely for using break instead of goto... - while(!failed) + if(m_thumbnailCache->get(filePath, temp)) { - QImage image(info.absoluteFilePath()); - if (image.isNull()) - { - // TODO: schedule a retry. - failed = true; - break; - } - QImage thumbnail = image.scaledToWidth(512).scaledToWidth(256, Qt::SmoothTransformation); - QIcon icon(QPixmap::fromImage(thumbnail)); - // the casts are a hack for the stupid method being const. - ((QMap &)thumbnailCache).insert(filePath, icon); - return icon; + return temp; } - // we failed anyway... - return sourceModel()->data(mapToSource(proxyIndex), QFileSystemModel::FileIconRole); - } - else - { - QVariant result = sourceModel()->data(mapToSource(proxyIndex), role); - return result; + if(!m_failed.contains(filePath)) + { + ((FilterModel *)this)->thumbnailImage(filePath); + } + return(m_thumbnailCache->get("placeholder")); } + return sourceModel()->data(mapToSource(proxyIndex), role); } virtual bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) @@ -85,7 +220,38 @@ public: return model->setData(mapToSource(index), value.toString() + ".png", role); } private: - QMap thumbnailCache; + void thumbnailImage(QString path) + { + auto runnable = new ThumbnailRunnable(path, m_thumbnailCache); + connect(&(runnable->m_resultEmitter),SIGNAL(resultsReady(QString)), SLOT(thumbnailReady(QString))); + connect(&(runnable->m_resultEmitter),SIGNAL(resultsFailed(QString)), SLOT(thumbnailFailed(QString))); + ((QThreadPool &)m_thumbnailingPool).start(runnable); + } +private +slots: + void thumbnailReady(QString path) + { + emit layoutChanged(); + } + void thumbnailFailed(QString path) + { + m_failed.insert(path); + } + void fileChanged(QString filepath) + { + m_thumbnailCache->setStale(filepath); + thumbnailImage(filepath); + // reinsert the path... + watcher.removePath(filepath); + watcher.addPath(filepath); + } + +private: + SharedIconCachePtr m_thumbnailCache; + QThreadPool m_thumbnailingPool; + QSet m_failed; + QSet watched; + QFileSystemWatcher watcher; }; class CenteredEditingDelegate : public QStyledItemDelegate @@ -135,13 +301,15 @@ ScreenshotsPage::ScreenshotsPage(BaseInstance *instance, QWidget *parent) m_filterModel->setSourceModel(m_model.get()); m_model->setFilter(QDir::Files | QDir::Writable | QDir::Readable); m_model->setReadOnly(false); + m_model->setNameFilters({"*.png"}); + m_model->setNameFilterDisables(false); m_folder = PathCombine(instance->minecraftRoot(), "screenshots"); m_valid = ensureFolderPathExists(m_folder); ui->setupUi(this); ui->listView->setModel(m_filterModel.get()); ui->listView->setIconSize(QSize(128, 128)); - ui->listView->setGridSize(QSize(192, 128)); + ui->listView->setGridSize(QSize(192, 160)); ui->listView->setSpacing(9); // ui->listView->setUniformItemSizes(true); ui->listView->setLayoutMode(QListView::Batched); @@ -268,3 +436,5 @@ void ScreenshotsPage::opened() ui->listView->setRootIndex(m_filterModel->mapFromSource(m_model->index(path))); } } + +#include "ScreenshotsPage.moc" -- cgit v1.2.3