Replaced AsyncReqComponent with some handles.

UI is no longer completely blocked during asynchronous operations.
This commit is contained in:
Aloshi 2014-03-18 19:55:37 -05:00
parent dbde900629
commit 1e8b040f73
12 changed files with 238 additions and 160 deletions

View file

@ -135,6 +135,7 @@ endif()
#-------------------------------------------------------------------------------
#define basic sources and headers
set(ES_HEADERS
${CMAKE_CURRENT_SOURCE_DIR}/src/AsyncHandle.h
${CMAKE_CURRENT_SOURCE_DIR}/src/AudioManager.h
${CMAKE_CURRENT_SOURCE_DIR}/src/EmulationStation.h
${CMAKE_CURRENT_SOURCE_DIR}/src/FileData.h

43
src/AsyncHandle.h Normal file
View file

@ -0,0 +1,43 @@
#pragma once
enum AsyncHandleStatus
{
ASYNC_IN_PROGRESS,
ASYNC_ERROR,
ASYNC_DONE
};
// Handle for some asynchronous operation.
class AsyncHandle
{
public:
AsyncHandle() : mStatus(ASYNC_IN_PROGRESS) {};
virtual void update() = 0;
// Update and return the latest status.
inline AsyncHandleStatus status() { update(); return mStatus; }
// User-friendly string of our current status. Will return error message if status() == SEARCH_ERROR.
inline std::string getStatusString()
{
switch(mStatus)
{
case ASYNC_IN_PROGRESS:
return "in progress";
case ASYNC_ERROR:
return mError;
case ASYNC_DONE:
return "done";
default:
return "something impossible has occured; row, row, fight the power";
}
}
protected:
inline void setStatus(AsyncHandleStatus status) { mStatus = status; }
inline void setError(const std::string& error) { setStatus(ASYNC_ERROR); mError = error; }
std::string mError;
AsyncHandleStatus mStatus;
};

View file

@ -144,14 +144,9 @@ HttpReq::Status HttpReq::status()
return mStatus;
}
std::string HttpReq::getContent()
std::string HttpReq::getContent() const
{
if(mStatus != REQ_SUCCESS)
{
LOG(LogError) << "Called getContent() on an incomplete HttpReq!";
return "";
}
assert(mStatus == REQ_SUCCESS);
return mContent.str();
}

View file

@ -42,7 +42,7 @@ public:
std::string getErrorMsg();
std::string getContent();
std::string getContent() const; // mStatus must be REQ_SUCCESS
static std::string urlEncode(const std::string &s);
static bool isUrl(const std::string& s);

View file

@ -116,6 +116,8 @@ void ComponentList::onCursorChanged(const CursorState& state)
mCameraOffset = 0;
else if(mCameraOffset + mSize.y() > totalHeight)
mCameraOffset = totalHeight - mSize.y();
}else{
mCameraOffset = 0;
}
updateHelpPrompts();

View file

