// SPDX-License-Identifier: MIT // // ES-DE Frontend // GuiMetaDataEd.cpp // // Game metadata edit user interface. // This interface is triggered from the GuiGamelistOptions menu. // The scraping interface is handled by GuiScraperSingle which calls GuiScraperSearch. // #include "guis/GuiMetaDataEd.h" #include "CollectionSystemsManager.h" #include "FileData.h" #include "FileFilterIndex.h" #include "GamelistFileParser.h" #include "MameNames.h" #include "SystemData.h" #include "Window.h" #include "components/ButtonComponent.h" #include "components/ComponentList.h" #include "components/DateTimeEditComponent.h" #include "components/MenuComponent.h" #include "components/RatingComponent.h" #include "components/SwitchComponent.h" #include "components/TextComponent.h" #include "guis/GuiMsgBox.h" #include "guis/GuiScraperSingle.h" #include "guis/GuiTextEditKeyboardPopup.h" #include "guis/GuiTextEditPopup.h" #include "resources/Font.h" #include "utils/LocalizationUtil.h" #include "utils/StringUtil.h" #define TITLE_HEIGHT \ (mTitle->getFont()->getLetterHeight() + (Renderer::getIsVerticalOrientation() ? \ Renderer::getScreenWidth() * 0.060f : \ Renderer::getScreenHeight() * 0.060f)) GuiMetaDataEd::GuiMetaDataEd(MetaDataList* md, const std::vector& mdd, const ScraperSearchParams scraperParams, std::function saveCallback, std::function clearGameFunc, std::function deleteGameFunc) : mRenderer {Renderer::getInstance()} , mBackground {":/graphics/frame.svg"} , mGrid {glm::ivec2 {2, 6}} , mScraperParams {scraperParams} , mControllerBadges {BadgeComponent::getGameControllers()} , mMetaDataDecl {mdd} , mMetaData {md} , mSavedCallback {saveCallback} , mClearGameFunc {clearGameFunc} , mDeleteGameFunc {deleteGameFunc} , mIsCustomCollection {false} , mMediaFilesUpdated {false} , mSavedMediaAndAborted {false} , mInvalidEmulatorEntry {false} , mInvalidFolderLinkEntry {false} { if (ViewController::getInstance()->getState().getSystem()->isCustomCollection() || ViewController::getInstance()->getState().getSystem()->getThemeFolder() == "custom-collections") mIsCustomCollection = true; // Remove the last "unknown" controller entry. if (mControllerBadges.size() > 1) mControllerBadges.pop_back(); addChild(&mBackground); addChild(&mGrid); mTitle = std::make_shared( _("EDIT METADATA"), Font::get(FONT_SIZE_LARGE * Utils::Localization::sMenuTitleScaleFactor), mMenuColorTitle, ALIGN_CENTER); mGrid.setEntry(mTitle, glm::ivec2 {0, 0}, false, true, glm::ivec2 {2, 2}); // Extract possible subfolders from the path. std::string folderPath { Utils::String::replace(Utils::FileSystem::getParent(scraperParams.game->getPath()), scraperParams.system->getSystemEnvData()->mStartPath, "")}; if (folderPath.size() >= 2) { folderPath.erase(0, 1); #if defined(_WIN64) folderPath.push_back('\\'); folderPath = Utils::String::replace(folderPath, "/", "\\"); #else folderPath.push_back('/'); #endif } mSubtitle = std::make_shared( folderPath + Utils::FileSystem::getFileName(scraperParams.game->getPath()) + " [" + Utils::String::toUpper(scraperParams.system->getName()) + "]" + (scraperParams.game->getType() == FOLDER ? " " + ViewController::FOLDER_CHAR : ""), Font::get(FONT_SIZE_SMALL), mMenuColorPrimary, ALIGN_CENTER); mGrid.setEntry(mSubtitle, glm::ivec2 {0, 2}, false, true, glm::ivec2 {2, 1}); mList = std::make_shared(); mList->setRowHeight(std::round(Font::get(FONT_SIZE_SMALL)->getHeight())); mGrid.setEntry(mList, glm::ivec2 {0, 4}, true, true, glm::ivec2 {2, 1}); // Set up scroll indicators. mScrollUp = std::make_shared(); mScrollDown = std::make_shared(); mScrollUp->setResize(0.0f, mTitle->getFont()->getLetterHeight() / 2.0f); mScrollUp->setOrigin(0.0f, -0.35f); mScrollDown->setResize(0.0f, mTitle->getFont()->getLetterHeight() / 2.0f); mScrollDown->setOrigin(0.0f, 0.35f); mScrollIndicator = std::make_shared(mList, mScrollUp, mScrollDown); mGrid.setEntry(mScrollUp, glm::ivec2 {1, 0}, false, false, glm::ivec2 {1, 1}); mGrid.setEntry(mScrollDown, glm::ivec2 {1, 1}, false, false, glm::ivec2 {1, 1}); // Populate list. for (auto it = mdd.cbegin(); it != mdd.cend(); ++it) { std::shared_ptr ed; std::string currentKey {it->key}; std::string originalValue {mMetaData->get(it->key)}; std::string gamePath; // Only display the custom collections sortname entry if we're editing the game // from within a custom collection. if (currentKey == "collectionsortname" && !mIsCustomCollection) continue; // Don't add statistics. if (it->isStatistic) continue; // Don't show the alternative emulator entry if the corresponding option has been disabled. if (!Settings::getInstance()->getBool("AlternativeEmulatorPerGame") && it->type == MD_ALT_EMULATOR) { ed = std::make_shared("", Font::get(FONT_SIZE_SMALL, FONT_PATH_LIGHT), mMenuColorPrimary, ALIGN_RIGHT); ed->setSize(0.0f, ed->getFont()->getHeight()); ed->setValue(mMetaData->get(it->key)); mEditors.push_back(ed); continue; } // It's very important to put the element with the help prompt as the last row // entry instead of for instance the spacer. That is so because ComponentList // always looks for the help prompt at the back of the element stack. ComponentListRow row; auto lbl = std::make_shared(_p("metadata", it->displayName.c_str()), Font::get(FONT_SIZE_SMALL), mMenuColorPrimary); row.addElement(lbl, true); // Label. switch (it->type) { case MD_BOOL: { ed = std::make_shared(); // Make the switches slightly smaller. ed->setSize(glm::ceil(ed->getSize() * 0.9f)); ed->setChangedColor(mMenuColorBlue); row.addElement(ed, false, true); break; } case MD_RATING: { auto spacer = std::make_shared(); spacer->setSize(mRenderer->getScreenWidth() * 0.0025f, 0.0f); row.addElement(spacer, false); ed = std::make_shared(true, true); ed->setChangedColor(mMenuColorBlue); const float height {lbl->getSize().y * 0.71f}; ed->setSize(0.0f, height); row.addElement(ed, false, true); // Pass input to the actual RatingComponent instead of the spacer. row.inputHandler = std::bind(&GuiComponent::input, ed.get(), std::placeholders::_1, std::placeholders::_2); break; } case MD_DATE: { auto spacer = std::make_shared(); spacer->setSize(mRenderer->getScreenWidth() * 0.0025f, 0.0f); row.addElement(spacer, false); ed = std::make_shared(true); ed->setOriginalColor(mMenuColorPrimary); ed->setChangedColor(mMenuColorBlue); row.addElement(ed, false); // Pass input to the actual DateTimeEditComponent instead of the spacer. row.inputHandler = std::bind(&GuiComponent::input, ed.get(), std::placeholders::_1, std::placeholders::_2); break; } case MD_CONTROLLER: { ed = std::make_shared("", Font::get(FONT_SIZE_SMALL, FONT_PATH_LIGHT), mMenuColorPrimary, ALIGN_RIGHT); ed->setSize(0.0f, ed->getFont()->getHeight()); row.addElement(ed, true); auto spacer = std::make_shared(); spacer->setSize(mRenderer->getScreenWidth() * 0.005f, 0.0f); row.addElement(spacer, false); auto bracket = std::make_shared(); bracket->setResize(glm::vec2 {0.0f, lbl->getFont()->getLetterHeight()}); bracket->setImage(":/graphics/arrow.svg"); bracket->setColorShift(mMenuColorPrimary); row.addElement(bracket, false); const std::string title {_p("metadata", it->displayPrompt.c_str())}; // OK callback (apply new value to ed). auto updateVal = [ed, originalValue](const std::string& newVal) { ed->setValue(newVal); if (newVal == BadgeComponent::getDisplayName(originalValue)) ed->setColor(mMenuColorPrimary); else ed->setColor(mMenuColorBlue); }; row.makeAcceptInputHandler([this, title, ed, updateVal] { GuiSettings* s {new GuiSettings(title)}; for (auto controller : mControllerBadges) { std::string selectedLabel {ed->getValue()}; std::string label; ComponentListRow row; std::shared_ptr labelText {std::make_shared( label, Font::get(FONT_SIZE_MEDIUM), mMenuColorPrimary)}; labelText->setSelectable(true); labelText->setValue(controller.displayName); label = controller.displayName; row.addElement(labelText, true); row.makeAcceptInputHandler([s, updateVal, controller] { updateVal(controller.displayName); delete s; }); // Select the row that corresponds to the selected label. if (selectedLabel == label) s->addRow(row, true); else s->addRow(row, false); } // If a value is set, then display "Clear entry" as the last entry. if (ed->getValue() != "") { ComponentListRow row; std::shared_ptr clearText {std::make_shared( ViewController::CROSSEDCIRCLE_CHAR + " " + _("CLEAR ENTRY"), Font::get(FONT_SIZE_MEDIUM), mMenuColorPrimary)}; clearText->setSelectable(true); row.addElement(clearText, true); row.makeAcceptInputHandler([s, ed] { ed->setValue(""); delete s; }); s->addRow(row, false); } const float aspectValue {1.778f / mRenderer->getScreenAspectRatio()}; const float maxWidthModifier { glm::clamp(0.64f * aspectValue, 0.42f, (mRenderer->getIsVerticalOrientation() ? 0.95f : 0.92f))}; const float maxWidth {mRenderer->getScreenWidth() * maxWidthModifier}; s->setMenuSize(glm::vec2 {maxWidth, s->getMenuSize().y}); s->setMenuPosition( glm::vec3 {(s->getSize().x - maxWidth) / 2.0f, mPosition.y, mPosition.z}); mWindow->pushGui(s); }); break; } case MD_ALT_EMULATOR: { mInvalidEmulatorEntry = false; ed = std::make_shared("", Font::get(FONT_SIZE_SMALL, FONT_PATH_LIGHT), mMenuColorPrimary, ALIGN_RIGHT); ed->setSize(0.0f, ed->getFont()->getHeight()); row.addElement(ed, true); auto spacer = std::make_shared(); spacer->setSize(mRenderer->getScreenWidth() * 0.005f, 0.0f); row.addElement(spacer, false); auto bracket = std::make_shared(); bracket->setResize(glm::vec2 {0.0f, lbl->getFont()->getLetterHeight()}); bracket->setImage(":/graphics/arrow.svg"); bracket->setColorShift(mMenuColorPrimary); row.addElement(bracket, false); const std::string title {mRenderer->getIsVerticalOrientation() ? _("SELECT EMULATOR") : _p("metadata", it->displayPrompt.c_str())}; // OK callback (apply new value to ed). auto updateVal = [this, ed, originalValue](const std::string& newVal) { ed->setValue(newVal); if (newVal == originalValue) { ed->setColor(mMenuColorPrimary); } else { ed->setColor(mMenuColorBlue); mInvalidEmulatorEntry = false; } }; if (originalValue != "" && scraperParams.system->getLaunchCommandFromLabel(originalValue) == "") { LOG(LogWarning) << "GuiMetaDataEd: Invalid alternative emulator \"" << originalValue << "\" configured for game \"" << mScraperParams.game->getName() << "\""; mInvalidEmulatorEntry = true; } if (scraperParams.system->getSystemEnvData()->mLaunchCommands.size() == 1) { lbl->setOpacity(DISABLED_OPACITY); bracket->setOpacity(DISABLED_OPACITY); } if (mInvalidEmulatorEntry || scraperParams.system->getSystemEnvData()->mLaunchCommands.size() > 1) { row.makeAcceptInputHandler([this, title, scraperParams, ed, updateVal, originalValue] { const bool singleEntry { scraperParams.system->getSystemEnvData()->mLaunchCommands.size() == 1}; if (!mInvalidEmulatorEntry && ed->getValue() == "" && singleEntry) return; GuiSettings* s {nullptr}; if (mInvalidEmulatorEntry && singleEntry) s = new GuiSettings(_("CLEAR INVALID ENTRY")); else s = new GuiSettings(title); std::vector> launchCommands { scraperParams.system->getSystemEnvData()->mLaunchCommands}; if (ed->getValue() != "" && mInvalidEmulatorEntry && singleEntry) launchCommands.push_back(std::make_pair( "", ViewController::EXCLAMATION_CHAR + " " + originalValue)); else if (ed->getValue() != "") launchCommands.push_back(std::make_pair( "", ViewController::CROSSEDCIRCLE_CHAR + " " + _("CLEAR ENTRY"))); for (auto entry : launchCommands) { if (mInvalidEmulatorEntry && singleEntry && entry.second != ViewController::EXCLAMATION_CHAR + " " + originalValue) continue; std::string selectedLabel {ed->getValue()}; std::string label; ComponentListRow row; if (entry.second == "") continue; else label = entry.second; std::shared_ptr labelText { std::make_shared(label, Font::get(FONT_SIZE_MEDIUM), mMenuColorPrimary)}; labelText->setSelectable(true); if (scraperParams.system->getAlternativeEmulator() == "" && scraperParams.system->getSystemEnvData() ->mLaunchCommands.front() .second == label) labelText->setValue(labelText->getValue() .append(" [") .append(_("SYSTEM-WIDE")) .append("]")); if (scraperParams.system->getAlternativeEmulator() == label) labelText->setValue(labelText->getValue() .append(" [") .append(_("SYSTEM-WIDE")) .append("]")); row.addElement(labelText, true); row.makeAcceptInputHandler( [this, s, updateVal, entry, selectedLabel, launchCommands] { if (entry.second == launchCommands.back().second && launchCommands.back().first == "") { updateVal(""); } else if (entry.second != selectedLabel) { updateVal(entry.second); } mInvalidEmulatorEntry = false; delete s; }); // Select the row that corresponds to the selected label. if (selectedLabel == label) s->addRow(row, true); else s->addRow(row, false); } const float aspectValue {1.778f / mRenderer->getScreenAspectRatio()}; const float maxWidthModifier { glm::clamp(0.64f * aspectValue, 0.42f, (mRenderer->getIsVerticalOrientation() ? 0.95f : 0.92f))}; const float maxWidth {static_cast(mRenderer->getScreenWidth()) * maxWidthModifier}; s->setMenuSize(glm::vec2 {maxWidth, s->getMenuSize().y}); s->setMenuPosition(glm::vec3 {(s->getSize().x - maxWidth) / 2.0f, mPosition.y, mPosition.z}); mWindow->pushGui(s); }); } else { lbl->setOpacity(DISABLED_OPACITY); bracket->setOpacity(DISABLED_OPACITY); } break; } case MD_FOLDER_LINK: { ed = std::make_shared("", Font::get(FONT_SIZE_SMALL, FONT_PATH_LIGHT), mMenuColorPrimary, ALIGN_RIGHT); ed->setSize(0.0f, ed->getFont()->getHeight()); row.addElement(ed, true); auto spacer = std::make_shared(); spacer->setSize(mRenderer->getScreenWidth() * 0.005f, 0.0f); row.addElement(spacer, false); auto bracket = std::make_shared(); bracket->setResize(glm::vec2 {0.0f, lbl->getFont()->getLetterHeight()}); bracket->setImage(":/graphics/arrow.svg"); bracket->setColorShift(mMenuColorPrimary); row.addElement(bracket, false); const std::string title {_p("metadata", it->displayPrompt.c_str())}; std::vector children; if (originalValue != "") mInvalidFolderLinkEntry = true; for (auto child : scraperParams.game->getChildrenRecursive()) { if (child->getType() == GAME && child->getCountAsGame() && !child->getHidden()) { children.emplace_back(child); std::string filePath {child->getPath()}; filePath = Utils::String::replace(filePath, scraperParams.game->getPath() + "/", ""); if (Utils::String::replace(filePath, scraperParams.game->getPath() + "/", "") == originalValue) mInvalidFolderLinkEntry = false; } } #if defined(__unix__) std::sort(std::begin(children), std::end(children), [](FileData* a, FileData* b) { return a->getPath() < b->getPath(); }); #else std::sort(std::begin(children), std::end(children), [](FileData* a, FileData* b) { return Utils::String::toUpper(a->getPath()) < Utils::String::toUpper(b->getPath()); }); #endif // OK callback (apply new value to ed). auto updateVal = [this, ed, originalValue, scraperParams](const std::string& newVal) { mInvalidFolderLinkEntry = false; ed->setValue(newVal); if (newVal == originalValue) ed->setColor(mMenuColorPrimary); else ed->setColor(mMenuColorBlue); }; row.makeAcceptInputHandler([this, children, title, ed, updateVal, scraperParams] { GuiSettings* s {new GuiSettings(title)}; for (auto child : children) { std::string selectedLabel {ed->getValue()}; std::string label; ComponentListRow row; std::string filePath {child->getPath()}; filePath = Utils::String::replace(filePath, scraperParams.game->getPath() + "/", ""); std::shared_ptr labelText {std::make_shared( label, Font::get(FONT_SIZE_MEDIUM), mMenuColorPrimary)}; labelText->setSelectable(true); labelText->setValue(filePath); label = filePath; row.addElement(labelText, true); row.makeAcceptInputHandler([s, updateVal, filePath] { updateVal(filePath); delete s; }); // Select the row that corresponds to the selected label. if (selectedLabel == label) s->addRow(row, true); else s->addRow(row, false); } // If a value is set, then display "Clear entry" as the last entry. if (ed->getValue() != "") { ComponentListRow row; std::shared_ptr clearText {std::make_shared( ViewController::CROSSEDCIRCLE_CHAR + " " + _("CLEAR ENTRY"), Font::get(FONT_SIZE_MEDIUM), mMenuColorPrimary)}; clearText->setSelectable(true); row.addElement(clearText, true); row.makeAcceptInputHandler([this, s, ed] { mInvalidFolderLinkEntry = false; ed->setValue(""); delete s; }); s->addRow(row, false); } const float aspectValue {1.778f / mRenderer->getScreenAspectRatio()}; const float maxWidthModifier { glm::clamp(0.64f * aspectValue, 0.42f, (mRenderer->getIsVerticalOrientation() ? 0.95f : 0.92f))}; const float maxWidth {mRenderer->getScreenWidth() * maxWidthModifier}; s->setMenuSize(glm::vec2 {maxWidth, s->getMenuSize().y}); s->setMenuPosition( glm::vec3 {(s->getSize().x - maxWidth) / 2.0f, mPosition.y, mPosition.z}); mWindow->pushGui(s); }); break; } case MD_MULTILINE_STRING: default: { // MD_STRING. ed = std::make_shared("", Font::get(FONT_SIZE_SMALL, FONT_PATH_LIGHT), mMenuColorPrimary, ALIGN_RIGHT); ed->setRemoveLineBreaks(true); ed->setSize(0.0f, ed->getFont()->getHeight()); row.addElement(ed, true); auto spacer = std::make_shared(); spacer->setSize(mRenderer->getScreenWidth() * 0.005f, 0.0f); row.addElement(spacer, false); auto bracket = std::make_shared(); bracket->setResize(glm::vec2 {0.0f, lbl->getFont()->getLetterHeight()}); bracket->setImage(":/graphics/arrow.svg"); bracket->setColorShift(mMenuColorPrimary); row.addElement(bracket, false); bool multiLine {it->type == MD_MULTILINE_STRING}; const std::string title {_p("metadata", it->displayPrompt.c_str())}; gamePath = Utils::FileSystem::getStem(scraperParams.game->getPath()); // OK callback (apply new value to ed). auto updateVal = [ed, currentKey, originalValue, gamePath, scraperParams](const std::string& newVal) { // If the user has entered a blank game name, then set the name to the ROM // filename (minus the extension). if (currentKey == "name" && newVal == "") { if (scraperParams.game->isArcadeGame()) { ed->setValue(MameNames::getInstance().getCleanName( scraperParams.game->getCleanName())); } else { // For the special case where a directory has a supported file extension // and is therefore interpreted as a file, exclude the extension. if (scraperParams.game->getType() == GAME && Utils::FileSystem::isDirectory(scraperParams.game->getFullPath())) ed->setValue(Utils::FileSystem::getStem(gamePath)); else ed->setValue(gamePath); } if (gamePath == originalValue) ed->setColor(mMenuColorPrimary); else ed->setColor(mMenuColorBlue); } else if (newVal == "" && (currentKey == "developer" || currentKey == "publisher" || currentKey == "genre" || currentKey == "players")) { ed->setValue(_("unknown")); if (originalValue == "unknown") ed->setColor(mMenuColorPrimary); else ed->setColor(mMenuColorBlue); } else { ed->setValue(newVal); if ((currentKey == "developer" || currentKey == "publisher" || currentKey == "genre" || currentKey == "players") && newVal == _("unknown") && newVal == _(originalValue.c_str())) { ed->setColor(mMenuColorPrimary); } else { if (newVal == originalValue) ed->setColor(mMenuColorPrimary); else ed->setColor(mMenuColorBlue); } } }; if (Settings::getInstance()->getBool("VirtualKeyboard")) { row.makeAcceptInputHandler([this, title, ed, updateVal, multiLine] { const float verticalPosition { mRenderer->getIsVerticalOrientation() ? mPosition.y : 0.0f}; mWindow->pushGui(new GuiTextEditKeyboardPopup( getHelpStyle(), verticalPosition, title, ed->getValue(), updateVal, multiLine, _("APPLY"), _("APPLY CHANGES?"), "", "")); }); } else { row.makeAcceptInputHandler([this, title, ed, updateVal, multiLine] { mWindow->pushGui(new GuiTextEditPopup(getHelpStyle(), title, ed->getValue(), updateVal, multiLine, _("APPLY"), _("APPLY CHANGES?"))); }); } break; } } assert(ed); mList->addRow(row); if (it->type == MD_ALT_EMULATOR && mInvalidEmulatorEntry) { ed->setValue(ViewController::EXCLAMATION_CHAR + " " + originalValue); } else if (it->type == MD_FOLDER_LINK && mInvalidFolderLinkEntry) { ed->setValue(ViewController::EXCLAMATION_CHAR + " " + originalValue); } else if (it->type == MD_CONTROLLER && mMetaData->get(it->key) != "") { std::string displayName {BadgeComponent::getDisplayName(mMetaData->get(it->key))}; if (displayName != "unknown") ed->setValue(displayName); else ed->setValue(ViewController::EXCLAMATION_CHAR + " " + mMetaData->get(it->key)); } else { if ((currentKey == "developer" || currentKey == "publisher" || currentKey == "genre" || currentKey == "players") && mMetaData->get(it->key) == "unknown") ed->setValue(_(mMetaData->get(it->key).c_str())); else ed->setValue(mMetaData->get(it->key)); } mEditors.push_back(ed); } std::vector> buttons; if (!scraperParams.system->hasPlatformId(PlatformIds::PLATFORM_IGNORE)) buttons.push_back(std::make_shared( _("SCRAPE"), _("scrape"), std::bind(&GuiMetaDataEd::fetch, this))); buttons.push_back(std::make_shared(_("SAVE"), _("save metadata"), [&] { save(); delete this; })); buttons.push_back( std::make_shared(_("CANCEL"), _("cancel changes"), [&] { delete this; })); if (scraperParams.game->getType() == FOLDER) { if (mClearGameFunc) { auto clearSelf = [&] { mClearGameFunc(); delete this; }; auto clearSelfBtnFunc = [this, clearSelf] { mWindow->pushGui(new GuiMsgBox( getHelpStyle(), _("THIS WILL DELETE ANY MEDIA FILES AND " "THE GAMELIST.XML ENTRY FOR THIS FOLDER, " "BUT NEITHER THE DIRECTORY ITSELF OR ANY " "CONTENT INSIDE IT WILL BE REMOVED\nARE YOU SURE?"), _("YES"), clearSelf, _("NO"), nullptr, "", nullptr, nullptr, false, true, (mRenderer->getIsVerticalOrientation() ? 0.70f : 0.46f * (1.778f / mRenderer->getScreenAspectRatio())))); }; buttons.push_back( std::make_shared(_("CLEAR"), _("clear folder"), clearSelfBtnFunc)); } } else { if (mClearGameFunc) { auto clearSelf = [&] { mClearGameFunc(); delete this; }; auto clearSelfBtnFunc = [this, clearSelf] { mWindow->pushGui(new GuiMsgBox( getHelpStyle(), _("THIS WILL DELETE ANY MEDIA FILES " "AND THE GAMELIST.XML ENTRY FOR " "THIS GAME, BUT THE GAME FILE " "ITSELF WILL NOT BE REMOVED\nARE YOU SURE?"), _("YES"), clearSelf, _("NO"), nullptr, "", nullptr, nullptr, false, true, (mRenderer->getIsVerticalOrientation() ? 0.70f : 0.46f * (1.778f / mRenderer->getScreenAspectRatio())))); }; buttons.push_back( std::make_shared(_("CLEAR"), _("clear file"), clearSelfBtnFunc)); } // For the special case where a directory has a supported file extension and is therefore // interpreted as a file, don't add the delete button. if (mDeleteGameFunc && !Utils::FileSystem::isDirectory(scraperParams.game->getPath())) { auto deleteFilesAndSelf = [&] { mDeleteGameFunc(); delete this; }; auto deleteGameBtnFunc = [this, deleteFilesAndSelf] { mWindow->pushGui( new GuiMsgBox(getHelpStyle(), _("THIS WILL DELETE THE GAME " "FILE, ANY MEDIA FILES AND " "THE GAMELIST.XML ENTRY\nARE YOU SURE?"), _("YES"), deleteFilesAndSelf, _("NO"), nullptr, "", nullptr, nullptr, false, true, (mRenderer->getIsVerticalOrientation() ? 0.70f : 0.46f * (1.778f / mRenderer->getScreenAspectRatio())))); }; buttons.push_back(std::make_shared(_("DELETE"), _("delete game"), deleteGameBtnFunc)); } } mButtons = MenuComponent::makeButtonGrid(buttons); mGrid.setEntry(mButtons, glm::ivec2 {0, 5}, true, false, glm::ivec2 {2, 1}); // Resize + center. float width {std::min(mRenderer->getScreenHeight() * 1.05f, mRenderer->getScreenWidth() * (mRenderer->getIsVerticalOrientation() ? 0.95f : 0.90f))}; // Set height explicitly to ten rows for the component list. float height {mList->getRowHeight() * 10.0f + mTitle->getSize().y + mSubtitle->getSize().y + mButtons->getSize().y}; setSize(width, height); } void GuiMetaDataEd::onSizeChanged() { const float titleSubtitleSpacing {mSize.y * 0.03f}; mGrid.setRowHeightPerc(0, TITLE_HEIGHT / mSize.y / 2.0f); mGrid.setRowHeightPerc(1, TITLE_HEIGHT / mSize.y / 2.0f); mGrid.setRowHeightPerc(2, titleSubtitleSpacing / mSize.y); mGrid.setRowHeightPerc(3, (titleSubtitleSpacing * 1.2f) / mSize.y); mGrid.setRowHeightPerc(4, ((mList->getRowHeight() * 10.0f) + 2.0f) / mSize.y); mGrid.setColWidthPerc(1, 0.055f); mGrid.setSize(mSize); mBackground.fitTo(mSize); setPosition((mRenderer->getScreenWidth() - mSize.x) / 2.0f, (mRenderer->getScreenHeight() - mSize.y) / 2.0f); // Add some extra margins to the file/folder name. const float newSizeX {mSize.x * 0.96f}; mSubtitle->setSize(newSizeX, mSubtitle->getSize().y); mSubtitle->setPosition((mSize.x - newSizeX) / 2.0f, mSubtitle->getPosition().y); } void GuiMetaDataEd::save() { // Remove game from index. mScraperParams.system->getIndex()->removeFromIndex(mScraperParams.game); // We need this to handle the special situation where the user sets a game to hidden while // ShowHiddenGames is set to false, meaning it will immediately disappear from the gamelist. bool showHiddenGames {Settings::getInstance()->getBool("ShowHiddenGames")}; bool hideGameWhileHidden {false}; bool setGameAsCounted {false}; int offset {0}; for (unsigned int i {0}; i < mEditors.size(); ++i) { // The offset is needed to make the editor and metadata fields match up if we're // skipping the custom collections sortname field (which we do if not editing the // game from within a custom collection gamelist). if (mMetaDataDecl.at(i).key == "collectionsortname" && !mIsCustomCollection) offset = 1; if (mMetaDataDecl.at(i + offset).isStatistic) continue; const std::string& key {mMetaDataDecl.at(i + offset).key}; if (key == "altemulator" && mInvalidEmulatorEntry == true) continue; if (key == "folderlink" && mInvalidFolderLinkEntry) continue; if (key == "controller" && mEditors.at(i)->getValue() != "") { std::string shortName {BadgeComponent::getShortName(mEditors.at(i)->getValue())}; if (shortName != "unknown") mMetaData->set(key, shortName); continue; } if (!showHiddenGames && key == "hidden" && mEditors.at(i)->getValue() != mMetaData->get("hidden")) hideGameWhileHidden = true; // Check whether the flag to count the entry as a game was set to enabled. if (key == "nogamecount" && mEditors.at(i)->getValue() != mMetaData->get("nogamecount") && mMetaData->get("nogamecount") == "true") { setGameAsCounted = true; } if ((key == "developer" || key == "publisher" || key == "genre" || key == "players") && mEditors.at(i)->getValue() == _("unknown")) { mMetaData->set(key, "unknown"); } else { mMetaData->set(key, mEditors.at(i)->getValue()); } } // If hidden games are not shown and the hide flag was set for the entry, then write the // metadata immediately regardless of the SaveGamelistsMode setting. Otherwise the file // will never be written as the game will be filtered from the gamelist. This solution is // not really good as the gamelist will be written twice, but it's a very special and // hopefully rare situation. if (hideGameWhileHidden) GamelistFileParser::updateGamelist(mScraperParams.system); // Enter game in index. if (mScraperParams.game->getType() == GAME) mScraperParams.system->getIndex()->addToIndex(mScraperParams.game); // If it's a folder that has been updated, we need to manually sort the gamelist // as CollectionSystemsManager ignores folders. if (mScraperParams.game->getType() == FOLDER) mScraperParams.system->sortSystem(false); if (mSavedCallback && !mSavedMediaAndAborted) mSavedCallback(); if (hideGameWhileHidden) { std::vector hideGames; // If a folder was hidden there may be children inside that we also need to hide. if (mScraperParams.game->getType() == FOLDER) { for (FileData* child : mScraperParams.game->getChildrenRecursive()) { if (!child->getHidden()) child->metadata.set("hidden", "true"); hideGames.push_back(child); } } else { hideGames.push_back(mScraperParams.game); } for (FileData* hideGame : hideGames) { if (hideGame->getType() == GAME) { // Update disabled auto collections when hiding a game, as otherwise these could // get invalid gamelist cursor positions. A cursor pointing to a removed game // would crash the application upon enabling the collections. CollectionSystemsManager::getInstance()->refreshCollectionSystems(hideGame, true); // Remove the game from the index of all systems. for (SystemData* sys : SystemData::sSystemVector) { std::vector children; for (FileData* child : sys->getRootFolder()->getChildrenRecursive()) children.push_back(child->getSourceFileData()); if (std::find(children.begin(), children.end(), hideGame) != children.end()) { sys->getIndex()->removeFromIndex(hideGame); // Reload the gamelist as well as the view style may need to change. ViewController::getInstance()->reloadGamelistView(sys); } } } } } else { // Update all collections where the game is present. CollectionSystemsManager::getInstance()->refreshCollectionSystems(mScraperParams.game); } // If game counting was re-enabled for the game, then reactivate it in any custom collections // where it may exist. if (setGameAsCounted) CollectionSystemsManager::getInstance()->reactivateCustomCollectionEntry( mScraperParams.game); mScraperParams.system->onMetaDataSavePoint(); // If hidden games are not shown and the hide flag was set for the entry, we also need // to re-sort the gamelist as we may need to remove the parent folder if all the entries // within this folder are now hidden. if (hideGameWhileHidden) mScraperParams.system->sortSystem(true); // Make sure that the cached background is updated to reflect any possible visible // changes to the gamelist (e.g. the game name). mWindow->invalidateCachedBackground(); } void GuiMetaDataEd::fetch() { GuiScraperSingle* scr {new GuiScraperSingle( mScraperParams, std::bind(&GuiMetaDataEd::fetchDone, this, std::placeholders::_1), mSavedMediaAndAborted)}; mWindow->pushGui(scr); } void GuiMetaDataEd::fetchDone(const ScraperSearchResult& result) { // Clone the mMetaData object. MetaDataList* metadata {new MetaDataList(*mMetaData)}; mMediaFilesUpdated = result.savedNewMedia; int offset {0}; // Check if any values were manually changed before starting the scraping. // If so, it's these values we should compare against when scraping, not // the values previously saved for the game. for (unsigned int i = 0; i < mEditors.size(); ++i) { if (mMetaDataDecl.at(i).key == "collectionsortname" && !mIsCustomCollection) offset = 1; const std::string& key {mMetaDataDecl.at(i + offset).key}; if (metadata->get(key) != mEditors[i]->getValue()) metadata->set(key, mEditors[i]->getValue()); } GuiScraperSearch::saveMetadata(result, *metadata, mScraperParams.game); offset = 0; // Update the list with the scraped metadata values. for (unsigned int i {0}; i < mEditors.size(); ++i) { if (mMetaDataDecl.at(i).key == "collectionsortname" && !mIsCustomCollection) offset = 1; const std::string& key {mMetaDataDecl.at(i + offset).key}; if (key == "controller" && metadata->get(key) != "") { std::string displayName = BadgeComponent::getDisplayName(metadata->get(key)); if (displayName != "unknown") metadata->set(key, displayName); } if (mEditors.at(i)->getValue() != metadata->get(key)) { if (key == "rating") mEditors.at(i)->setOriginalColor(mMenuColorRed); else mEditors.at(i)->setColor(mMenuColorRed); } // Save all the keys that should be scraped. if (mMetaDataDecl.at(i + offset).shouldScrape) mEditors.at(i)->setValue(metadata->get(key)); } delete metadata; } void GuiMetaDataEd::close() { // Find out if the user made any changes. bool metadataUpdated {false}; int offset {0}; for (unsigned int i = 0; i < mEditors.size(); ++i) { if (mMetaDataDecl.at(i).key == "collectionsortname" && !mIsCustomCollection) offset = 1; const std::string& key {mMetaDataDecl.at(i + offset).key}; if (key == "altemulator" && mInvalidEmulatorEntry) continue; if (key == "folderlink" && mInvalidFolderLinkEntry) continue; std::string mMetaDataValue {mMetaData->get(key)}; std::string mEditorsValue {mEditors.at(i)->getValue()}; if ((key == "developer" || key == "publisher" || key == "genre" || key == "players") && mEditorsValue == _("unknown")) { mEditorsValue = "unknown"; } if (key == "controller" && mEditors.at(i)->getValue() != "") { std::string shortName = BadgeComponent::getShortName(mEditors.at(i)->getValue()); if (shortName == "unknown" || mMetaDataValue == shortName) continue; } if (mMetaDataValue != mEditorsValue) { metadataUpdated = true; break; } } std::function closeFunc; closeFunc = [this] { if (mMediaFilesUpdated || mSavedMediaAndAborted) { // Always reload the gamelist if media files were updated, even if the user // chose to not save any metadata changes or aborted the scraping. Also manually // unload the game image and marquee, as otherwise they would not get updated // until the user scrolls up and down the gamelist. TextureResource::manualUnload(mScraperParams.game->getImagePath(), false); TextureResource::manualUnload(mScraperParams.game->getMarqueePath(), false); ViewController::getInstance()->reloadGamelistView(mScraperParams.system); // Update all collections where the game is present. CollectionSystemsManager::getInstance()->refreshCollectionSystems(mScraperParams.game); mWindow->invalidateCachedBackground(); } delete this; }; if (metadataUpdated) { // Changes were made, ask if the user wants to save them. mWindow->pushGui(new GuiMsgBox( getHelpStyle(), _("SAVE CHANGES?"), _("YES"), [this, closeFunc] { save(); closeFunc(); }, _("NO"), closeFunc, "", nullptr, nullptr, true)); } else { // Always save if the media files have been changed (i.e. newly scraped images). if (mMediaFilesUpdated) save(); closeFunc(); } } bool GuiMetaDataEd::input(InputConfig* config, Input input) { if (GuiComponent::input(config, input)) return true; if (input.value != 0 && (config->isMappedTo("b", input))) { close(); return true; } if (input.value != 0 && (config->isMappedTo("y", input))) { fetch(); return true; } return false; } std::vector GuiMetaDataEd::getHelpPrompts() { std::vector prompts {mGrid.getHelpPrompts()}; prompts.push_back(HelpPrompt("y", _("scrape"))); prompts.push_back(HelpPrompt("b", _("back"))); return prompts; }