pollymc/gui/pages/ScreenshotsPage.cpp
Petr Mrázek a218d7b7f6 Improve screenshot view/model.
Changes to screenshots are tracked.
Thumbnails are generated in a thread pool.
2014-07-05 13:27:32 +02:00

441 lines
11 KiB
C++

#include "ScreenshotsPage.h"
#include "ui_ScreenshotsPage.h"
#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>
#include "gui/dialogs/ProgressDialog.h"
#include "gui/dialogs/CustomMessageBox.h"
#include "logic/net/NetJob.h"
#include "logic/screenshots/ImgurUpload.h"
#include "logic/screenshots/ImgurAlbumCreation.h"
#include "logic/tasks/SequentialTask.h"
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();
if (!model)
return QVariant();
if (role == Qt::DisplayRole || role == Qt::EditRole)
{
QVariant result = sourceModel()->data(mapToSource(proxyIndex), role);
return result.toString().remove(QRegExp("\\.png$"));
}
if (role == Qt::DecorationRole)
{
QVariant result = sourceModel()->data(mapToSource(proxyIndex), QFileSystemModel::FilePathRole);
QString filePath = result.toString();
QIcon temp;
if(!watched.contains(filePath))
{
((QFileSystemWatcher &)watcher).addPath(filePath);
((QSet<QString> &)watched).insert(filePath);
}
if(m_thumbnailCache->get(filePath, temp))
{
return temp;
}
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)
{
auto model = sourceModel();
if (!model)
return false;
if (role != Qt::EditRole)
return false;
// FIXME: this is a workaround for a bug in QFileSystemModel, where it doesn't
// sort after renames
{
((QFileSystemModel *)model)->setNameFilterDisables(true);
((QFileSystemModel *)model)->setNameFilterDisables(false);
}
return model->setData(mapToSource(index), value.toString() + ".png", role);
}
private:
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
{
public:
explicit CenteredEditingDelegate(QObject *parent = 0) : QStyledItemDelegate(parent)
{
}
virtual ~CenteredEditingDelegate()
{
}
virtual QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem &option,
const QModelIndex &index) const
{
auto widget = QStyledItemDelegate::createEditor(parent, option, index);
auto foo = dynamic_cast<QLineEdit *>(widget);
if (foo)
{
foo->setAlignment(Qt::AlignHCenter);
foo->setFrame(true);
foo->setMaximumWidth(192);
}
return widget;
}
};
QString ScreenshotsPage::displayName()
{
return tr("Screenshots");
}
QIcon ScreenshotsPage::icon()
{
return QIcon::fromTheme("screenshots");
}
QString ScreenshotsPage::id()
{
return "screenshots";
}
ScreenshotsPage::ScreenshotsPage(BaseInstance *instance, QWidget *parent)
: QWidget(parent), ui(new Ui::ScreenshotsPage)
{
m_model.reset(new QFileSystemModel());
m_filterModel.reset(new FilterModel());
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, 160));
ui->listView->setSpacing(9);
// ui->listView->setUniformItemSizes(true);
ui->listView->setLayoutMode(QListView::Batched);
ui->listView->setViewMode(QListView::IconMode);
ui->listView->setResizeMode(QListView::Adjust);
ui->listView->installEventFilter(this);
ui->listView->setEditTriggers(0);
ui->listView->setItemDelegate(new CenteredEditingDelegate(this));
connect(ui->listView, SIGNAL(activated(QModelIndex)), SLOT(onItemActivated(QModelIndex)));
}
bool ScreenshotsPage::eventFilter(QObject *obj, QEvent *evt)
{
if (obj != ui->listView)
return QWidget::eventFilter(obj, evt);
if (evt->type() != QEvent::KeyPress)
{
return QWidget::eventFilter(obj, evt);
}
QKeyEvent *keyEvent = static_cast<QKeyEvent *>(evt);
switch (keyEvent->key())
{
case Qt::Key_Delete:
on_deleteBtn_clicked();
return true;
case Qt::Key_F2:
on_renameBtn_clicked();
return true;
default:
break;
}
return QWidget::eventFilter(obj, evt);
}
ScreenshotsPage::~ScreenshotsPage()
{
delete ui;
}
void ScreenshotsPage::onItemActivated(QModelIndex index)
{
if (!index.isValid())
return;
auto info = m_model->fileInfo(index);
QString fileName = info.absoluteFilePath();
openFileInDefaultProgram(info.absoluteFilePath());
}
void ScreenshotsPage::on_viewFolderBtn_clicked()
{
openDirInDefaultProgram(m_folder, true);
}
void ScreenshotsPage::on_uploadBtn_clicked()
{
auto selection = ui->listView->selectionModel()->selectedIndexes();
if (selection.isEmpty())
return;
QList<ScreenshotPtr> uploaded;
auto job = std::make_shared<NetJob>("Screenshot Upload");
for (auto item : selection)
{
auto info = m_model->fileInfo(item);
auto screenshot = std::make_shared<ScreenShot>(info);
uploaded.push_back(screenshot);
job->addNetAction(ImgurUpload::make(screenshot));
}
SequentialTask task;
auto albumTask = std::make_shared<NetJob>("Imgur Album Creation");
auto imgurAlbum = ImgurAlbumCreation::make(uploaded);
albumTask->addNetAction(imgurAlbum);
task.addTask(job);
task.addTask(albumTask);
ProgressDialog prog(this);
if (prog.exec(&task) != QDialog::Accepted)
{
CustomMessageBox::selectable(this, tr("Failed to upload screenshots!"),
tr("Unknown error"), QMessageBox::Warning)->exec();
}
else
{
CustomMessageBox::selectable(
this, tr("Upload finished"),
tr("<a href=\"https://imgur.com/a/%1\">Visit album</a><br/>Delete hash: %2 (save "
"this if you want to be able to edit/delete the album)")
.arg(imgurAlbum->id(), imgurAlbum->deleteHash()),
QMessageBox::Information)->exec();
}
}
void ScreenshotsPage::on_deleteBtn_clicked()
{
auto mbox = CustomMessageBox::selectable(
this, tr("Are you sure?"), tr("This will delete all selected screenshots."),
QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No);
std::unique_ptr<QMessageBox> box(mbox);
if (box->exec() != QMessageBox::Yes)
return;
auto selected = ui->listView->selectionModel()->selectedIndexes();
for (auto item : selected)
{
m_model->remove(item);
}
}
void ScreenshotsPage::on_renameBtn_clicked()
{
auto selection = ui->listView->selectionModel()->selectedIndexes();
if (selection.isEmpty())
return;
ui->listView->edit(selection[0]);
// TODO: mass renaming
}
void ScreenshotsPage::opened()
{
if (m_valid)
{
QString path = QDir(m_folder).absolutePath();
m_model->setRootPath(path);
ui->listView->setRootIndex(m_filterModel->mapFromSource(m_model->index(path)));
}
}
#include "ScreenshotsPage.moc"