// SPDX-License-Identifier: MIT // // ES-DE // BadgeComponent.cpp // // Game badges icons. // Used by the gamelist views. // #define SLOT_COLLECTION "collection" #define SLOT_FOLDER "folder" #define SLOT_FAVORITE "favorite" #define SLOT_COMPLETED "completed" #define SLOT_KIDGAME "kidgame" #define SLOT_BROKEN "broken" #define SLOT_CONTROLLER "controller" #define SLOT_ALTEMULATOR "altemulator" #define SLOT_MANUAL "manual" #include "components/BadgeComponent.h" #include "Log.h" #include "Settings.h" #include "ThemeData.h" #include "resources/TextureResource.h" #include "utils/StringUtil.h" namespace { // clang-format off // The "unknown" controller entry has to be placed last. GameControllers sControllerDefinitions [] { // shortName displayName fileName {"gamepad_generic", "Gamepad (Generic)", ":/graphics/controllers/gamepad_generic.svg"}, {"gamepad_nintendo_nes", "Gamepad (Nintendo NES)", ":/graphics/controllers/gamepad_nintendo_nes.svg"}, {"gamepad_nintendo_snes", "Gamepad (Nintendo SNES)", ":/graphics/controllers/gamepad_nintendo_snes.svg"}, {"gamepad_nintendo_64", "Gamepad (Nintendo 64)", ":/graphics/controllers/gamepad_nintendo_64.svg"}, {"gamepad_nintendo_gamecube", "Gamepad (Nintendo GameCube)", ":/graphics/controllers/gamepad_nintendo_gamecube.svg"}, {"gamepad_playstation", "Gamepad (PlayStation)", ":/graphics/controllers/gamepad_playstation.svg"}, {"gamepad_sega_master_system", "Gamepad (Sega Master System)", ":/graphics/controllers/gamepad_sega_master_system.svg"}, {"gamepad_sega_md_3_buttons", "Gamepad (Sega Mega Drive/Genesis 3 Buttons)", ":/graphics/controllers/gamepad_sega_md_3_buttons.svg"}, {"gamepad_sega_md_6_buttons", "Gamepad (Sega Mega Drive/Genesis 6 Buttons)", ":/graphics/controllers/gamepad_sega_md_6_buttons.svg"}, {"gamepad_sega_dreamcast", "Gamepad (Sega Dreamcast)", ":/graphics/controllers/gamepad_sega_dreamcast.svg"}, {"gamepad_xbox", "Gamepad (Xbox)", ":/graphics/controllers/gamepad_xbox.svg"}, {"joystick_generic", "Joystick (Generic)", ":/graphics/controllers/joystick_generic.svg"}, {"joystick_arcade_no_buttons", "Joystick (Arcade No Buttons)", ":/graphics/controllers/joystick_arcade_no_buttons.svg"}, {"joystick_arcade_no_buttons_twin", "Joystick (Arcade No Buttons Twin Stick)", ":/graphics/controllers/joystick_arcade_no_buttons_twin.svg"}, {"joystick_arcade_1_button", "Joystick (Arcade 1 Button)", ":/graphics/controllers/joystick_arcade_1_button.svg"}, {"joystick_arcade_2_buttons", "Joystick (Arcade 2 Buttons)", ":/graphics/controllers/joystick_arcade_2_buttons.svg"}, {"joystick_arcade_3_buttons", "Joystick (Arcade 3 Buttons)", ":/graphics/controllers/joystick_arcade_3_buttons.svg"}, {"joystick_arcade_4_buttons", "Joystick (Arcade 4 Buttons)", ":/graphics/controllers/joystick_arcade_4_buttons.svg"}, {"joystick_arcade_5_buttons", "Joystick (Arcade 5 Buttons)", ":/graphics/controllers/joystick_arcade_5_buttons.svg"}, {"joystick_arcade_6_buttons", "Joystick (Arcade 6 Buttons)", ":/graphics/controllers/joystick_arcade_6_buttons.svg"}, {"keyboard_generic", "Keyboard (Generic)", ":/graphics/controllers/keyboard_generic.svg"}, {"keyboard_and_mouse_generic", "Keyboard and Mouse (Generic)", ":/graphics/controllers/keyboard_and_mouse_generic.svg"}, {"mouse_generic", "Mouse (Generic)", ":/graphics/controllers/mouse_generic.svg"}, {"mouse_amiga", "Mouse (Amiga)", ":/graphics/controllers/mouse_amiga.svg"}, {"lightgun_generic", "Lightgun (Generic)", ":/graphics/controllers/lightgun_generic.svg"}, {"lightgun_nintendo", "Lightgun (Nintendo)", ":/graphics/controllers/lightgun_nintendo.svg"}, {"steering_wheel_generic", "Steering Wheel (Generic)", ":/graphics/controllers/steering_wheel_generic.svg"}, {"flight_stick_generic", "Flight Stick (Generic)", ":/graphics/controllers/flight_stick_generic.svg"}, {"spinner_generic", "Spinner (Generic)", ":/graphics/controllers/spinner_generic.svg"}, {"trackball_generic", "Trackball (Generic)", ":/graphics/controllers/trackball_generic.svg"}, {"wii_remote_nintendo", "Wii Remote (Nintendo)", ":/graphics/controllers/wii_remote_nintendo.svg"}, {"wii_remote_and_nunchuk_nintendo", "Wii Remote and Nunchuk (Nintendo)", ":/graphics/controllers/wii_remote_and_nunchuk_nintendo.svg"}, {"joycon_left_or_right_nintendo", "Joy-Con Left or Right (Nintendo)", ":/graphics/controllers/joycon_left_or_right_nintendo.svg"}, {"joycon_pair_nintendo", "Joy-Con Pair (Nintendo)", ":/graphics/controllers/joycon_pair_nintendo.svg"}, {"xbox_kinect", "Xbox Kinect", ":/graphics/controllers/xbox_kinect.svg"}, {"unknown", "Unknown Controller", ":/graphics/controllers/unknown.svg"} }; // clang-format on } // namespace BadgeComponent::BadgeComponent() : mFlexboxItems {} , mFlexboxComponent {mFlexboxItems} , mBadgeTypes {{SLOT_COLLECTION, SLOT_FOLDER, SLOT_FAVORITE, SLOT_COMPLETED, SLOT_KIDGAME, SLOT_BROKEN, SLOT_CONTROLLER, SLOT_ALTEMULATOR, SLOT_MANUAL}} , mLinearInterpolation {false} { mBadgeIcons[SLOT_COLLECTION] = ":/graphics/badge_collection.svg"; mBadgeIcons[SLOT_FOLDER] = ":/graphics/badge_folder.svg"; mBadgeIcons[SLOT_FAVORITE] = ":/graphics/badge_favorite.svg"; mBadgeIcons[SLOT_COMPLETED] = ":/graphics/badge_completed.svg"; mBadgeIcons[SLOT_KIDGAME] = ":/graphics/badge_kidgame.svg"; mBadgeIcons[SLOT_BROKEN] = ":/graphics/badge_broken.svg"; mBadgeIcons[SLOT_CONTROLLER] = ":/graphics/badge_controller.svg"; mBadgeIcons[SLOT_ALTEMULATOR] = ":/graphics/badge_altemulator.svg"; mBadgeIcons[SLOT_MANUAL] = ":/graphics/badge_manual.svg"; } void BadgeComponent::populateGameControllers() { sGameControllers.clear(); for (auto controller : sControllerDefinitions) sGameControllers.emplace_back(controller); } void BadgeComponent::setBadges(const std::vector& badges) { std::map prevVisibility; std::map prevPlayers; std::map prevController; // Save the visibility status to know whether any badges changed. for (auto& item : mFlexboxItems) { prevVisibility[item.label] = item.visible; if (item.overlayImage.getTexture() != nullptr) prevController[item.label] = item.overlayImage.getTexture()->getTextureFilePath(); item.visible = false; } for (auto& badge : badges) { auto it = std::find_if( mFlexboxItems.begin(), mFlexboxItems.end(), [badge](FlexboxComponent::FlexboxItem item) { return item.label == badge.badgeType; }); if (it != mFlexboxItems.end()) { // Don't show the alternative emulator badge if the corresponding setting has been // disabled. if (badge.badgeType == "altemulator" && !Settings::getInstance()->getBool("AlternativeEmulatorPerGame")) continue; it->visible = true; std::string texturePath; if (it->overlayImage.getTexture() != nullptr) texturePath = it->overlayImage.getTexture()->getTextureFilePath(); if (badge.badgeType == "folder") { if (badge.folderLink) it->overlayImage.setVisible(true); else it->overlayImage.setVisible(false); } if (badge.gameController != "" && badge.gameController != texturePath) { auto it2 = std::find_if(sGameControllers.begin(), sGameControllers.end(), [badge](GameControllers gameController) { return gameController.shortName == badge.gameController; }); if (it2 != sGameControllers.cend()) { it->overlayImage.setImage((*it2).fileName); // This is done to keep the texture cache entry from expiring. mOverlayMap[it2->shortName] = std::make_unique(it->overlayImage); } else if (badge.gameController != "") it->overlayImage.setImage(sGameControllers.back().fileName); } } } // Only recalculate the flexbox if any badges changed. for (auto& item : mFlexboxItems) { if (prevVisibility[item.label] != item.visible || prevController[item.label] != item.label) { mFlexboxComponent.onSizeChanged(); break; } } } const std::string BadgeComponent::getShortName(const std::string& displayName) { auto it = std::find_if(sGameControllers.begin(), sGameControllers.end(), [displayName](GameControllers gameController) { return gameController.displayName == displayName; }); if (it != sGameControllers.end()) return (*it).shortName; else return "unknown"; } const std::string BadgeComponent::getDisplayName(const std::string& shortName) { auto it = std::find_if(sGameControllers.begin(), sGameControllers.end(), [shortName](GameControllers gameController) { return gameController.shortName == shortName; }); if (it != sGameControllers.end()) return (*it).displayName; else return "unknown"; } void BadgeComponent::render(const glm::mat4& parentTrans) { if (!isVisible() || mFlexboxItems.empty() || mOpacity == 0.0f || mThemeOpacity == 0.0f) return; if (mOpacity * mThemeOpacity == 1.0f) { mFlexboxComponent.render(parentTrans); } else { mFlexboxComponent.setOpacity(mOpacity * mThemeOpacity); mFlexboxComponent.render(parentTrans); mFlexboxComponent.setOpacity(1.0f); } } void BadgeComponent::applyTheme(const std::shared_ptr& theme, const std::string& view, const std::string& element, unsigned int properties) { populateGameControllers(); using namespace ThemeFlags; const ThemeData::ThemeElement* elem {theme->getElement(view, element, "badges")}; if (!elem) return; if (properties & ThemeFlags::POSITION && elem->has("stationary")) { const std::string& stationary {elem->get("stationary")}; if (stationary == "never") mStationary = Stationary::NEVER; else if (stationary == "always") mStationary = Stationary::ALWAYS; else if (stationary == "withinView") mStationary = Stationary::WITHIN_VIEW; else if (stationary == "betweenViews") mStationary = Stationary::BETWEEN_VIEWS; else LOG(LogWarning) << "BadgeComponent: Invalid theme configuration, property " "\"stationary\" for element \"" << element.substr(7) << "\" defined as \"" << stationary << "\""; } if (elem->has("horizontalAlignment")) { const std::string horizontalAlignment {elem->get("horizontalAlignment")}; if (horizontalAlignment != "left" && horizontalAlignment != "center" && horizontalAlignment != "right") { LOG(LogWarning) << "BadgeComponent: Invalid theme configuration, property \"horizontalAlignment\" " "for element \"" << element.substr(7) << "\" defined as \"" << horizontalAlignment << "\""; } else { mFlexboxComponent.setAlignment(horizontalAlignment); } } if (elem->has("direction")) { const std::string direction {elem->get("direction")}; if (direction != "row" && direction != "column") { LOG(LogWarning) << "BadgeComponent: Invalid theme configuration, property \"direction\"" " for element \"" << element.substr(7) << "\" defined as \"" << direction << "\""; } else { mFlexboxComponent.setDirection(direction); } } mFlexboxComponent.setLines(3); if (elem->has("lines")) { const unsigned int lines {elem->get("lines")}; if (lines < 1 || lines > 10) { LOG(LogWarning) << "BadgeComponent: Invalid theme configuration, property \"lines\"" " for element \"" << element.substr(7) << "\" defined as \"" << lines << "\""; } else { mFlexboxComponent.setLines(lines); } } mFlexboxComponent.setItemsPerLine(4); if (elem->has("itemsPerLine")) { const unsigned int itemsPerLine {elem->get("itemsPerLine")}; if (itemsPerLine < 1 || itemsPerLine > 10) { LOG(LogWarning) << "BadgeComponent: Invalid theme configuration, property \"itemsPerLine\"" " for element \"" << element.substr(7) << "\" defined as \"" << itemsPerLine << "\""; } else { mFlexboxComponent.setItemsPerLine(itemsPerLine); } } if (elem->has("itemMargin")) { glm::vec2 itemMargin = elem->get("itemMargin"); if ((itemMargin.x != -1.0 && itemMargin.y != -1.0) && (itemMargin.x < 0.0f || itemMargin.x > 0.2f || itemMargin.y < 0.0f || itemMargin.y > 0.2f)) { LOG(LogWarning) << "BadgeComponent: Invalid theme configuration, property \"itemMargin\"" " for element \"" << element.substr(7) << "\" defined as \"" << itemMargin.x << " " << itemMargin.y << "\""; } else { mFlexboxComponent.setItemMargin(itemMargin); } } // Enable linear interpolation by default if element is arbitrarily rotated. if (properties & ThemeFlags::ROTATION && elem->has("rotation")) { const float rotation {std::abs(elem->get("rotation"))}; if (rotation != 0.0f && (std::round(rotation) != rotation || static_cast(rotation) % 90 != 0)) mLinearInterpolation = true; } if (elem->has("interpolation")) { const std::string& interpolation {elem->get("interpolation")}; if (interpolation == "linear") { mLinearInterpolation = true; } else if (interpolation == "nearest") { mLinearInterpolation = false; } else { LOG(LogWarning) << "BadgeComponent: Invalid theme configuration, property " "\"interpolation\" for element \"" << element.substr(7) << "\" defined as \"" << interpolation << "\""; } } unsigned int badgeIconColorShift {0xFFFFFFFF}; unsigned int badgeIconColorShiftEnd {0xFFFFFFFF}; bool badgeIconColorGradientHorizontal {true}; if (elem->has("badgeIconColor")) { badgeIconColorShift = elem->get("badgeIconColor"); badgeIconColorShiftEnd = badgeIconColorShift; } if (elem->has("badgeIconColorEnd")) badgeIconColorShiftEnd = elem->get("badgeIconColorEnd"); if (elem->has("badgeIconGradientType")) { const std::string& gradientType {elem->get("badgeIconGradientType")}; if (gradientType == "horizontal") { badgeIconColorGradientHorizontal = true; } else if (gradientType == "vertical") { badgeIconColorGradientHorizontal = false; } else { badgeIconColorGradientHorizontal = true; LOG(LogWarning) << "BadgeComponent: Invalid theme configuration, property " "\"badgeIconGradientType\" for element \"" << element.substr(7) << "\" defined as \"" << gradientType << "\""; } } unsigned int controllerIconColorShift {0xFFFFFFFF}; unsigned int controllerIconColorShiftEnd {0xFFFFFFFF}; bool controllerIconColorGradientHorizontal {true}; if (elem->has("controllerIconColor")) { controllerIconColorShift = elem->get("controllerIconColor"); controllerIconColorShiftEnd = controllerIconColorShift; } if (elem->has("controllerIconColorEnd")) controllerIconColorShiftEnd = elem->get("controllerIconColorEnd"); if (elem->has("controllerIconGradientType")) { const std::string& gradientType {elem->get("controllerIconGradientType")}; if (gradientType == "horizontal") { controllerIconColorGradientHorizontal = true; } else if (gradientType == "vertical") { controllerIconColorGradientHorizontal = false; } else { controllerIconColorGradientHorizontal = true; LOG(LogWarning) << "BadgeComponent: Invalid theme configuration, property " "\"controllerIconGradientType\" for element \"" << element.substr(7) << "\" defined as \"" << gradientType << "\""; } } unsigned int folderLinkIconColorShift {0xFFFFFFFF}; unsigned int folderLinkIconColorShiftEnd {0xFFFFFFFF}; bool folderLinkIconColorGradientHorizontal {true}; if (elem->has("folderLinkIconColor")) { folderLinkIconColorShift = elem->get("folderLinkIconColor"); folderLinkIconColorShiftEnd = folderLinkIconColorShift; } if (elem->has("folderLinkIconColorEnd")) folderLinkIconColorShiftEnd = elem->get("folderLinkIconColorEnd"); if (elem->has("folderLinkIconGradientType")) { const std::string& gradientType {elem->get("folderLinkIconGradientType")}; if (gradientType == "horizontal") { folderLinkIconColorGradientHorizontal = true; } else if (gradientType == "vertical") { folderLinkIconColorGradientHorizontal = false; } else { folderLinkIconColorGradientHorizontal = true; LOG(LogWarning) << "BadgeComponent: Invalid theme configuration, property " "\"folderLinkIconGradientType\" for element \"" << element.substr(7) << "\" defined as \"" << gradientType << "\""; } } if (elem->has("slots")) { // Replace possible whitespace separators with commas. std::string slotsTag {Utils::String::toLower(elem->get("slots"))}; for (auto& character : slotsTag) { if (std::isspace(character)) character = ','; } slotsTag = Utils::String::replace(slotsTag, ",,", ","); std::vector slots {Utils::String::delimitedStringToVector(slotsTag, ",")}; // If the "all" value has been set, then populate all badges not already defined. if (std::find(slots.begin(), slots.end(), "all") != slots.end()) { for (auto& badge : mBadgeTypes) { if (std::find(slots.begin(), slots.end(), badge) == slots.end()) slots.emplace_back(badge); } } for (auto slot : slots) { if (std::find(mBadgeTypes.cbegin(), mBadgeTypes.cend(), slot) != mBadgeTypes.end()) { // The "badge_" string is required as ThemeData adds this as a prefix to avoid // name collisions when using XML attributes. if (properties & PATH && elem->has("badge_" + slot)) { const std::string& path {elem->get("badge_" + slot)}; if (Utils::FileSystem::exists(path) && !Utils::FileSystem::isDirectory(path)) { mBadgeIcons[slot] = path; } else { LOG(LogWarning) << "BadgeComponent: Invalid theme configuration, property " "\"customBadgeIcon\" for element \"" << element.substr(7) << "\", image does not exist: \"" << path << "\""; } } FlexboxComponent::FlexboxItem item; item.label = slot; ImageComponent badgeImage {false, false}; badgeImage.setLinearInterpolation(mLinearInterpolation); badgeImage.setImage(mBadgeIcons[slot]); item.baseImage = badgeImage; item.overlayImage = ImageComponent {false, false}; item.overlayImage.setLinearInterpolation(mLinearInterpolation); item.baseImage.setColorShift(badgeIconColorShift); item.baseImage.setColorShiftEnd(badgeIconColorShiftEnd); if (badgeIconColorGradientHorizontal != true) item.baseImage.setColorGradientHorizontal(badgeIconColorGradientHorizontal); if (slot == "folder") { std::string folderLinkPath {":/graphics/badge_folderlink_overlay.svg"}; if (elem->has("customFolderLinkIcon")) { const std::string path {elem->get("customFolderLinkIcon")}; if (Utils::FileSystem::exists(path) && !Utils::FileSystem::isDirectory(path)) { folderLinkPath = path; } else { LOG(LogWarning) << "BadgeComponent: Invalid theme configuration, property " "\"customFolderLinkIcon\" for element \"" << element.substr(7) << "\", image does not exist: \"" << path << "\""; } } item.overlayImage.setImage(folderLinkPath); item.overlayImage.setColorShift(folderLinkIconColorShift); item.overlayImage.setColorShiftEnd(folderLinkIconColorShiftEnd); if (folderLinkIconColorGradientHorizontal != true) item.overlayImage.setColorGradientHorizontal( folderLinkIconColorGradientHorizontal); if (elem->has("folderLinkPos")) { glm::vec2 folderLinkPos {elem->get("folderLinkPos")}; folderLinkPos.x = glm::clamp(folderLinkPos.x, -1.0f, 2.0f); folderLinkPos.y = glm::clamp(folderLinkPos.y, -1.0f, 2.0f); item.overlayPosition = folderLinkPos; } if (elem->has("folderLinkSize")) { item.overlaySize = glm::clamp(elem->get("folderLinkSize"), 0.1f, 1.0f); } } else if (slot == "controller") { if (elem->has("controllerPos")) { glm::vec2 controllerPos {elem->get("controllerPos")}; controllerPos.x = glm::clamp(controllerPos.x, -1.0f, 2.0f); controllerPos.y = glm::clamp(controllerPos.y, -1.0f, 2.0f); item.overlayPosition = controllerPos; item.overlayImage.setColorShift(controllerIconColorShift); item.overlayImage.setColorShiftEnd(controllerIconColorShiftEnd); if (controllerIconColorGradientHorizontal != true) item.overlayImage.setColorGradientHorizontal( controllerIconColorGradientHorizontal); } if (elem->has("controllerSize")) item.overlaySize = glm::clamp(elem->get("controllerSize"), 0.1f, 2.0f); } mFlexboxItems.emplace_back(std::move(item)); } else if (slot != "all") { LOG(LogError) << "Invalid badge slot \"" << slot << "\" defined"; } } for (auto& gameController : sGameControllers) { if (properties & PATH && elem->has("controller_" + gameController.shortName)) { const std::string& path { elem->get("controller_" + gameController.shortName)}; if (Utils::FileSystem::exists(path) && !Utils::FileSystem::isDirectory(path)) { gameController.fileName = path; } else { LOG(LogWarning) << "BadgeComponent: Invalid theme configuration, property " "\"customControllerIcon\" for element \"" << element.substr(7) << "\", image does not exist: \"" << path << "\""; } } } GuiComponent::applyTheme(theme, view, element, properties); mFlexboxComponent.setPosition(mPosition); mFlexboxComponent.setSize(mSize); mFlexboxComponent.setOrigin(mOrigin); mFlexboxComponent.setRotation(mRotation); mFlexboxComponent.setRotationOrigin(mRotationOrigin); mFlexboxComponent.setVisible(mVisible); mFlexboxComponent.setDefaultZIndex(mDefaultZIndex); mFlexboxComponent.setZIndex(mZIndex); } }