diff --git a/NEWS.md b/NEWS.md index b5d939404..2ac61ee5a 100644 --- a/NEWS.md +++ b/NEWS.md @@ -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 diff --git a/es-app/src/guis/GuiMetaDataEd.cpp b/es-app/src/guis/GuiMetaDataEd.cpp index 46eb93643..014091583 100644 --- a/es-app/src/guis/GuiMetaDataEd.cpp +++ b/es-app/src/guis/GuiMetaDataEd.cpp @@ -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 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(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(window); + ed = std::make_shared(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(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(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(window, +// DateTimeEditComponent::DISP_RELATIVE_TO_NOW); +// row.addElement(ed, false); +// break; +// } case MD_LAUNCHCOMMAND: { ed = std::make_shared(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?")); @@ -202,7 +225,7 @@ GuiMetaDataEd::GuiMetaDataEd( mEditors.push_back(ed); } - std::vector< std::shared_ptr > buttons; + std::vector> buttons; if (mScraperParams.game->getType() != FOLDER) { if (!scraperParams.system->hasPlatformId(PlatformIds::PLATFORM_IGNORE)) @@ -217,10 +240,10 @@ GuiMetaDataEd::GuiMetaDataEd( if (mDeleteFunc) { auto deleteFileAndSelf = [&] { mDeleteFunc(); delete this; }; - 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)); }; + 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(mWindow, "DELETE", "delete game", deleteBtnFunc)); } @@ -303,20 +326,21 @@ 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); } - mEditors.at(i)->setValue(metadata->get(key)); + // 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)); } delete metadata; @@ -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 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?", diff --git a/es-app/src/guis/GuiMetaDataEd.h b/es-app/src/guis/GuiMetaDataEd.h index 00d1a25b7..e93b79c66 100644 --- a/es-app/src/guis/GuiMetaDataEd.h +++ b/es-app/src/guis/GuiMetaDataEd.h @@ -54,14 +54,13 @@ private: ScraperSearchParams mScraperParams; - std::vector< std::shared_ptr > mEditors; + std::vector> mEditors; std::vector mMetaDataDecl; MetaDataList* mMetaData; std::function mSavedCallback; std::function mDeleteFunc; - bool mMetadataUpdated; bool mMediaFilesUpdated; }; diff --git a/es-core/src/GuiComponent.h b/es-core/src/GuiComponent.h index e6d8ab2a1..fd5bf4dfd 100644 --- a/es-core/src/GuiComponent.h +++ b/es-core/src/GuiComponent.h @@ -16,6 +16,14 @@ #include #include +#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; diff --git a/es-core/src/components/ComponentList.cpp b/es-core/src/components/ComponentList.cpp index 0df4c961c..a5e611eb4 100644 --- a/es-core/src/components/ComponentList.cpp +++ b/es-core/src/components/ComponentList.cpp @@ -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,8 +226,9 @@ void ComponentList::render(const Transform4x4f& parentTrans) it->component->render(trans); } } - else + else { drawAfterCursor.push_back(it->component.get()); + } } } diff --git a/es-core/src/components/ComponentList.h b/es-core/src/components/ComponentList.h index a33046211..590a73b4b 100644 --- a/es-core/src/components/ComponentList.h +++ b/es-core/src/components/ComponentList.h @@ -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& func) { input_handler = [func](InputConfig* config, Input input) -> bool { diff --git a/es-core/src/components/DateTimeEditComponent.cpp b/es-core/src/components/DateTimeEditComponent.cpp index 577df8d3f..03dd0abff 100644 --- a/es-core/src/components/DateTimeEditComponent.cpp +++ b/es-core/src/components/DateTimeEditComponent.cpp @@ -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(); } diff --git a/es-core/src/components/DateTimeEditComponent.h b/es-core/src/components/DateTimeEditComponent.h index a8aadca8f..8dce7a1a7 100644 --- a/es-core/src/components/DateTimeEditComponent.h +++ b/es-core/src/components/DateTimeEditComponent.h @@ -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); // Force text to be uppercase when in DISP_RELATIVE_TO_NOW mode. void setUppercase(bool uppercase); @@ -76,6 +80,10 @@ private: std::vector mCursorBoxes; unsigned int mColor; + Utils::Time::DateTime mOriginalValue; + unsigned int mColorOriginalValue; + unsigned int mColorChangedValue; + std::shared_ptr mFont; bool mUppercase; bool mAutoSize; diff --git a/es-core/src/components/ImageComponent.cpp b/es-core/src/components/ImageComponent.cpp index 5eb9dd343..8ce5693e7 100644 --- a/es-core/src/components/ImageComponent.cpp +++ b/es-core/src/components/ImageComponent.cpp @@ -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!"; diff --git a/es-core/src/components/RatingComponent.cpp b/es-core/src/components/RatingComponent.cpp index faa4c48ab..2a18a6a64 100644 --- a/es-core/src/components/RatingComponent.cpp +++ b/es-core/src/components/RatingComponent.cpp @@ -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(mValue * 10); + + // If the argument to colorize the rating icons has been passed, set the + // color shift accordingly. + if (mColorizeChanges) { + if (static_cast(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,11 +130,12 @@ 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. -// for (int i = 0; i < 8; ++i) -// mVertices[i].pos.round(); + +// 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(); } void RatingComponent::updateColors() @@ -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(mValue * 10) == mOriginalValue) + setColorShift(mColorOriginalValue); + else + setColorShift(mColorChangedValue); + } updateVertices(); } diff --git a/es-core/src/components/RatingComponent.h b/es-core/src/components/RatingComponent.h index 29f433e19..260b81c97 100644 --- a/es-core/src/components/RatingComponent.h +++ b/es-core/src/components/RatingComponent.h @@ -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& 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 mFilledTexture; std::shared_ptr mUnfilledTexture; + + bool mColorizeChanges; }; #endif // ES_APP_COMPONENTS_RATING_COMPONENT_H diff --git a/es-core/src/components/SwitchComponent.cpp b/es-core/src/components/SwitchComponent.cpp index 65dacc918..e923f1c11 100644 --- a/es-core/src/components/SwitchComponent.cpp +++ b/es-core/src/components/SwitchComponent.cpp @@ -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 SwitchComponent::getHelpPrompts() diff --git a/es-core/src/components/SwitchComponent.h b/es-core/src/components/SwitchComponent.h index c8c3a0098..dc977ac64 100644 --- a/es-core/src/components/SwitchComponent.h +++ b/es-core/src/components/SwitchComponent.h @@ -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 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 diff --git a/es-core/src/components/TextComponent.cpp b/es-core/src/components/TextComponent.cpp index e5a78806a..251a21558 100644 --- a/es-core/src/components/TextComponent.cpp +++ b/es-core/src/components/TextComponent.cpp @@ -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; diff --git a/es-core/src/resources/Font.cpp b/es-core/src/resources/Font.cpp index 69a077b17..a99ab997c 100644 --- a/es-core/src/resources/Font.cpp +++ b/es-core/src/resources/Font.cpp @@ -552,7 +552,7 @@ TextCache* Font::buildTextCache( float y = offset[1] + (yBot + yTop)/2.0f; // Vertices by texture. - std::map< FontTexture*, std::vector > vertMap; + std::map> vertMap; size_t cursor = 0; while (cursor < text.length()) { @@ -581,8 +581,8 @@ TextCache* Font::buildTextCache( verts.resize(oldVertSize + 6); Renderer::Vertex* vertices = verts.data() + oldVertSize; - const float glyphStartX = x + glyph->bearing.x(); - const Vector2i& textureSize = glyph->texture->textureSize; + const float glyphStartX = x + glyph->bearing.x(); + const Vector2i& textureSize = glyph->texture->textureSize; const unsigned int convertedColor = Renderer::convertColor(color); vertices[1] = {