ES-DE/es-app/src/guis/GuiMetaDataEd.cpp

1055 lines
47 KiB
C++

// 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<MetaDataDecl>& mdd,
const ScraperSearchParams scraperParams,
std::function<void()> saveCallback,
std::function<void()> clearGameFunc,
std::function<void()> 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<TextComponent>(_("EDIT METADATA"), Font::get(FONT_SIZE_LARGE),
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<TextComponent>(
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<ComponentList>();
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<ImageComponent>();
mScrollDown = std::make_shared<ImageComponent>();
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<ScrollIndicatorComponent>(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<GuiComponent> 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<TextComponent>("", Font::get(FONT_SIZE_SMALL, FONT_PATH_LIGHT),
mMenuColorPrimary, ALIGN_RIGHT);
assert(ed);
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<TextComponent>(_(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<SwitchComponent>();
// 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<GuiComponent>();
spacer->setSize(mRenderer->getScreenWidth() * 0.0025f, 0.0f);
row.addElement(spacer, false);
ed = std::make_shared<RatingComponent>(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<GuiComponent>();
spacer->setSize(mRenderer->getScreenWidth() * 0.0025f, 0.0f);
row.addElement(spacer, false);
ed = std::make_shared<DateTimeEditComponent>(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<TextComponent>("", Font::get(FONT_SIZE_SMALL, FONT_PATH_LIGHT),
mMenuColorPrimary, ALIGN_RIGHT);
row.addElement(ed, true);
auto spacer = std::make_shared<GuiComponent>();
spacer->setSize(mRenderer->getScreenWidth() * 0.005f, 0.0f);
row.addElement(spacer, false);
auto bracket = std::make_shared<ImageComponent>();
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 {_(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<TextComponent> labelText {std::make_shared<TextComponent>(
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<TextComponent> clearText {std::make_shared<TextComponent>(
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<TextComponent>("", Font::get(FONT_SIZE_SMALL, FONT_PATH_LIGHT),
mMenuColorPrimary, ALIGN_RIGHT);
row.addElement(ed, true);
auto spacer = std::make_shared<GuiComponent>();
spacer->setSize(mRenderer->getScreenWidth() * 0.005f, 0.0f);
row.addElement(spacer, false);
auto bracket = std::make_shared<ImageComponent>();
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") :
_(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<std::pair<std::string, std::string>> 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<TextComponent> labelText {
std::make_shared<TextComponent>(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<float>(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<TextComponent>("", Font::get(FONT_SIZE_SMALL, FONT_PATH_LIGHT),
mMenuColorPrimary, ALIGN_RIGHT);
row.addElement(ed, true);
auto spacer = std::make_shared<GuiComponent>();
spacer->setSize(mRenderer->getScreenWidth() * 0.005f, 0.0f);
row.addElement(spacer, false);
auto bracket = std::make_shared<ImageComponent>();
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 {_(it->displayPrompt.c_str())};
std::vector<FileData*> 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<TextComponent> labelText {std::make_shared<TextComponent>(
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<TextComponent> clearText {std::make_shared<TextComponent>(
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<TextComponent>("", Font::get(FONT_SIZE_SMALL, FONT_PATH_LIGHT),
mMenuColorPrimary, ALIGN_RIGHT);
row.addElement(ed, true);
auto spacer = std::make_shared<GuiComponent>();
spacer->setSize(mRenderer->getScreenWidth() * 0.005f, 0.0f);
row.addElement(spacer, false);
auto bracket = std::make_shared<ImageComponent>();
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 {_(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 (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 {
ed->setValue(mMetaData->get(it->key));
}
mEditors.push_back(ed);
}
std::vector<std::shared_ptr<ButtonComponent>> buttons;
if (!scraperParams.system->hasPlatformId(PlatformIds::PLATFORM_IGNORE))
buttons.push_back(std::make_shared<ButtonComponent>(
_("SCRAPE"), _("scrape"), std::bind(&GuiMetaDataEd::fetch, this)));
buttons.push_back(std::make_shared<ButtonComponent>(_("SAVE"), _("save metadata"), [&] {
save();
delete this;
}));
buttons.push_back(
std::make_shared<ButtonComponent>(_("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"),
_("PROCEED"), clearSelf, _("CANCEL"), nullptr, "", nullptr,
nullptr, false, true,
(mRenderer->getIsVerticalOrientation() ?
0.70f :
0.46f * (1.778f / mRenderer->getScreenAspectRatio()))));
};
buttons.push_back(
std::make_shared<ButtonComponent>(_("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"),
_("PROCEED"), clearSelf, _("CANCEL"), nullptr, "", nullptr,
nullptr, false, true,
(mRenderer->getIsVerticalOrientation() ?
0.70f :
0.46f * (1.778f / mRenderer->getScreenAspectRatio()))));
};
buttons.push_back(
std::make_shared<ButtonComponent>(_("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"),
_("PROCEED"), deleteFilesAndSelf, _("CANCEL"), nullptr, "",
nullptr, nullptr, false, true,
(mRenderer->getIsVerticalOrientation() ?
0.70f :
0.46f * (1.778f / mRenderer->getScreenAspectRatio()))));
};
buttons.push_back(std::make_shared<ButtonComponent>(_("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;
}
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<FileData*> 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<FileData*> 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 == "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<void()> 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<HelpPrompt> GuiMetaDataEd::getHelpPrompts()
{
std::vector<HelpPrompt> prompts {mGrid.getHelpPrompts()};
prompts.push_back(HelpPrompt("y", _("scrape")));
prompts.push_back(HelpPrompt("b", _("back")));
return prompts;
}