Both user changes and scraper changes are now color marked in the metadata editor.

User changes are marked with blue and scraper changes with red.
This commit is contained in:
Leon Styhre 2020-07-15 17:44:27 +02:00
parent 1b65eaac2e
commit 70d0057295
15 changed files with 174 additions and 58 deletions

View file

@ -8,7 +8,7 @@ v1.0.0
* Many quality of life improvements and removal of GUI inconsistencies
* New game media file logic using a media directory with files matching the ROM names instead of pointing to the media files in gamelist.xml
* Updated scraper to support additional media files, detailed configuration of what to scrape, semi-automatic mode etc.
* For single-game scraping, any values updated by the scraper are now highlighted using a different font color in the metadata editor
* In the metadata editor, any values updated by the single-game scraper or by the user are now highlighted using a different font color
* Gamelist sorting now working as expected and is persistent throughout the application session
* Full navigation sound support, configurable per theme
* New default theme rbsimple-DE bundled with the software, this theme is largely based on recalbox-multi by the Recalbox community
@ -37,7 +37,7 @@ v1.0.0
### Bug fixes
* Metadata editor insisted that changes were made although nothing was updated
Note: The editor will still ask for save confirmations after automatically rounding fractional game ratings to half-star values
Note: The editor will still ask for save confirmations after automatically rounding fractional game ratings to half-star values, but any time such a rounding has taken place, the rating stars will be colored green in the metadata editor to nofity the user
* Game images were sometimes scaled incorrectly
* Non-transparent favorite icons were not rendered correctly
* Restart and power-off menu entries not working

View file

