Improve screenshot view/model.
Changes to screenshots are tracked. Thumbnails are generated in a thread pool.
This commit is contained in:
@ -3,11 +3,14 @@
|
||||
|
||||
#include <QModelIndex>
|
||||
#include <QMutableListIterator>
|
||||
#include <QMap>
|
||||
#include <QSet>
|
||||
#include <QFileIconProvider>
|
||||
#include <QFileSystemModel>
|
||||
#include <QStyledItemDelegate>
|
||||
#include <QLineEdit>
|
||||
#include <QtGui/qevent.h>
|
||||
#include <QtGui/QPainter>
|
||||
|
||||
#include <pathutils.h>
|
||||
|
||||
@ -18,9 +21,156 @@
|
||||
#include "logic/screenshots/ImgurAlbumCreation.h"
|
||||
#include "logic/tasks/SequentialTask.h"
|
||||
|
||||
class FilterModel : public QIdentityProxyModel
|
||||
template <typename K, typename V>
|
||||
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<K, V> cache;
|
||||
QSet<K> stale_entries;
|
||||
};
|
||||
|
||||
typedef RWStorage<QString, QIcon> SharedIconCache;
|
||||
typedef std::shared_ptr<SharedIconCache> 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<SharedIconCache>();
|
||||
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<QString> &)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<QString, QIcon> &)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<QString, QIcon> 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<QString> m_failed;
|
||||
QSet<QString> 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"
|
||||
|
Reference in New Issue
Block a user