@ -259,7 +259,7 @@ private:
ss << getSelectedObjects().size() << " SELECTED";
mText.setText(ss.str());
mText.setSize(0, mText.getSize().y());
setSize(mText.getSize().x() + mRightArrow.getSize().x() + 16, mText.getSize().y());
setSize(mText.getSize().x() + mRightArrow.getSize().x() + 24, mText.getSize().y());
if(mParent) // hack since theres no "on child size changed" callback atm...
mParent->onSizeChanged();
}else{
@ -270,7 +270,7 @@ private:
{
mText.setText(strToUpper(it->name));
mText.setSize(0, mText.getSize().y());
setSize(mText.getSize().x() + mLeftArrow.getSize().x() + mRightArrow.getSize().x() + 16, mText.getSize().y());
setSize(mText.getSize().x() + mLeftArrow.getSize().x() + mRightArrow.getSize().x() + 24, mText.getSize().y());
if(mParent) // hack since theres no "on child size changed" callback atm...
mParent->onSizeChanged();
break;

View file

@ -15,6 +15,8 @@ ScraperSearchComponent::ScraperSearchComponent(Window* window, SearchType type)
{
addChild(&mGrid);
mBlockAccept = false;
using namespace Eigen;
// left spacer (empty component, needed for borders)
@ -87,6 +89,8 @@ void ScraperSearchComponent::search(const ScraperSearchParams& params)
{
mResultList->clear();
mScraperResults.clear();
mThumbnailReq.reset();
mMDResolveHandle.reset();
updateInfoPane();
mLastSearch = params;
@ -120,6 +124,7 @@ void ScraperSearchComponent::onSearchDone(const std::vector<ScraperSearchResult>
mGrid.resetCursor();
}
mBlockAccept = false;
updateInfoPane();
if(mSearchType == ALWAYS_ACCEPT_FIRST_RESULT)
@ -177,6 +182,9 @@ bool ScraperSearchComponent::input(InputConfig* config, Input input)
{
if(config->isMappedTo("a", input) && input.value != 0)
{
if(mBlockAccept)
return true;
//if you're on a result
if(getSelectedIndex() != -1)
{
@ -195,24 +203,28 @@ bool ScraperSearchComponent::input(InputConfig* config, Input input)
return ret;
}
void ScraperSearchComponent::render(const Eigen::Affine3f& parentTrans)
{
Eigen::Affine3f trans = parentTrans * getTransform();
if(mBlockAccept)
{
Renderer::setMatrix(trans);
Renderer::drawRect((int)mResultList->getPosition().x(), (int)mResultList->getPosition().y(),
(int)mResultList->getSize().x(), (int)mResultList->getSize().y(), 0x00000011);
}
renderChildren(trans);
}
void ScraperSearchComponent::returnResult(ScraperSearchResult result)
{
mBlockAccept = true;
// resolve metadata image before returning
if(!result.imageUrl.empty())
{
downloadImageAsync(mWindow, result.imageUrl, getSaveAsPath(mLastSearch, "image", result.imageUrl),
[this, result] (std::string filePath) mutable -> void
{
if(filePath.empty())
{
onSearchError("Error downloading boxart.");
return;
}
result.mdl.set("image", filePath);
result.imageUrl = "";
this->returnResult(result); // re-enter this function
});
mMDResolveHandle = resolveMetaDataAssets(result, mLastSearch);
return;
}
@ -226,19 +238,30 @@ void ScraperSearchComponent::update(int deltaTime)
updateThumbnail();
}
if(mSearchHandle && mSearchHandle->status() != SEARCH_IN_PROGRESS)
if(mSearchHandle && mSearchHandle->status() != ASYNC_IN_PROGRESS)
{
if(mSearchHandle->status() == SEARCH_DONE)
if(mSearchHandle->status() == ASYNC_DONE)
{
onSearchDone(mSearchHandle->getResults());
}else if(mSearchHandle->status() == SEARCH_ERROR)
}else if(mSearchHandle->status() == ASYNC_ERROR)
{
onSearchError(mSearchHandle->getStatusString());
}
mSearchHandle.reset();
}
if(mMDResolveHandle && mMDResolveHandle->status() != ASYNC_IN_PROGRESS)
{
if(mMDResolveHandle->status() == ASYNC_DONE)
{
returnResult(mMDResolveHandle->getResult());
}else if(mMDResolveHandle->status() == ASYNC_ERROR)
{
onSearchError(mMDResolveHandle->getStatusString());
}
mMDResolveHandle.reset();
}
GuiComponent::update(deltaTime);
}

View file

@ -34,6 +34,7 @@ public:
bool input(InputConfig* config, Input input) override;
void update(int deltaTime) override;
void render(const Eigen::Affine3f& parentTrans) override;
std::vector<HelpPrompt> getHelpPrompts() override;
void onSizeChanged() override;
void onFocusGained() override;
@ -65,8 +66,10 @@ private:
std::function<void(const ScraperSearchResult&)> mAcceptCallback;
std::function<void()> mSkipCallback;
std::function<void()> mCancelCallback;
bool mBlockAccept;
std::unique_ptr<ScraperSearchHandle> mSearchHandle;
std::unique_ptr<MDResolveHandle> mMDResolveHandle;
std::vector<ScraperSearchResult> mScraperResults;
std::unique_ptr<HttpReq> mThumbnailReq;
};

View file

@ -79,11 +79,14 @@ std::unique_ptr<ScraperSearchHandle> GamesDBScraper::getResultsAsync(const Scrap
GamesDBHandle::GamesDBHandle(const ScraperSearchParams& params, const std::string& url) :
mReq(std::unique_ptr<HttpReq>(new HttpReq(url)))
{
setStatus(SEARCH_IN_PROGRESS);
setStatus(ASYNC_IN_PROGRESS);
}
void GamesDBHandle::update()
{
if(mStatus == ASYNC_DONE)
return;
if(mReq->status() == HttpReq::REQ_IN_PROGRESS)
return;
@ -156,7 +159,7 @@ void GamesDBHandle::update()
game = game.next_sibling("Game");
}
setStatus(SEARCH_DONE);
setStatus(ASYNC_DONE);
setResults(results);
return;
}

View file

@ -9,48 +9,116 @@
#include "GamesDBScraper.h"
#include "TheArchiveScraper.h"
std::string ScraperSearchHandle::getStatusString()
std::shared_ptr<Scraper> createScraperByName(const std::string& name)
{
switch(mStatus)
if(name == "TheGamesDB")
return std::shared_ptr<Scraper>(new GamesDBScraper());
else if(name == "TheArchive")
return std::shared_ptr<Scraper>(new TheArchiveScraper());
return nullptr;
}
std::unique_ptr<MDResolveHandle> resolveMetaDataAssets(const ScraperSearchResult& result, const ScraperSearchParams& search)
{
return std::unique_ptr<MDResolveHandle>(new MDResolveHandle(result, search));
}
MDResolveHandle::MDResolveHandle(const ScraperSearchResult& result, const ScraperSearchParams& search) : mResult(result)
{
if(!result.imageUrl.empty())
{
case SEARCH_IN_PROGRESS:
return "search in progress";
case SEARCH_ERROR:
return mError;
case SEARCH_DONE:
return "search done";
default:
return "something impossible has occured";
std::string imgPath = getSaveAsPath(search, "image", result.imageUrl);
mFuncs.push_back(ResolvePair(downloadImageAsync(result.imageUrl, imgPath), [this, imgPath]
{
mResult.mdl.set("image", imgPath);
mResult.imageUrl = "";
}));
}
}
std::string processFileDownload(std::shared_ptr<HttpReq> r, std::string saveAs)
void MDResolveHandle::update()
{
if(r->status() != HttpReq::REQ_SUCCESS)
if(mStatus == ASYNC_DONE || mStatus == ASYNC_ERROR)
return;
auto it = mFuncs.begin();
while(it != mFuncs.end())
{
LOG(LogError) << "Failed to download file - HttpReq error: " << r->getErrorMsg();
return "";
if(it->first->status() == ASYNC_ERROR)
{
setError(it->first->getStatusString());
return;
}else if(it->first->status() == ASYNC_DONE)
{
it->second();
it = mFuncs.erase(it);
continue;
}
it++;
}
std::ofstream stream(saveAs, std::ios_base::out | std::ios_base::binary);
if(stream.fail())
if(mFuncs.empty())
setStatus(ASYNC_DONE);
}
std::unique_ptr<AsyncHandle> downloadImageAsync(const std::string& url, const std::string& saveAs)
{
return std::unique_ptr<ImageDownloadHandle>(new ImageDownloadHandle(url, saveAs,
Settings::getInstance()->getInt("ScraperResizeWidth"), Settings::getInstance()->getInt("ScraperResizeHeight")));
}
ImageDownloadHandle::ImageDownloadHandle(const std::string& url, const std::string& path, int maxWidth, int maxHeight) :
mSavePath(path), mMaxWidth(maxWidth), mMaxHeight(maxHeight), mReq(new HttpReq(url))
{
}
void ImageDownloadHandle::update()
{
if(mReq->status() == HttpReq::REQ_IN_PROGRESS)
return;
if(mReq->status() != HttpReq::REQ_SUCCESS)
{
LOG(LogError) << "Failed to open \"" << saveAs << "\" for writing downloaded file.";
return "";
std::stringstream ss;
ss << "Network error: " << mReq->getErrorMsg();
setError(ss.str());
return;
}
std::string content = r->getContent();
// download is done, save it to disk
std::ofstream stream(mSavePath, std::ios_base::out | std::ios_base::binary);
if(stream.bad())
{
setError("Failed to open image path to write. Permission error? Disk full?");
return;
}
const std::string& content = mReq->getContent();
stream.write(content.data(), content.length());
stream.close();
if(stream.bad())
{
setError("Failed to save image. Disk full?");
return;
}
return saveAs;
// resize it
if(!resizeImage(mSavePath, mMaxWidth, mMaxHeight))
{
setError("Error saving resized image. Out of memory? Disk full?");
return;
}
setStatus(ASYNC_DONE);
}
//you can pass 0 for width or height to keep aspect ratio
void resizeImage(const std::string& path, int maxWidth, int maxHeight)
bool resizeImage(const std::string& path, int maxWidth, int maxHeight)
{
// nothing to do
if(maxWidth == 0 && maxHeight == 0)
return;
return true;
FREE_IMAGE_FORMAT format = FIF_UNKNOWN;
FIBITMAP* image = NULL;
@ -62,7 +130,7 @@ void resizeImage(const std::string& path, int maxWidth, int maxHeight)
if(format == FIF_UNKNOWN)
{
LOG(LogError) << "Error - could not detect filetype for image \"" << path << "\"!";
return;
return false;
}
//make sure we can read this filetype first, then load it
@ -71,7 +139,7 @@ void resizeImage(const std::string& path, int maxWidth, int maxHeight)
image = FreeImage_Load(format, path.c_str());
}else{
LOG(LogError) << "Error - file format reading not supported for image \"" << path << "\"!";
return;
return false;
}
float width = (float)FreeImage_GetWidth(image);
@ -91,43 +159,16 @@ void resizeImage(const std::string& path, int maxWidth, int maxHeight)
if(imageRescaled == NULL)
{
LOG(LogError) << "Could not resize image! (not enough memory? invalid bitdepth?)";
return;
}
if(!FreeImage_Save(format, imageRescaled, path.c_str()))
{
LOG(LogError) << "Failed to save resized image!";
return false;
}
bool saved = FreeImage_Save(format, imageRescaled, path.c_str());
FreeImage_Unload(imageRescaled);
}
void downloadImageAsync(Window* window, const std::string& url, const std::string& saveAs, std::function<void(std::string)> returnFunc)
{
std::shared_ptr<HttpReq> httpreq = std::make_shared<HttpReq>(url);
AsyncReqComponent* req = new AsyncReqComponent(window, httpreq,
[returnFunc, saveAs] (std::shared_ptr<HttpReq> r)
{
std::string file = processFileDownload(r, saveAs);
if(!file.empty())
resizeImage(file, Settings::getInstance()->getInt("ScraperResizeWidth"), Settings::getInstance()->getInt("ScraperResizeHeight"));
returnFunc(file);
}, NULL);
if(!saved)
LOG(LogError) << "Failed to save resized image!";
window->pushGui(req);
}
std::string downloadImage(const std::string& url, const std::string& saveAs)
{
std::shared_ptr<HttpReq> httpreq = std::make_shared<HttpReq>(url);
while(httpreq->status() == HttpReq::REQ_IN_PROGRESS);
std::string file = processFileDownload(httpreq, saveAs);
if(!file.empty())
resizeImage(file, Settings::getInstance()->getInt("ScraperResizeWidth"), Settings::getInstance()->getInt("ScraperResizeHeight"));
return file;
return saved;
}
std::string getSaveAsPath(const ScraperSearchParams& params, const std::string& suffix, const std::string& url)
@ -153,42 +194,3 @@ std::string getSaveAsPath(const ScraperSearchParams& params, const std::string&
path += name + ext;
return path;
}
std::shared_ptr<Scraper> createScraperByName(const std::string& name)
{
if(name == "TheGamesDB")
return std::shared_ptr<Scraper>(new GamesDBScraper());
else if(name == "TheArchive")
return std::shared_ptr<Scraper>(new TheArchiveScraper());
return nullptr;
}
void resolveMetaDataAssetsAsync(Window* window, const ScraperSearchParams& params, MetaDataList mdl, std::function<void(MetaDataList)> returnFunc)
{
const std::vector<MetaDataDecl>& mdd = params.game->metadata.getMDD();
for(auto it = mdd.begin(); it != mdd.end(); it++)
{
std::string key = it->key;
std::string val = mdl.get(key);
if(it->type == MD_IMAGE_PATH && HttpReq::isUrl(val))
{
downloadImageAsync(window, val, getSaveAsPath(params, key, val), [window, params, mdl, key, returnFunc] (std::string savedAs) mutable ->
void
{
if(savedAs.empty())
{
//error
LOG(LogError) << "Could not resolve image for [" << getCleanFileName(params.game->getPath()) << "]! Skipping.";
}
mdl.set(key, savedAs);
resolveMetaDataAssetsAsync(window, params, mdl, returnFunc);
});
return;
}
}
returnFunc(mdl);
}

View file

@ -3,11 +3,10 @@
#include "../MetaData.h"
#include "../SystemData.h"
#include "../HttpReq.h"
#include "../AsyncHandle.h"
#include <vector>
#include <functional>
class Window;
struct ScraperSearchParams
{
SystemData* system;
@ -25,34 +24,16 @@ struct ScraperSearchResult
std::string thumbnailUrl;
};
enum ScraperSearchStatus
{
SEARCH_IN_PROGRESS,
SEARCH_ERROR,
SEARCH_DONE
};
class ScraperSearchHandle
class ScraperSearchHandle : public AsyncHandle
{
public:
virtual void update() = 0;
// Update and return the latest status.
inline ScraperSearchStatus status() { update(); return mStatus; }
// User-friendly string of our current status. Will return error message if status() == SEARCH_ERROR.
std::string getStatusString();
inline const std::vector<ScraperSearchResult>& getResults() const { assert(mStatus != SEARCH_IN_PROGRESS); return mResults; }
inline const std::vector<ScraperSearchResult>& getResults() const { assert(mStatus != ASYNC_IN_PROGRESS); return mResults; }
protected:
inline void setError(const std::string& error) { setStatus(SEARCH_ERROR); mError = error; }
inline void setStatus(ScraperSearchStatus status) { mStatus = status; }
inline void setResults(const std::vector<ScraperSearchResult>& results) { mResults = results; }
private:
std::string mError;
ScraperSearchStatus mStatus;
std::vector<ScraperSearchResult> mResults;
};
@ -67,23 +48,48 @@ public:
std::shared_ptr<Scraper> createScraperByName(const std::string& name);
// Meta data asset downloading stuff.
class MDResolveHandle : public AsyncHandle
{
public:
MDResolveHandle(const ScraperSearchResult& result, const ScraperSearchParams& search);
void update() override;
inline const ScraperSearchResult& getResult() const { assert(mStatus == ASYNC_DONE); return mResult; }
private:
ScraperSearchResult mResult;
typedef std::pair< std::unique_ptr<AsyncHandle>, std::function<void()> > ResolvePair;
std::vector<ResolvePair> mFuncs;
};
class ImageDownloadHandle : public AsyncHandle
{
public:
ImageDownloadHandle(const std::string& url, const std::string& path, int maxWidth, int maxHeight);
void update() override;
private:
std::unique_ptr<HttpReq> mReq;
std::string mSavePath;
int mMaxWidth;
int mMaxHeight;
};
//About the same as "~/.emulationstation/downloaded_images/[system_name]/[game_name].[url's extension]".
//Will create the "downloaded_images" and "subdirectory" directories if they do not exist.
std::string getSaveAsPath(const ScraperSearchParams& params, const std::string& suffix, const std::string& url);
//Returns the path to the downloaded file (saveAs) on completion.
//Returns empty string if an error occured.
//Will resize according to Settings::getInt("ScraperResizeWidth") and Settings::getInt("ScraperResizeHeight").
std::string downloadImage(const std::string& url, const std::string& saveAs);
std::unique_ptr<AsyncHandle> downloadImageAsync(const std::string& url, const std::string& saveAs);
//Returns (via returnFunc) the path to the downloaded file (saveAs) on completion.
//Returns empty string if an error occured.
//Will resize according to Settings::getInt("ScraperResizeWidth") and Settings::getInt("ScraperResizeHeight").
//Same as downloadImage, just async.
void downloadImageAsync(Window* window, const std::string& url, const std::string& saveAs, std::function<void(std::string)> returnFunc);
void resolveMetaDataAssetsAsync(Window* window, const ScraperSearchParams& params, MetaDataList mdl, std::function<void(MetaDataList)> returnFunc);
// Resolves all metadata assets that need to be downloaded.
std::unique_ptr<MDResolveHandle> resolveMetaDataAssets(const ScraperSearchResult& result, const ScraperSearchParams& search);
//You can pass 0 for maxWidth or maxHeight to automatically keep the aspect ratio.
//Will overwrite the image at [path] with the new resized one.
void resizeImage(const std::string& path, int maxWidth, int maxHeight);
//Returns true if successful, false otherwise.
bool resizeImage(const std::string& path, int maxWidth, int maxHeight);

View file

@ -25,12 +25,12 @@ std::unique_ptr<ScraperSearchHandle> TheArchiveScraper::getResultsAsync(const Sc
TheArchiveHandle::TheArchiveHandle(const ScraperSearchParams& params, const std::string& url) :
mReq(std::unique_ptr<HttpReq>(new HttpReq(url)))
{
setStatus(SEARCH_IN_PROGRESS);
setStatus(ASYNC_IN_PROGRESS);
}
void TheArchiveHandle::update()
{
if(status() == SEARCH_DONE)
if(mStatus == ASYNC_DONE)
return;
if(mReq->status() == HttpReq::REQ_IN_PROGRESS)
@ -91,6 +91,6 @@ void TheArchiveHandle::update()
game = game.next_sibling("game");
}
setStatus(SEARCH_DONE);
setStatus(ASYNC_DONE);
setResults(results);
}