@ -46,8 +46,7 @@ GuiMetaDataEd::GuiMetaDataEd(
mMetaDataDecl(mdd),
mMetaData(md),
mSavedCallback(saveCallback),
mDeleteFunc(deleteFunc),
mMetadataUpdated(false)
mDeleteFunc(deleteFunc)
{
addChild(&mBackground);
addChild(&mGrid);
@ -70,6 +69,7 @@ GuiMetaDataEd::GuiMetaDataEd(
// Populate list.
for (auto iter = mdd.cbegin(); iter != mdd.cend(); iter++) {
std::shared_ptr<GuiComponent> ed;
std::string originalValue;
// Don't add statistics.
if (iter->isStatistic)
@ -99,6 +99,7 @@ GuiMetaDataEd::GuiMetaDataEd(
switch (iter->type) {
case MD_BOOL: {
ed = std::make_shared<SwitchComponent>(window);
ed->setChangedColor(ICONCOLOR_USERMARKED);
row.addElement(ed, false, true);
break;
}
@ -107,7 +108,8 @@ GuiMetaDataEd::GuiMetaDataEd(
spacer->setSize(Renderer::getScreenWidth() * 0.0025f, 0);
row.addElement(spacer, false);
ed = std::make_shared<RatingComponent>(window);
ed = std::make_shared<RatingComponent>(window, true);
ed->setChangedColor(ICONCOLOR_USERMARKED);
const float height = lbl->getSize().y() * 0.71f;
ed->setSize(0, height);
row.addElement(ed, false, true);
@ -123,6 +125,7 @@ GuiMetaDataEd::GuiMetaDataEd(
row.addElement(spacer, false);
ed = std::make_shared<DateTimeEditComponent>(window);
ed->setChangedColor(TEXTCOLOR_USERMARKED);
row.addElement(ed, false);
// Pass input to the actual DateTimeEditComponent instead of the spacer.
@ -130,12 +133,14 @@ GuiMetaDataEd::GuiMetaDataEd(
std::placeholders::_1, std::placeholders::_2);
break;
}
case MD_TIME: {
ed = std::make_shared<DateTimeEditComponent>(window,
DateTimeEditComponent::DISP_RELATIVE_TO_NOW);
row.addElement(ed, false);
break;
}
// Not in use as 'lastplayed' is flagged as statistics and these are skipped.
// Let's still keep the code because it may be needed in the future.
// case MD_TIME: {
// ed = std::make_shared<DateTimeEditComponent>(window,
// DateTimeEditComponent::DISP_RELATIVE_TO_NOW);
// row.addElement(ed, false);
// break;
// }
case MD_LAUNCHCOMMAND: {
ed = std::make_shared<TextComponent>(window, "",
Font::get(FONT_SIZE_SMALL, FONT_PATH_LIGHT), 0x777777FF, ALIGN_RIGHT);
@ -152,8 +157,17 @@ GuiMetaDataEd::GuiMetaDataEd(
bool multiLine = false;
const std::string title = iter->displayPrompt;
auto updateVal = [ed](const std::string& newVal) {
ed->setValue(newVal); }; // OK callback (apply new value to ed).
originalValue = mMetaData->get(iter->key);
// OK callback (apply new value to ed).
auto updateVal = [ed, originalValue](const std::string& newVal) {
ed->setValue(newVal);
if (newVal == originalValue)
ed->setColor(DEFAULT_TEXTCOLOR);
else
ed->setColor(TEXTCOLOR_USERMARKED);
};
std::string staticTextString = "Default value from es_systems.cfg:";
std::string defaultLaunchCommand = scraperParams.system->
@ -186,8 +200,17 @@ GuiMetaDataEd::GuiMetaDataEd(
bool multiLine = iter->type == MD_MULTILINE_STRING;
const std::string title = iter->displayPrompt;
originalValue = mMetaData->get(iter->key);
// OK callback (apply new value to ed).
auto updateVal = [ed](const std::string& newVal) { ed->setValue(newVal); };
auto updateVal = [ed, originalValue](const std::string& newVal) {
ed->setValue(newVal);
if (newVal == originalValue)
ed->setColor(DEFAULT_TEXTCOLOR);
else
ed->setColor(TEXTCOLOR_USERMARKED);
};
row.makeAcceptInputHandler([this, title, ed, updateVal, multiLine] {
mWindow->pushGui(new GuiTextEditPopup(mWindow, getHelpStyle(), title,
ed->getValue(), updateVal, multiLine, "APPLY", "APPLY CHANGES?"));
@ -217,8 +240,8 @@ GuiMetaDataEd::GuiMetaDataEd(
if (mDeleteFunc) {
auto deleteFileAndSelf = [&] { mDeleteFunc(); delete this; };
auto deleteBtnFunc = [this, deleteFileAndSelf] { mWindow->pushGui(
new GuiMsgBox(mWindow, getHelpStyle(),
auto deleteBtnFunc = [this, deleteFileAndSelf] {
mWindow->pushGui(new GuiMsgBox(mWindow, getHelpStyle(),
"THIS WILL DELETE THE ACTUAL GAME FILE(S)!\nARE YOU SURE?",
"YES", deleteFileAndSelf, "NO", nullptr)); };
buttons.push_back(std::make_shared<ButtonComponent>(mWindow, "DELETE",
@ -303,19 +326,20 @@ void GuiMetaDataEd::fetchDone(const ScraperSearchResult& result)
metadata->set(key, mEditors[i]->getValue());
}
mMetadataUpdated = GuiScraperSearch::saveMetadata(result, *metadata);
GuiScraperSearch::saveMetadata(result, *metadata);
// Update the list with the scraped metadata values.
for (unsigned int i = 0; i < mEditors.size(); i++) {
const std::string& key = mMetaDataDecl.at(i).key;
if (mEditors.at(i)->getValue() != metadata->get(key)) {
if (key == "rating") {
mEditors.at(i)->setColorShift(0xDD2222FF);
}
else {
mEditors.at(i)->setColor(0x994444FF);
}
if (key == "rating")
mEditors.at(i)->setOriginalColor(ICONCOLOR_SCRAPERMARKED);
else
mEditors.at(i)->setColor(TEXTCOLOR_SCRAPERMARKED);
}
// Save all the keys, except the following which can't be scraped.
if (key != "favorite" && key != "completed" && key != "broken" &&
key != "hidden" && key != "kidgame")
mEditors.at(i)->setValue(metadata->get(key));
}
@ -325,7 +349,7 @@ void GuiMetaDataEd::fetchDone(const ScraperSearchResult& result)
void GuiMetaDataEd::close()
{
// Find out if the user made any changes.
bool dirty = mMetadataUpdated;
bool metadataUpdated = false;
for (unsigned int i = 0; i < mEditors.size(); i++) {
const std::string& key = mMetaDataDecl.at(i).key;
std::string mMetaDataValue = mMetaData->get(key);
@ -337,7 +361,7 @@ void GuiMetaDataEd::close()
mMetaDataValue = "19700101T010000";
if (mMetaDataValue != mEditorsValue) {
dirty = true;
metadataUpdated = true;
break;
}
}
@ -358,7 +382,7 @@ void GuiMetaDataEd::close()
std::function<void()> closeFunc;
closeFunc = [this] { delete this; };
if (dirty) {
if (metadataUpdated) {
// Changes were made, ask if the user wants to save them.
mWindow->pushGui(new GuiMsgBox(mWindow, getHelpStyle(),
"SAVE CHANGES?",

View file

@ -61,7 +61,6 @@ private:
std::function<void()> mSavedCallback;
std::function<void()> mDeleteFunc;
bool mMetadataUpdated;
bool mMediaFilesUpdated;
};

View file

@ -16,6 +16,14 @@
#include <functional>
#include <memory>
#define DEFAULT_TEXTCOLOR 0x777777FF
#define DEFAULT_INVERTED_TEXTCOLOR 0x444444FF
#define DEFAULT_COLORSHIFT 0xFFFFFFFF
#define ICONCOLOR_SCRAPERMARKED 0xFF5555FF
#define ICONCOLOR_USERMARKED 0x5555FFFF
#define TEXTCOLOR_SCRAPERMARKED 0x992222FF
#define TEXTCOLOR_USERMARKED 0x222299FF
class Animation;
class AnimationController;
class Font;
@ -132,6 +140,8 @@ public:
virtual void setOpacity(unsigned char opacity);
virtual void setColor(unsigned int color);
virtual void setColorShift(unsigned int color);
virtual void setOriginalColor(unsigned int color) { mColorOriginalValue = color; };
virtual void setChangedColor(unsigned int color) { mColorChangedValue = color; };
virtual unsigned int getColor() const;
const Transform4x4f& getTransform();
@ -177,6 +187,9 @@ protected:
unsigned char mColorOpacity;
unsigned int mColorShift;
unsigned int mColorShiftEnd;
unsigned int mColorOriginalValue;
unsigned int mColorChangedValue;
Window* mWindow;
GuiComponent* mParent;

View file

@ -203,15 +203,20 @@ void ComponentList::render(const Transform4x4f& parentTrans)
it->component->render(trans);
}
else {
// If there is a hue, average the brightness values to make
// an equivalent gray value before inverting the text.
// This is not the proper way to do a BW conversion as the RGB values
// should not be evenly distributed, but it's definitely good enough
// for this situation.
unsigned char byteAverage = (byteRed + byteGreen + byteBlue) / 3;
unsigned int averageColor = byteAverage << 24 | byteAverage << 16 |
byteAverage << 8 | 0xFF;
it->component->setColor(averageColor);
// Note: I've disabled this code as it's overly complicated,
// instead we're now using a simple constant which should be
// good enough. Let's keep the code though if needed in the
// future for some reason.
// // If there is a hue, average the brightness values to make
// // an equivalent gray value before inverting the text.
// // This is not the proper way to do a BW conversion as the RGB values
// // should not be evenly distributed, but it's definitely good enough
// // for this situation.
// unsigned char byteAverage = (byteRed + byteGreen + byteBlue) / 3;
// unsigned int averageColor = byteAverage << 24 | byteAverage << 16 |
// byteAverage << 8 | 0xFF;
// it->component->setColor(averageColor);
it->component->setColor(DEFAULT_INVERTED_TEXTCOLOR);
it->component->render(trans);
// Revert to the original color after rendering.
it->component->setColor(origColor);
@ -221,10 +226,11 @@ void ComponentList::render(const Transform4x4f& parentTrans)
it->component->render(trans);
}
}
else
else {
drawAfterCursor.push_back(it->component.get());
}
}
}
// Custom rendering.
Renderer::setMatrix(trans);

View file

@ -41,7 +41,7 @@ struct ComponentListRow
elements.push_back(ComponentListElement(component, resize_width, invert_when_selected));
}
// Utility method for making an input handler for "when the users presses A on this, do func".
// Utility function for making an input handler for "when the users presses A on this, do func".
inline void makeAcceptInputHandler(const std::function<void()>& func)
{
input_handler = [func](InputConfig* config, Input input) -> bool {

View file

@ -107,6 +107,12 @@ bool DateTimeEditComponent::input(InputConfig* config, Input input)
mTime = new_tm;
// Change the color of the text to reflect the changes.
if (mTime == mOriginalValue)
setColor(mColorOriginalValue);
else
setColor(mColorChangedValue);
updateTextCache();
return true;
}
@ -170,6 +176,7 @@ void DateTimeEditComponent::render(const Transform4x4f& parentTrans)
void DateTimeEditComponent::setValue(const std::string& val)
{
mTime = val;
mOriginalValue = val;
updateTextCache();
}

View file

@ -46,6 +46,10 @@ public:
// Text color.
void setColor(unsigned int color) override;
// Font to use. Default is Font::get(FONT_SIZE_MEDIUM).
void setOriginalColor(unsigned int color) override { mColorOriginalValue = color; };
void setChangedColor(unsigned int color) override { mColorChangedValue = color; };
void setFont(std::shared_ptr<Font> font);
// Force text to be uppercase when in DISP_RELATIVE_TO_NOW mode.
void setUppercase(bool uppercase);
@ -76,6 +80,10 @@ private:
std::vector<Vector4f> mCursorBoxes;
unsigned int mColor;
Utils::Time::DateTime mOriginalValue;
unsigned int mColorOriginalValue;
unsigned int mColorChangedValue;
std::shared_ptr<Font> mFont;
bool mUppercase;
bool mAutoSize;

View file

@ -363,7 +363,6 @@ void ImageComponent::render(const Transform4x4f& parentTrans)
// 'jump' in when it finally loads.
fadeIn(mTexture->bind());
Renderer::drawTriangleStrips(&mVertices[0], 4);
}
else {
LOG(LogError) << "Image texture is not initialized!";

View file

@ -11,8 +11,16 @@
#include "Settings.h"
#include "ThemeData.h"
RatingComponent::RatingComponent(Window* window) : GuiComponent(window),
mColorShift(0xFFFFFFFF), mColorShiftEnd(0xFFFFFFFF), mUnfilledColor(0xFFFFFFFF)
RatingComponent::RatingComponent(
Window* window,
bool colorizeChanges)
: GuiComponent(window),
mColorShift(DEFAULT_COLORSHIFT),
mColorShiftEnd(DEFAULT_COLORSHIFT),
mUnfilledColor(DEFAULT_COLORSHIFT),
mColorizeChanges(colorizeChanges),
mColorOriginalValue(DEFAULT_COLORSHIFT),
mColorChangedValue(DEFAULT_COLORSHIFT)
{
mFilledTexture = TextureResource::get(":/graphics/star_filled.svg", true);
mUnfilledTexture = TextureResource::get(":/graphics/star_unfilled.svg", true);
@ -30,6 +38,26 @@ void RatingComponent::setValue(const std::string& value)
else {
// Round up to the closest .1 value, i.e. to the closest half-icon.
mValue = Math::ceilf(stof(value) / 0.1) / 10;
mOriginalValue = static_cast<int>(mValue * 10);
// If the argument to colorize the rating icons has been passed, set the
// color shift accordingly.
if (mColorizeChanges) {
if (static_cast<int>(mValue * 10) == mOriginalValue)
setColorShift(mColorOriginalValue);
else
setColorShift(mColorChangedValue);
}
// For the special situation where there is a fractional rating in the gamelist.xml
// file that has been rounded to a half-star rating, render the rating icons green.
// This should only happen if an external scraper has been used or if the file has
// been manually edited.
if (mColorizeChanges && mValue != stof(value)) {
mOriginalValue = ICONCOLOR_USERMARKED;
setColorShift(0x449944FF);
}
if (mValue > 1.0f)
mValue = 1.0f;
else if (mValue < 0.0f)
@ -102,9 +130,10 @@ void RatingComponent::updateVertices()
mVertices[6] = { { fw, 0.0f }, { numStars, 1.0f }, color };
mVertices[7] = { { fw, h }, { numStars, 0.0f }, color };
// Round vertices.
// Disabled as it caused subtle but strange rendering errors where
// the icons changed size slightly when changing rating scores.
// Disabled this code as it caused subtle but strange rendering errors
// where the icons changed size slightly when changing rating scores.
// // Round vertices.
// for (int i = 0; i < 8; ++i)
// mVertices[i].pos.round();
}
@ -162,6 +191,14 @@ bool RatingComponent::input(InputConfig* config, Input input)
if (mValue > 1.05f)
mValue = 0.0f;
// If the argument to colorize the rating icons has been passed,
// set the color shift accordingly.
if (mColorizeChanges) {
if (static_cast<int>(mValue * 10) == mOriginalValue)
setColorShift(mColorOriginalValue);
else
setColorShift(mColorChangedValue);
}
updateVertices();
}

View file

@ -24,7 +24,7 @@ class TextureResource;
class RatingComponent : public GuiComponent
{
public:
RatingComponent(Window* window);
RatingComponent(Window* window, bool colorizeChanges = false);
std::string getValue() const override;
// Should be a normalized float (in the range [0..1]) - if it's not, it will be clamped.
@ -40,6 +40,9 @@ public:
// Multiply all pixels in the image by this color when rendering.
void setColorShift(unsigned int color) override;
void setOriginalColor(unsigned int color) override { mColorOriginalValue = color; };
void setChangedColor(unsigned int color) override { mColorChangedValue = color; };
virtual void applyTheme(const std::shared_ptr<ThemeData>& theme, const std::string& view,
const std::string& element, unsigned int properties) override;
@ -52,6 +55,9 @@ private:
void updateColors();
float mValue;
int mOriginalValue;
unsigned int mColorOriginalValue;
unsigned int mColorChangedValue;
Renderer::Vertex mVertices[8];
@ -61,6 +67,8 @@ private:
std::shared_ptr<TextureResource> mFilledTexture;
std::shared_ptr<TextureResource> mUnfilledTexture;
bool mColorizeChanges;
};
#endif // ES_APP_COMPONENTS_RATING_COMPONENT_H

View file

@ -13,7 +13,9 @@ SwitchComponent::SwitchComponent(
bool state)
: GuiComponent(window),
mImage(window),
mState(state)
mState(state),
mColorOriginalValue(DEFAULT_COLORSHIFT),
mColorChangedValue(DEFAULT_COLORSHIFT)
{
mImage.setImage(":/graphics/off.svg");
mImage.setResize(0, Font::get(FONT_SIZE_MEDIUM)->getLetterHeight());
@ -65,12 +67,20 @@ void SwitchComponent::setValue(const std::string& statestring)
mState = true;
else
mState = false;
mOriginalValue = mState;
onStateChanged();
}
void SwitchComponent::onStateChanged()
{
mImage.setImage(mState ? ":/graphics/on.svg" : ":/graphics/off.svg");
// Change the color of the switch to reflect the changes.
if (mState == mOriginalValue)
mImage.setColorShift(mColorOriginalValue);
else
mImage.setColorShift(mColorChangedValue);
}
std::vector<HelpPrompt> SwitchComponent::getHelpPrompts()

View file

@ -26,6 +26,9 @@ public:
std::string getValue() const override;
void setValue(const std::string& statestring) override;
void setOriginalColor(unsigned int color) override { mColorOriginalValue = color; };
void setChangedColor(unsigned int color) override { mColorChangedValue = color; };
virtual std::vector<HelpPrompt> getHelpPrompts() override;
private:
@ -33,6 +36,9 @@ private:
ImageComponent mImage;
bool mState;
bool mOriginalValue;
unsigned int mColorOriginalValue;
unsigned int mColorChangedValue;
};
#endif // ES_CORE_COMPONENTS_SWITCH_COMPONENT_H

View file

@ -88,10 +88,9 @@ void TextComponent::setRenderBackground(bool render)
// Scale the opacity.
void TextComponent::setOpacity(unsigned char opacity)
{
// This method is mostly called to do fading in-out of the Text component element.
// This function is mostly called to do fading in-out of the Text component element.
// Therefore, we assume here that opacity is a fractional value (expressed as an int 0-255),
// of the opacity originally set with setColor() or setBackgroundColor().
unsigned char o = (unsigned char)((float)opacity / 255.f * (float) mColorOpacity);
mColor = (mColor & 0xFFFFFF00) | (unsigned char) o;