Improve screenshot view/model.
Changes to screenshots are tracked. Thumbnails are generated in a thread pool.
This commit is contained in:
parent
b5d6f50fb1
commit
a218d7b7f6
@ -3,11 +3,14 @@
|
|||||||
|
|
||||||
#include <QModelIndex>
|
#include <QModelIndex>
|
||||||
#include <QMutableListIterator>
|
#include <QMutableListIterator>
|
||||||
|
#include <QMap>
|
||||||
|
#include <QSet>
|
||||||
#include <QFileIconProvider>
|
#include <QFileIconProvider>
|
||||||
#include <QFileSystemModel>
|
#include <QFileSystemModel>
|
||||||
#include <QStyledItemDelegate>
|
#include <QStyledItemDelegate>
|
||||||
#include <QLineEdit>
|
#include <QLineEdit>
|
||||||
#include <QtGui/qevent.h>
|
#include <QtGui/qevent.h>
|
||||||
|
#include <QtGui/QPainter>
|
||||||
|
|
||||||
#include <pathutils.h>
|
#include <pathutils.h>
|
||||||
|
|
||||||
@ -18,9 +21,156 @@
|
|||||||
#include "logic/screenshots/ImgurAlbumCreation.h"
|
#include "logic/screenshots/ImgurAlbumCreation.h"
|
||||||
#include "logic/tasks/SequentialTask.h"
|
#include "logic/tasks/SequentialTask.h"
|
||||||
|
|
||||||
class FilterModel : public QIdentityProxyModel
|
template <typename K, typename V>
|
||||||
|
class RWStorage
|
||||||
{
|
{
|
||||||
public:
|
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
|
virtual QVariant data(const QModelIndex &proxyIndex, int role = Qt::DisplayRole) const
|
||||||
{
|
{
|
||||||
auto model = sourceModel();
|
auto model = sourceModel();
|
||||||
@ -35,38 +185,23 @@ public:
|
|||||||
{
|
{
|
||||||
QVariant result = sourceModel()->data(mapToSource(proxyIndex), QFileSystemModel::FilePathRole);
|
QVariant result = sourceModel()->data(mapToSource(proxyIndex), QFileSystemModel::FilePathRole);
|
||||||
QString filePath = result.toString();
|
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;
|
if(m_thumbnailCache->get(filePath, temp))
|
||||||
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)
|
|
||||||
{
|
{
|
||||||
QImage image(info.absoluteFilePath());
|
return temp;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
// we failed anyway...
|
if(!m_failed.contains(filePath))
|
||||||
return sourceModel()->data(mapToSource(proxyIndex), QFileSystemModel::FileIconRole);
|
{
|
||||||
}
|
((FilterModel *)this)->thumbnailImage(filePath);
|
||||||
else
|
}
|
||||||
{
|
return(m_thumbnailCache->get("placeholder"));
|
||||||
QVariant result = sourceModel()->data(mapToSource(proxyIndex), role);
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
return sourceModel()->data(mapToSource(proxyIndex), role);
|
||||||
}
|
}
|
||||||
virtual bool setData(const QModelIndex &index, const QVariant &value,
|
virtual bool setData(const QModelIndex &index, const QVariant &value,
|
||||||
int role = Qt::EditRole)
|
int role = Qt::EditRole)
|
||||||
@ -85,7 +220,38 @@ public:
|
|||||||
return model->setData(mapToSource(index), value.toString() + ".png", role);
|
return model->setData(mapToSource(index), value.toString() + ".png", role);
|
||||||
}
|
}
|
||||||
private:
|
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
|
class CenteredEditingDelegate : public QStyledItemDelegate
|
||||||
@ -135,13 +301,15 @@ ScreenshotsPage::ScreenshotsPage(BaseInstance *instance, QWidget *parent)
|
|||||||
m_filterModel->setSourceModel(m_model.get());
|
m_filterModel->setSourceModel(m_model.get());
|
||||||
m_model->setFilter(QDir::Files | QDir::Writable | QDir::Readable);
|
m_model->setFilter(QDir::Files | QDir::Writable | QDir::Readable);
|
||||||
m_model->setReadOnly(false);
|
m_model->setReadOnly(false);
|
||||||
|
m_model->setNameFilters({"*.png"});
|
||||||
|
m_model->setNameFilterDisables(false);
|
||||||
m_folder = PathCombine(instance->minecraftRoot(), "screenshots");
|
m_folder = PathCombine(instance->minecraftRoot(), "screenshots");
|
||||||
m_valid = ensureFolderPathExists(m_folder);
|
m_valid = ensureFolderPathExists(m_folder);
|
||||||
|
|
||||||
ui->setupUi(this);
|
ui->setupUi(this);
|
||||||
ui->listView->setModel(m_filterModel.get());
|
ui->listView->setModel(m_filterModel.get());
|
||||||
ui->listView->setIconSize(QSize(128, 128));
|
ui->listView->setIconSize(QSize(128, 128));
|
||||||
ui->listView->setGridSize(QSize(192, 128));
|
ui->listView->setGridSize(QSize(192, 160));
|
||||||
ui->listView->setSpacing(9);
|
ui->listView->setSpacing(9);
|
||||||
// ui->listView->setUniformItemSizes(true);
|
// ui->listView->setUniformItemSizes(true);
|
||||||
ui->listView->setLayoutMode(QListView::Batched);
|
ui->listView->setLayoutMode(QListView::Batched);
|
||||||
@ -268,3 +436,5 @@ void ScreenshotsPage::opened()
|
|||||||
ui->listView->setRootIndex(m_filterModel->mapFromSource(m_model->index(path)));
|
ui->listView->setRootIndex(m_filterModel->mapFromSource(m_model->index(path)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#include "ScreenshotsPage.moc"
|
||||||
|
@ -164,5 +164,8 @@
|
|||||||
<file>24x24/noaccount.png</file>
|
<file>24x24/noaccount.png</file>
|
||||||
<file>32x32/noaccount.png</file>
|
<file>32x32/noaccount.png</file>
|
||||||
<file>48x48/noaccount.png</file>
|
<file>48x48/noaccount.png</file>
|
||||||
|
|
||||||
|
<!-- placeholder when loading screenshot images -->
|
||||||
|
<file>scalable/screenshot-placeholder.svg</file>
|
||||||
</qresource>
|
</qresource>
|
||||||
</RCC>
|
</RCC>
|
||||||
|
86
resources/multimc/scalable/screenshot-placeholder.svg
Normal file
86
resources/multimc/scalable/screenshot-placeholder.svg
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||||
|
xmlns:cc="http://creativecommons.org/ns#"
|
||||||
|
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
width="256"
|
||||||
|
height="256"
|
||||||
|
id="svg2"
|
||||||
|
version="1.1"
|
||||||
|
inkscape:version="0.48.5 r10040"
|
||||||
|
sodipodi:docname="screenshot-placeholder.svg">
|
||||||
|
<defs
|
||||||
|
id="defs4" />
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="base"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#666666"
|
||||||
|
borderopacity="1.0"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pageshadow="2"
|
||||||
|
inkscape:zoom="0.49497475"
|
||||||
|
inkscape:cx="-33.672765"
|
||||||
|
inkscape:cy="136.19802"
|
||||||
|
inkscape:document-units="px"
|
||||||
|
inkscape:current-layer="layer1"
|
||||||
|
showgrid="false"
|
||||||
|
inkscape:snap-bbox="true"
|
||||||
|
inkscape:bbox-paths="true"
|
||||||
|
inkscape:bbox-nodes="true"
|
||||||
|
inkscape:snap-bbox-edge-midpoints="true"
|
||||||
|
inkscape:snap-bbox-midpoints="true"
|
||||||
|
inkscape:snap-nodes="false"
|
||||||
|
inkscape:window-width="1612"
|
||||||
|
inkscape:window-height="1026"
|
||||||
|
inkscape:window-x="1677"
|
||||||
|
inkscape:window-y="-4"
|
||||||
|
inkscape:window-maximized="1">
|
||||||
|
<inkscape:grid
|
||||||
|
type="xygrid"
|
||||||
|
id="grid2985"
|
||||||
|
empspacing="5"
|
||||||
|
visible="true"
|
||||||
|
enabled="true"
|
||||||
|
snapvisiblegridlinesonly="true"
|
||||||
|
spacingx="8px"
|
||||||
|
spacingy="8px" />
|
||||||
|
</sodipodi:namedview>
|
||||||
|
<metadata
|
||||||
|
id="metadata7">
|
||||||
|
<rdf:RDF>
|
||||||
|
<cc:Work
|
||||||
|
rdf:about="">
|
||||||
|
<dc:format>image/svg+xml</dc:format>
|
||||||
|
<dc:type
|
||||||
|
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||||
|
<dc:title></dc:title>
|
||||||
|
</cc:Work>
|
||||||
|
</rdf:RDF>
|
||||||
|
</metadata>
|
||||||
|
<g
|
||||||
|
inkscape:label="Layer 1"
|
||||||
|
inkscape:groupmode="layer"
|
||||||
|
id="layer1"
|
||||||
|
transform="translate(0,-796.36218)">
|
||||||
|
<g
|
||||||
|
id="g3009"
|
||||||
|
style="fill:#000000">
|
||||||
|
<path
|
||||||
|
id="rect2987"
|
||||||
|
transform="translate(0,796.36218)"
|
||||||
|
d="M 24 8 C 15.136 8 8 15.136 8 24 L 8 232 C 8 240.864 15.136 248 24 248 L 232 248 C 240.864 248 248 240.864 248 232 L 248 24 C 248 15.136 240.864 8 232 8 L 24 8 z M 24 16 L 232 16 C 236.432 16 240 19.568 240 24 L 240 232 C 240 236.432 236.432 240 232 240 L 24 240 C 19.568 240 16 236.432 16 232 L 16 24 C 16 19.568 19.568 16 24 16 z "
|
||||||
|
style="opacity:0.75000000000000000;color:#000000;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.75000000000000000;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
|
||||||
|
<path
|
||||||
|
style="fill:#000000;opacity:0.75000000000000000;stroke:none"
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
d="m 85.749999,937.9006 c 0,24.30067 18.915811,43.99997 42.249991,43.99997 23.33419,0 42.25001,-19.6993 42.25001,-43.99997 0,-24.30069 -18.91582,-43.99999 -42.25001,-43.99999 -23.33418,0 -42.249991,19.6993 -42.249991,43.99999 z M 219,863.43909 h -45.5 c -3.25,-13.53846 -6.50001,-27.07691 -19.5,-27.07691 h -52 c -13.000001,0 -16.250001,13.53845 -19.500001,27.07691 H 37 c -7.15,0 -13,6.09231 -13,13.53846 v 121.84603 c 0,7.44632 5.85,13.53862 13,13.53862 h 182 c 7.15,0 13,-6.0923 13,-13.53862 V 876.97755 c 0,-7.44615 -5.85,-13.53846 -13,-13.53846 z m -91.00001,134.53849 c -31.860147,0 -57.687491,-26.89678 -57.687491,-60.07698 0,-33.1798 25.827344,-60.0769 57.687491,-60.0769 31.86057,0 57.68751,26.8971 57.68751,60.0769 0,33.1802 -25.82653,60.07698 -57.68751,60.07698 z M 219,904.05445 h -26 v -13.53846 h 26 v 13.53846 z"
|
||||||
|
id="path2996" />
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 3.7 KiB |
Loading…
Reference in New Issue
Block a user