diff --git a/CMakeLists.txt b/CMakeLists.txt index 31fad76de..d2f7ce5c6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -154,10 +154,11 @@ set(ES_HEADERS ${CMAKE_CURRENT_SOURCE_DIR}/src/Settings.h ${CMAKE_CURRENT_SOURCE_DIR}/src/Sound.h ${CMAKE_CURRENT_SOURCE_DIR}/src/SystemData.h + ${CMAKE_CURRENT_SOURCE_DIR}/src/ThemeData.h ${CMAKE_CURRENT_SOURCE_DIR}/src/VolumeControl.h ${CMAKE_CURRENT_SOURCE_DIR}/src/Window.h ${CMAKE_CURRENT_SOURCE_DIR}/src/XMLReader.h - ${CMAKE_CURRENT_SOURCE_DIR}/src/components/AnimationComponent.h + ${CMAKE_CURRENT_SOURCE_DIR}/src/components/AsyncReqComponent.h ${CMAKE_CURRENT_SOURCE_DIR}/src/components/ButtonComponent.h ${CMAKE_CURRENT_SOURCE_DIR}/src/components/ComponentListComponent.h @@ -172,24 +173,28 @@ set(ES_HEADERS ${CMAKE_CURRENT_SOURCE_DIR}/src/components/TextComponent.h ${CMAKE_CURRENT_SOURCE_DIR}/src/components/TextEditComponent.h ${CMAKE_CURRENT_SOURCE_DIR}/src/components/TextListComponent.h - ${CMAKE_CURRENT_SOURCE_DIR}/src/components/ThemeComponent.h ${CMAKE_CURRENT_SOURCE_DIR}/src/components/GuiDetectDevice.h - ${CMAKE_CURRENT_SOURCE_DIR}/src/components/GuiFastSelect.h ${CMAKE_CURRENT_SOURCE_DIR}/src/components/GuiMetaDataEd.h ${CMAKE_CURRENT_SOURCE_DIR}/src/components/GuiMsgBoxOk.h ${CMAKE_CURRENT_SOURCE_DIR}/src/components/GuiMsgBoxYesNo.h - ${CMAKE_CURRENT_SOURCE_DIR}/src/components/GuiGameList.h ${CMAKE_CURRENT_SOURCE_DIR}/src/components/GuiGameScraper.h ${CMAKE_CURRENT_SOURCE_DIR}/src/components/GuiInputConfig.h - ${CMAKE_CURRENT_SOURCE_DIR}/src/components/GuiMenu.h ${CMAKE_CURRENT_SOURCE_DIR}/src/components/GuiSettingsMenu.h ${CMAKE_CURRENT_SOURCE_DIR}/src/components/GuiScraperStart.h ${CMAKE_CURRENT_SOURCE_DIR}/src/components/GuiScraperLog.h + ${CMAKE_CURRENT_SOURCE_DIR}/src/scrapers/Scraper.h ${CMAKE_CURRENT_SOURCE_DIR}/src/scrapers/GamesDBScraper.h ${CMAKE_CURRENT_SOURCE_DIR}/src/scrapers/TheArchiveScraper.h + ${CMAKE_CURRENT_SOURCE_DIR}/src/pugiXML/pugiconfig.hpp ${CMAKE_CURRENT_SOURCE_DIR}/src/pugiXML/pugixml.hpp + + ${CMAKE_CURRENT_SOURCE_DIR}/src/views/BasicGameListView.h + ${CMAKE_CURRENT_SOURCE_DIR}/src/views/DetailedGameListView.h + ${CMAKE_CURRENT_SOURCE_DIR}/src/views/GameListView.h + ${CMAKE_CURRENT_SOURCE_DIR}/src/views/ViewController.h + ${CMAKE_CURRENT_SOURCE_DIR}/src/resources/Font.h ${CMAKE_CURRENT_SOURCE_DIR}/src/resources/ResourceManager.h ${CMAKE_CURRENT_SOURCE_DIR}/src/resources/TextureResource.h @@ -215,10 +220,11 @@ set(ES_SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/src/Settings.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Sound.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/SystemData.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/ThemeData.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/VolumeControl.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Window.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/XMLReader.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/src/components/AnimationComponent.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/components/AsyncReqComponent.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/components/ButtonComponent.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/components/ComponentListComponent.cpp @@ -231,27 +237,31 @@ set(ES_SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/src/components/SwitchComponent.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/components/TextComponent.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/components/TextEditComponent.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/src/components/ThemeComponent.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/components/GuiDetectDevice.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/src/components/GuiFastSelect.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/components/GuiMetaDataEd.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/components/GuiMsgBoxOk.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/components/GuiMsgBoxYesNo.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/src/components/GuiGameList.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/components/GuiGameScraper.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/components/GuiInputConfig.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/src/components/GuiMenu.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/components/GuiSettingsMenu.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/components/GuiScraperStart.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/components/GuiScraperLog.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/scrapers/Scraper.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/scrapers/GamesDBScraper.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/scrapers/TheArchiveScraper.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/pugiXML/pugixml.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/resources/Font.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/resources/ResourceManager.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/resources/TextureResource.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/views/BasicGameListView.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/views/DetailedGameListView.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/views/GameListView.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/views/ViewController.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/data/ResourceUtil.cpp ${CMAKE_CURRENT_SOURCE_DIR}/data/converted/ES_logo_16_png.cpp ${CMAKE_CURRENT_SOURCE_DIR}/data/converted/ES_logo_32_png.cpp diff --git a/DEVNOTES.md b/DEVNOTES.md new file mode 100644 index 000000000..7ec034477 --- /dev/null +++ b/DEVNOTES.md @@ -0,0 +1,35 @@ +Also known as "Really Bad Technical Documentation" + +These are mostly notes I create as I go along, marking potential gotchas for people who might try to extend my code. +Some day I'll try and add an overview of the code structure, what each class does, etc. + +Development Environment +======================= + +I personally launch ES in windowed mode with a smaller resolution than my monitor and with debug text enabled. + +`emulationstation --windowed --debug -w 1280 -h 720` + + +Creating a new GuiComponent +=========================== + +You probably want to override: + + `bool input(InputConfig* config, Input input);` + Check if some input is mapped to some action with `config->isMappedTo("a", input);`. + Check if an input is "pressed" with `input.value != 0` (input.value *can* be negative in the case of axes). + + `void update(int deltaTime);` + `deltaTime` is in milliseconds. + + `void render(const Eigen::Affine3f& parentTrans);` + You probably want to do `Eigen::Affine3f trans = parentTrans * getTransform();` to get your final "modelview" matrix. + Apply the modelview matrix with `Renderer::setMatrix(const Eigen::Affine3f&)`. + Render any children the component may have with `renderChildren(parentTrans);`. + + +Creating a new GameListView Class +================================= + +1. Don't allow the user to navigate to the root node's parent. If you use a stack of some sort to keep track of past cursor states this will be a natural side effect. diff --git a/THEMES.md b/THEMES.md index c908228c0..0adbf7858 100644 --- a/THEMES.md +++ b/THEMES.md @@ -1,176 +1,114 @@ Themes ====== -EmulationStation allows each system to have its own "theme." A theme is a collection of display settings and images defined in an XML document. +EmulationStation allows each system to have its own "theme." A theme is a collection of resources defined in an XML document. -ES will check 3 places for a theme, in the following order: - - a theme.xml file in the root of a system's %PATH% directory. - - $HOME/.emulationstation/%NAME%/theme.xml - - $HOME/.emulationstation/es_theme.xml - -Almost all positions, dimensions, origins, etc. work in percentages - that is, they are a decimal between 0 and 1, representing the percentage of the screen on that axis to use. This ensures that themes look similar at every resolution. - -Colors are hex values, either 6 or 8 characters, defined as RRGGBB or RRGGBBAA. If alpha is not included, a default value of FF will be assumed (not transparent). +Themes are loaded like this: +1. Initialize to default values. +2. If `$HOME/.emulationstation/es_theme_default.xml` exists, load it. +3a. If there is a `theme.xml` present in the root of a system's `path` directory, load it. +3b. IF NOT, If `$HOME/.emulationstation/%SYSTEMNAME%/theme.xml` exists, load it. Example ======= -Here's a theme that defines some colors, displays a background, and displays a logo in the top left corner: ``` 0000FF 00FF00 - - image - ./theme/background.png - 0 0 - 1 1 - 0 0 - + + ./../all_themes/font.ttf + 0.045 + - - image + + ./../all_themes/font.ttf + 0.035 + + + + ./theme/background.png + true + + + ./theme/logo.png - 0 0 - 0.4 0 - 0 0 - + false + + + ./../all_themes/scrollSound.wav - - - - 0000FF - 00FF00 - - - image - ./theme/background.png - 0 0 - 1 1 - 0 0 - - - ``` -Themes can be defined with two tags: `` and ``. -You can define both a normal and basic theme in the same file. +Themes must be enclosed in a `` tag. -If EmulationStation is running in "basic" mode, it will try to use ``. If that doesn't exist or ES is running in "detailed" mode (a gamelist.xml is present), `` will be used. - - -Components -========== - -A theme is made up of components, which have various types. At the moment, the only type is `image`. Components are rendered in the order they are defined - that means you'll want to define the background first, a header image second, etc. - - -The "image" component -===================== - -Used to display an image. - -`` - path to the image file. Most common file types are supported, and . and ~ are properly expanded. - -`` - the position, as two screen percentages, at which to display the image. - -`` - the dimensions, as two screen percentages, that the image will be resized to. Make one axis 0 to keep the aspect ratio. - -`` - the point on the image that `` defines, as an image percentage. "0.5 0.5", the center of the image, by default. - -`` - if present, the image is tiled instead of resized. - - -Display tags -============ - -Display tags define some "meta" display attributes about your theme. Display tags must be at the root of the `` tree - for example, they can't be inside a component tag. They are not required. - - -**Game list attributes:** - -`` - the hex font color to use for games on the GuiGameList. - -`` - the hex font color to use for folders on the GuiGameList. - -`` - the hex font color to use for the description on the GuiGameList. - -`` - the hex color to use for the "selector bar" on the GuiGameList. Default is `000000FF`. - -`` - the hex color to use for selected text on the GuiGameList. Default is zero, which means no change. - -`` - if present, the games list names will be left aligned to the value of `` + ``. On by default for detailed themes. - -`` - if present, the system name header won't be displayed (useful for replacing it with an image). If you're making a complete custom theme, you probably want to use this. - -`` - if present, the divider between games on the detailed GuiGameList won't be displayed. - -`` - the percentage to offset the list by. Default is 0.5 (half the screen). **Will also move the selector bar**. - -`` - the percentage to offset the text in the list by. Default is 0.005. Only works in combination with ``. - - - -**Game image attributes:** - -`` - two values for the position of the game art, in the form of `[x] [y]`, as a percentage. Default is `$infoWidth/2 $headerHeight`. - -`` - two values for the dimensions of the game art, in the form of `[width] [height]`, as a percentage of the screen. Default is `$infoWidth 0` (width fits within the info column). The image will only be resized if at least one axis is nonzero *and* exceeded by the image's size. You should always leave at least one axis as zero to preserve the aspect ratio. - -`` - two values for the origin of the game art, in the form of `[x] [y]`, as a percentage. Default is `0.5 0` (top-center of the image). - -`` - path to the image to display if a game's image is missing. '.' and '~' are expanded. - - - -**Fast Select box attributes:** - -`` - the hex color to use for the letter display on the fast select box. - -`` - the path to a "nine patch" image to use for the "background" of the fast select box. See the "Nine Patches" section for more info. - -`` - font definition to use for the fast select letter. See the "Fonts" section for more info. +All paths automatically expand `./` to the folder containing the theme.xml. +All paths automatically expand `~/` to the home directory ($HOME on Linux, %HOMEPATH% on Windows). +Stuff you can define +==================== Fonts ===== Fonts are defined like so: - ``` - - ./path/to/font - 0.05 - + + + ./some/path/here.ttf + + 0.035 + ``` -You can leave off any tags you don't want to use, and they'll use the default. Size is defined as a percentage of the screen height. "." and "~" are expanded for paths. +`` - Default size: 0.045. +`` - Default size: 0.035. -NOTE: If your font size is too big, it'll overrun the maximum OpenGL texture size. ES will attempt to rasterize it in progressively smaller sizes until one fits, then upscale it. +Colors +====== -**Font tags:** +Colors are defined in hex, like this: -`` - font to use for the game list. +`00FF00FF` +or +`00FF00` +(without the alpha channel specified - will assume FF) -`` - font to use for description text. +`` - Default: 0000FFFF. +`` - Default: 00FF00FF. +`` - Default: 000000FF. +`` - Default: 00000000. +`` - Default: 48474DFF. -`` - font to use for the fast select letter. +Images +====== +Images are defined like this: +``` + + ./some/path/here.png + + true + +``` +Pretty much any image format is supported. -Audio -===== +`` - No default. +`` - No default. -Themes can also define menu sounds. These tags go in the root of the `` tree, just like Display tags. Sounds should be in the .wav format. The relative path operator (.) and home operator (~) are properly expanded. +Sounds +====== -`` - path to the sound to play when the game list or fast select menu is scrolling. +Sounds are defined like this: +`./some/path/here.wav` +Only .wav files are supported. -`` - path to the sound to play when the user selects something from the game list. - -`` - path to the sound to play when the user "goes up" from a folder in the game list. - -`` - path to the sound to play when the user opens a menu (either the "main menu" or the fast select menu). +`` - No default. +`` - No default. +`` - No default. +`` - No default. Nine Patches @@ -178,18 +116,5 @@ Nine Patches EmulationStation borrows the concept of "nine patches" from Android (or "9-Slices"). Currently the implementation is very simple and hard-coded to only use 48x48px images (16x16px for each "patch"). Check the `data/resources` directory for some examples (button.png, frame.png). - -List of variables -================= - -Variables can be used in position and dimension definitions. They can be added, subtracted, multiplied, and divided. Parenthesis are valid. They are a percentage of the screen. - -For example, if you wanted to place an image that covered the left half of the screen, up to the game list, you could use `$infoWidth 1`. - -`$headerHeight` - height of the system name header. - -`$infoWidth` - where the left of the game list begins. Will follow ``. - - -Aloshi http://www.aloshi.com diff --git a/src/FileData.cpp b/src/FileData.cpp index 958ecf499..710313f85 100644 --- a/src/FileData.cpp +++ b/src/FileData.cpp @@ -9,8 +9,8 @@ std::string getCleanFileName(const fs::path& path) } -FileData::FileData(FileType type, const fs::path& path) - : mType(type), mPath(path), mParent(NULL), metadata(type == GAME ? GAME_METADATA : FOLDER_METADATA) // metadata is REALLY set in the constructor! +FileData::FileData(FileType type, const fs::path& path, SystemData* system) + : mType(type), mPath(path), mSystem(system), mParent(NULL), metadata(type == GAME ? GAME_METADATA : FOLDER_METADATA) // metadata is REALLY set in the constructor! { // metadata needs at least a name field (since that's what getName() will return) if(metadata.get("name").empty()) @@ -21,6 +21,9 @@ FileData::~FileData() { if(mParent) mParent->removeChild(this); + + while(mChildren.size()) + delete mChildren.back(); } const std::string& FileData::getThumbnailPath() const diff --git a/src/FileData.h b/src/FileData.h index 2ef987dc7..9d9ef0175 100644 --- a/src/FileData.h +++ b/src/FileData.h @@ -5,12 +5,21 @@ #include #include "MetaData.h" +class SystemData; + enum FileType { GAME = 1, // Cannot have children. FOLDER = 2 }; +enum FileChangeType +{ + FILE_ADDED, + FILE_METADATA_CHANGED, + FILE_REMOVED +}; + // Used for loading/saving gamelist.xml. const char* fileTypeToString(FileType type); FileType stringToFileType(const char* str); @@ -21,7 +30,7 @@ std::string getCleanFileName(const boost::filesystem::path& path); class FileData { public: - FileData(FileType type, const boost::filesystem::path& path); + FileData(FileType type, const boost::filesystem::path& path, SystemData* system); virtual ~FileData(); inline const std::string& getName() const { return metadata.get("name"); } @@ -29,6 +38,7 @@ public: inline const boost::filesystem::path& getPath() const { return mPath; } inline FileData* getParent() const { return mParent; } inline const std::vector& getChildren() const { return mChildren; } + inline SystemData* getSystem() const { return mSystem; } virtual const std::string& getThumbnailPath() const; @@ -57,6 +67,7 @@ public: private: FileType mType; boost::filesystem::path mPath; + SystemData* mSystem; FileData* mParent; std::vector mChildren; }; diff --git a/src/GuiComponent.cpp b/src/GuiComponent.cpp index 4f793713e..c4a1fde41 100644 --- a/src/GuiComponent.cpp +++ b/src/GuiComponent.cpp @@ -99,6 +99,9 @@ void GuiComponent::addChild(GuiComponent* cmp) void GuiComponent::removeChild(GuiComponent* cmp) { + if(!cmp->getParent()) + return; + if(cmp->getParent() != this) { LOG(LogError) << "Tried to remove child from incorrect parent!"; diff --git a/src/SystemData.cpp b/src/SystemData.cpp index d162a4b00..e7055d510 100644 --- a/src/SystemData.cpp +++ b/src/SystemData.cpp @@ -35,8 +35,8 @@ SystemData::SystemData(const std::string& name, const std::string& fullName, con mLaunchCommand = command; mPlatformId = platformId; - mRootFolder = new FileData(FOLDER, mStartPath); - mRootFolder->metadata.set("name", "Search Root"); + mRootFolder = new FileData(FOLDER, mStartPath, this); + mRootFolder->metadata.set("name", mFullName); if(!Settings::getInstance()->getBool("PARSEGAMELISTONLY")) populateFolder(mRootFolder); @@ -45,6 +45,9 @@ SystemData::SystemData(const std::string& name, const std::string& fullName, con parseGamelist(this); mRootFolder->sort(FileSorts::SortTypes.at(0)); + + mTheme = std::make_shared(); + mTheme->loadFile(getThemePath()); } SystemData::~SystemData() @@ -178,7 +181,7 @@ void SystemData::populateFolder(FileData* folder) isGame = false; if(std::find(mSearchExtensions.begin(), mSearchExtensions.end(), extension) != mSearchExtensions.end()) { - FileData* newGame = new FileData(GAME, filePath.generic_string()); + FileData* newGame = new FileData(GAME, filePath.generic_string(), this); folder->addChild(newGame); isGame = true; } @@ -186,7 +189,7 @@ void SystemData::populateFolder(FileData* folder) //add directories that also do not match an extension as folders if(!isGame && fs::is_directory(filePath)) { - FileData* newFolder = new FileData(FOLDER, filePath.generic_string()); + FileData* newFolder = new FileData(FOLDER, filePath.generic_string(), this); populateFolder(newFolder); //ignore folders that do not contain games @@ -347,7 +350,19 @@ std::string SystemData::getGamelistPath() const return filePath.generic_string(); } -bool SystemData::hasGamelist() +std::string SystemData::getThemePath() const +{ + fs::path filePath; + + filePath = mRootFolder->getPath() / "theme.xml"; + if(fs::exists(filePath)) + return filePath.generic_string(); + + filePath = getHomePath() + "/.emulationstation/" + getName() + "/theme.xml"; + return filePath.generic_string(); +} + +bool SystemData::hasGamelist() const { return (fs::exists(getGamelistPath())); } diff --git a/src/SystemData.h b/src/SystemData.h index c0f2b0e92..ca6e5bed0 100644 --- a/src/SystemData.h +++ b/src/SystemData.h @@ -7,6 +7,7 @@ #include "Window.h" #include "MetaData.h" #include "PlatformId.h" +#include "ThemeData.h" class SystemData { @@ -21,9 +22,11 @@ public: inline const std::string& getStartPath() const { return mStartPath; } inline const std::vector& getExtensions() const { return mSearchExtensions; } inline PlatformIds::PlatformId getPlatformId() const { return mPlatformId; } + inline const std::shared_ptr& getTheme() const { return mTheme; } std::string getGamelistPath() const; - bool hasGamelist(); + bool hasGamelist() const; + std::string getThemePath() const; unsigned int getGameCount() const; @@ -43,6 +46,7 @@ private: std::vector mSearchExtensions; std::string mLaunchCommand; PlatformIds::PlatformId mPlatformId; + std::shared_ptr mTheme; void populateFolder(FileData* folder); diff --git a/src/ThemeData.cpp b/src/ThemeData.cpp new file mode 100644 index 000000000..63f35bf5a --- /dev/null +++ b/src/ThemeData.cpp @@ -0,0 +1,207 @@ +#include "ThemeData.h" +#include "Renderer.h" +#include "resources/Font.h" +#include "Sound.h" +#include "resources/TextureResource.h" +#include "Log.h" +#include +#include +#include "pugiXML/pugixml.hpp" + +// Defaults +std::map ThemeData::sDefaultFonts = boost::assign::map_list_of + ("listFont", FontDef(0.045f, "")) + ("descriptionFont", FontDef(0.035f, "")); + +std::map ThemeData::sDefaultColors = boost::assign::map_list_of + ("listPrimaryColor", 0x0000FFFF) + ("listSecondaryColor", 0x00FF00FF) + ("listSelectorColor", 0x000000FF) + ("listSelectedColor", 0x00000000) + ("descriptionColor", 0x48474DFF); + +std::map ThemeData::sDefaultImages = boost::assign::map_list_of + ("backgroundImage", ImageDef("", true)) + ("headerImage", ImageDef("", false)); + +std::map ThemeData::sDefaultSounds = boost::assign::map_list_of + ("scrollSound", SoundDef("")) + ("gameSelectSound", SoundDef("")) + ("backSound", SoundDef("")) + ("menuOpenSound", SoundDef("")); + + + +const std::shared_ptr& ThemeData::getDefault() +{ + static const std::shared_ptr def = std::shared_ptr(new ThemeData()); + return def; +} + +ThemeData::ThemeData() +{ + setDefaults(); + + std::string defaultDir = getHomePath() + "/.emulationstation/es_theme_default.xml"; + if(boost::filesystem::exists(defaultDir)) + loadFile(defaultDir); +} + +void ThemeData::setDefaults() +{ + mFontMap.clear(); + mImageMap.clear(); + mColorMap.clear(); + mSoundMap.clear(); + + mFontMap = sDefaultFonts; + mImageMap = sDefaultImages; + mColorMap = sDefaultColors; + mSoundMap = sDefaultSounds; +} + +unsigned int getHexColor(const char* str, unsigned int defaultColor) +{ + if(!str) + return defaultColor; + + size_t len = strlen(str); + if(len != 6 && len != 8) + { + LOG(LogError) << "Invalid theme color \"" << str << "\" (must be 6 or 8 characters)"; + return defaultColor; + } + + unsigned int val; + std::stringstream ss; + ss << str; + ss >> std::hex >> val; + + if(len == 6) + val = (val << 8) | 0xFF; + + return val; +} + +std::string resolvePath(const char* in, const std::string& relative) +{ + if(!in || in[0] == '\0') + return in; + + boost::filesystem::path relPath(relative); + relPath = relPath.parent_path(); + + boost::filesystem::path path(in); + + // we use boost filesystem here instead of just string checks because + // some directories could theoretically start with ~ or . + if(*path.begin() == "~") + { + path = getHomePath() + (in + 1); + }else if(*path.begin() == ".") + { + path = relPath / (in + 1); + } + + return path.generic_string(); +} + +void ThemeData::loadFile(const std::string& themePath) +{ + if(themePath.empty() || !boost::filesystem::exists(themePath)) + return; + + pugi::xml_document doc; + pugi::xml_parse_result result = doc.load_file(themePath.c_str()); + if(!result) + { + LOG(LogWarning) << "Could not parse theme file \"" << themePath << "\":\n " << result.description(); + return; + } + + pugi::xml_node root = doc.child("theme"); + + // Fonts + for(auto it = mFontMap.begin(); it != mFontMap.end(); it++) + { + pugi::xml_node node = root.child(it->first.c_str()); + if(node) + { + std::string path = resolvePath(node.child("path").text().as_string(it->second.path.c_str()), themePath); + if(!boost::filesystem::exists(path)) + { + LOG(LogWarning) << "Font \"" << path << "\" doesn't exist!"; + path = it->second.path; + } + + float size = node.child("size").text().as_float(it->second.size); + mFontMap[it->first] = FontDef(size, path); + root.remove_child(node); + } + } + + // Images + for(auto it = mImageMap.begin(); it != mImageMap.end(); it++) + { + pugi::xml_node node = root.child(it->first.c_str()); + if(node) + { + std::string path = resolvePath(node.child("path").text().as_string(it->second.path.c_str()), themePath); + if(!boost::filesystem::exists(path)) + { + LOG(LogWarning) << "Image \"" << path << "\" doesn't exist!"; + path = it->second.path; + } + + bool tile = node.child("tile").text().as_bool(it->second.tile); + mImageMap[it->first] = ImageDef(path, tile); + root.remove_child(node); + } + } + + // Colors + for(auto it = mColorMap.begin(); it != mColorMap.end(); it++) + { + pugi::xml_node node = root.child(it->first.c_str()); + if(node) + { + mColorMap[it->first] = getHexColor(node.text().as_string(NULL), it->second); + root.remove_child(node); + } + } + + // Sounds + for(auto it = mSoundMap.begin(); it != mSoundMap.end(); it++) + { + pugi::xml_node node = root.child(it->first.c_str()); + if(node) + { + std::string path = resolvePath(node.text().as_string(it->second.path.c_str()), themePath); + if(!boost::filesystem::exists(path)) + { + LOG(LogWarning) << "Sound \"" << path << "\" doesn't exist!"; + path = it->second.path; + } + + mSoundMap[it->first] = SoundDef(path); + root.remove_child(node); + } + } + + if(root.begin() != root.end()) + { + std::stringstream ss; + ss << "Unused theme identifiers:\n"; + for(auto it = root.children().begin(); it != root.children().end(); it++) + { + ss << " " << it->name() << "\n"; + } + + LOG(LogWarning) << ss.str(); + } +} + +void ThemeData::playSound(const std::string& identifier) const +{ + mSoundMap.at(identifier).get()->play(); +} diff --git a/src/ThemeData.h b/src/ThemeData.h new file mode 100644 index 000000000..ea4222801 --- /dev/null +++ b/src/ThemeData.h @@ -0,0 +1,78 @@ +#pragma once + +#include +#include +#include +#include "resources/Font.h" +#include "resources/TextureResource.h" +#include "Renderer.h" +#include "AudioManager.h" +#include "Sound.h" + +struct FontDef +{ + FontDef() {} + FontDef(float sz, const std::string& p) : path(p), size(sz) {} + + std::string path; + float size; + + inline const std::shared_ptr& get() const { if(!font) font = Font::get((int)(size * Renderer::getScreenHeight()), path); return font; } + +private: + mutable std::shared_ptr font; +}; + +struct ImageDef +{ + ImageDef() {} + ImageDef(const std::string& p, bool t) : path(p), tile(t) {} + + std::string path; + bool tile; + + inline const std::shared_ptr& getTexture() const { if(!texture && !path.empty()) texture = TextureResource::get(path); return texture; } + +private: + mutable std::shared_ptr texture; +}; + +struct SoundDef +{ + SoundDef() {} + SoundDef(const std::string& p) : path(p) { sound = std::shared_ptr(new Sound(path)); AudioManager::getInstance()->registerSound(sound); } + + std::string path; + + inline const std::shared_ptr& get() const { return sound; } + +private: + std::shared_ptr sound; +}; + +class ThemeData +{ +public: + static const std::shared_ptr& getDefault(); + + ThemeData(); + + void setDefaults(); + void loadFile(const std::string& path); + + inline std::shared_ptr getFont(const std::string& identifier) const { return mFontMap.at(identifier).get(); } + inline const ImageDef& getImage(const std::string& identifier) const { return mImageMap.at(identifier); } + inline unsigned int getColor(const std::string& identifier) const { return mColorMap.at(identifier); } + void playSound(const std::string& identifier) const; + +private: + static std::map sDefaultImages; + static std::map sDefaultColors; + static std::map sDefaultFonts; + static std::map sDefaultSounds; + + std::map mImageMap; + std::map mColorMap; + std::map mFontMap; + std::map< std::string, SoundDef > mSoundMap; +}; diff --git a/src/Window.cpp b/src/Window.cpp index 847f55e48..c2b2e42f6 100644 --- a/src/Window.cpp +++ b/src/Window.cpp @@ -6,11 +6,14 @@ #include "Log.h" #include "Settings.h" #include +#include "views/ViewController.h" Window::Window() : mNormalizeNextUpdate(false), mFrameTimeElapsed(0), mFrameCountElapsed(0), mAverageDeltaTime(10), mZoomFactor(1.0f), mCenterPoint(0, 0), mMatrix(Eigen::Affine3f::Identity()), mFadePercent(0.0f), mAllowSleep(true) { mInputManager = new InputManager(this); + mViewController = new ViewController(this); + pushGui(mViewController); setCenterPoint(Eigen::Vector2f(Renderer::getScreenWidth() / 2, Renderer::getScreenHeight() / 2)); } @@ -88,8 +91,11 @@ void Window::input(InputConfig* config, Input input) { VolumeControl::getInstance()->setVolume(VolumeControl::getInstance()->getVolume() - 5); } - else if(peekGui()) - this->peekGui()->input(config, input); + else + { + if(peekGui()) + this->peekGui()->input(config, input); + } } void Window::update(int deltaTime) @@ -125,10 +131,6 @@ void Window::update(int deltaTime) void Window::render() { - //there's nothing to render, which should pretty much never happen - if(mGuiStack.size() == 0) - std::cout << "guistack empty\n"; - for(unsigned int i = 0; i < mGuiStack.size(); i++) { mGuiStack.at(i)->render(mMatrix); @@ -148,11 +150,6 @@ void Window::normalizeNextUpdate() mNormalizeNextUpdate = true; } -InputManager* Window::getInputManager() -{ - return mInputManager; -} - void Window::setZoomFactor(const float& zoom) { mZoomFactor = zoom; diff --git a/src/Window.h b/src/Window.h index dcff3f923..757fa2a7b 100644 --- a/src/Window.h +++ b/src/Window.h @@ -3,10 +3,11 @@ #include "GuiComponent.h" #include "InputManager.h" -#include "resources/ResourceManager.h" #include #include "resources/Font.h" +class ViewController; + class Window { public: @@ -24,8 +25,8 @@ public: bool init(unsigned int width = 0, unsigned int height = 0); void deinit(); - InputManager* getInputManager(); - ResourceManager* getResourceManager(); + inline InputManager* getInputManager() { return mInputManager; } + inline ViewController* getViewController() { return mViewController; } void normalizeNextUpdate(); @@ -39,6 +40,7 @@ public: private: InputManager* mInputManager; + ViewController* mViewController; std::vector mGuiStack; std::vector< std::shared_ptr > mDefaultFonts; diff --git a/src/XMLReader.cpp b/src/XMLReader.cpp index fbdf19bd6..b16fca806 100644 --- a/src/XMLReader.cpp +++ b/src/XMLReader.cpp @@ -72,13 +72,13 @@ FileData* createGameFromPath(std::string gameAbsPath, SystemData* system) //the folder didn't already exist, so create it if(!foundFolder) { - FileData* newFolder = new FileData(FOLDER, folder->getPath() / checkName); + FileData* newFolder = new FileData(FOLDER, folder->getPath() / checkName, system); folder->addChild(newFolder); folder = newFolder; } } - FileData* game = new FileData(GAME, gameAbsPath); + FileData* game = new FileData(GAME, gameAbsPath, system); folder->addChild(game); return game; } diff --git a/src/components/AnimationComponent.cpp b/src/components/AnimationComponent.cpp deleted file mode 100644 index c07be41f3..000000000 --- a/src/components/AnimationComponent.cpp +++ /dev/null @@ -1,100 +0,0 @@ -#include "AnimationComponent.h" - -AnimationComponent::AnimationComponent() -{ - mMoveX = 0; - mMoveY = 0; - mMoveSpeed = 0; - mFadeRate = 0; - mOpacity = 0; - mAccumulator = 0; -} - -void AnimationComponent::move(int x, int y, int speed) -{ - mMoveX = x; - mMoveY = y; - mMoveSpeed = speed; -} - -void AnimationComponent::fadeIn(int time) -{ - mOpacity = 0; - setChildrenOpacity(0); - - mFadeRate = time; -} - -void AnimationComponent::fadeOut(int time) -{ - mOpacity = 255; - setChildrenOpacity(255); - - mFadeRate = -time; -} - -//this should really be fixed at the system loop level... -void AnimationComponent::update(int deltaTime) -{ - mAccumulator += deltaTime; - while(mAccumulator >= ANIMATION_TICK_SPEED) - { - mAccumulator -= ANIMATION_TICK_SPEED; - - if(mMoveX != 0 || mMoveY != 0) - { - Eigen::Vector2i offset(mMoveX, mMoveY); - if(abs(offset.x()) > mMoveSpeed) - offset.x() = mMoveSpeed * (offset.x() > 0 ? 1 : -1); - if(abs(offset.y()) > mMoveSpeed) - offset.y() = mMoveSpeed * (offset.y() > 0 ? 1 : -1); - - moveChildren(offset.x(), offset.y()); - - mMoveX -= offset.x(); - mMoveY -= offset.y(); - } - - if(mFadeRate != 0) - { - int opacity = (int)mOpacity + mFadeRate; - if(opacity > 255) - { - mFadeRate = 0; - opacity = 255; - } - - if(opacity < 0) - { - mFadeRate = 0; - opacity = 0; - } - - mOpacity = (unsigned char)opacity; - setChildrenOpacity((unsigned char)opacity); - } - } -} - -void AnimationComponent::addChild(GuiComponent* gui) -{ - mChildren.push_back(gui); -} - -void AnimationComponent::moveChildren(int offsetx, int offsety) -{ - Eigen::Vector3f move((float)offsetx, (float)offsety, 0); - for(unsigned int i = 0; i < mChildren.size(); i++) - { - GuiComponent* comp = mChildren.at(i); - comp->setPosition(comp->getPosition() + move); - } -} - -void AnimationComponent::setChildrenOpacity(unsigned char opacity) -{ - for(unsigned int i = 0; i < mChildren.size(); i++) - { - mChildren.at(i)->setOpacity(opacity); - } -} diff --git a/src/components/AnimationComponent.h b/src/components/AnimationComponent.h deleted file mode 100644 index ce37beb8b..000000000 --- a/src/components/AnimationComponent.h +++ /dev/null @@ -1,39 +0,0 @@ -#ifndef _ANIMATIONCOMPONENT_H_ -#define _ANIMATIONCOMPONENT_H_ - -#include "../GuiComponent.h" -#include - -#define ANIMATION_TICK_SPEED 16 - -//just fyi, this is easily the worst animation system i've ever written. -//it was mostly written during a single lecture and it really shows in how un-thought-out it is -//it also hasn't been converted to use floats or vectors yet -class AnimationComponent -{ -public: - AnimationComponent(); - - void move(int x, int y, int speed); - void fadeIn(int time); - void fadeOut(int time); - - void update(int deltaTime); - - void addChild(GuiComponent* gui); - -private: - unsigned char mOpacity; - - std::vector mChildren; - - void moveChildren(int offsetx, int offsety); - void setChildrenOpacity(unsigned char opacity); - - int mFadeRate; - int mMoveX, mMoveY, mMoveSpeed; - - int mAccumulator; -}; - -#endif diff --git a/src/components/GuiFastSelect.cpp b/src/components/GuiFastSelect.cpp deleted file mode 100644 index 3a59398c5..000000000 --- a/src/components/GuiFastSelect.cpp +++ /dev/null @@ -1,185 +0,0 @@ -#include "GuiFastSelect.h" -#include "../Renderer.h" -#include -#include "GuiGameList.h" -#include "../FileSorts.h" - -#define DEFAULT_FS_IMAGE ":/frame.png" - -const std::string GuiFastSelect::LETTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; -const int GuiFastSelect::SCROLLSPEED = 100; -const int GuiFastSelect::SCROLLDELAY = 507; - -int GuiFastSelect::sortTypeId = 0; - -GuiFastSelect::GuiFastSelect(Window* window, GuiGameList* parent, TextListComponent* list, char startLetter, ThemeComponent * theme) - : GuiComponent(window), mParent(parent), mList(list), mTheme(theme), mBox(mWindow, "") -{ - mLetterID = LETTERS.find(toupper(startLetter)); - if(mLetterID == std::string::npos) - mLetterID = 0; - - mScrollSound = mTheme->getSound("menuScroll"); - mTextColor = mTheme->getColor("fastSelect"); - - mScrolling = false; - mScrollTimer = 0; - mScrollOffset = 0; - - unsigned int sw = Renderer::getScreenWidth(), sh = Renderer::getScreenHeight(); - - - if(theme->getString("fastSelectFrame").empty()) - { - mBox.setImagePath(DEFAULT_FS_IMAGE); - //mBox.setEdgeColor(0x0096ffFF); - mBox.setEdgeColor(0x005493FF); - mBox.setCenterColor(0x5e5e5eFF); - }else{ - mBox.setImagePath(theme->getString("fastSelectFrame")); - } - - mBox.setPosition(sw * 0.2f, sh * 0.2f); - mBox.setSize(sw * 0.6f, sh * 0.6f); -} - -GuiFastSelect::~GuiFastSelect() -{ - mParent->updateDetailData(); -} - -void GuiFastSelect::render(const Eigen::Affine3f& parentTrans) -{ - Eigen::Affine3f trans = parentTrans * getTransform(); - - unsigned int sw = Renderer::getScreenWidth(), sh = Renderer::getScreenHeight(); - - mBox.render(trans); - - Renderer::setMatrix(trans); - std::shared_ptr letterFont = mTheme->getFastSelectFont(); - std::shared_ptr subtextFont = mTheme->getDescriptionFont(); - - letterFont->drawCenteredText(LETTERS.substr(mLetterID, 1), 0, sh * 0.5f - (letterFont->getHeight() * 0.5f), mTextColor); - subtextFont->drawCenteredText("Sort order:", 0, sh * 0.6f - (subtextFont->getHeight() * 0.5f), mTextColor); - - std::string sortString = "<- " + FileSorts::SortTypes.at(sortTypeId).description + " ->"; - subtextFont->drawCenteredText(sortString, 0, sh * 0.6f + (subtextFont->getHeight() * 0.5f), mTextColor); -} - -bool GuiFastSelect::input(InputConfig* config, Input input) -{ - if(config->isMappedTo("up", input) && input.value != 0) - { - mScrollOffset = -1; - scroll(); - return true; - } - - if(config->isMappedTo("down", input) && input.value != 0) - { - mScrollOffset = 1; - scroll(); - return true; - } - - if(config->isMappedTo("left", input) && input.value != 0) - { - sortTypeId--; - if(sortTypeId < 0) - sortTypeId = FileSorts::SortTypes.size() - 1; - - mScrollSound->play(); - return true; - } - else if(config->isMappedTo("right", input) && input.value != 0) - { - sortTypeId = (sortTypeId + 1) % FileSorts::SortTypes.size(); - mScrollSound->play(); - return true; - } - - if((config->isMappedTo("up", input) || config->isMappedTo("down", input)) && input.value == 0) - { - mScrolling = false; - mScrollTimer = 0; - mScrollOffset = 0; - return true; - } - - if(config->isMappedTo("select", input) && input.value == 0) - { - setListPos(); - mParent->sort(FileSorts::SortTypes.at(sortTypeId)); - delete this; - return true; - } - - return false; -} - -void GuiFastSelect::update(int deltaTime) -{ - if(mScrollOffset != 0) - { - mScrollTimer += deltaTime; - - if(!mScrolling && mScrollTimer >= SCROLLDELAY) - { - mScrolling = true; - mScrollTimer = SCROLLSPEED; - } - - if(mScrolling && mScrollTimer >= SCROLLSPEED) - { - mScrollTimer = 0; - scroll(); - } - } -} - -void GuiFastSelect::scroll() -{ - setLetterID(mLetterID + mScrollOffset); - mScrollSound->play(); -} - -void GuiFastSelect::setLetterID(int id) -{ - while(id < 0) - id += LETTERS.length(); - while(id >= (int)LETTERS.length()) - id -= LETTERS.length(); - - mLetterID = (size_t)id; -} - -void GuiFastSelect::setListPos() -{ - char letter = LETTERS[mLetterID]; - - int min = 0; - int max = mList->getObjectCount() - 1; - - int mid = 0; - - while(max >= min) - { - mid = ((max - min) / 2) + min; - - char checkLetter = toupper(mList->getObject(mid)->getName()[0]); - - if(checkLetter < letter) - { - min = mid + 1; - }else if(checkLetter > letter) - { - max = mid - 1; - }else{ - //exact match found - break; - } - } - - mList->setSelection(mid); -} diff --git a/src/components/GuiFastSelect.h b/src/components/GuiFastSelect.h deleted file mode 100644 index 02cec1c03..000000000 --- a/src/components/GuiFastSelect.h +++ /dev/null @@ -1,50 +0,0 @@ -#ifndef _GUIFASTSELECT_H_ -#define _GUIFASTSELECT_H_ - -#include "../GuiComponent.h" -#include "../SystemData.h" -#include "../Sound.h" -#include "ThemeComponent.h" -#include "TextListComponent.h" -#include "NinePatchComponent.h" - -class GuiGameList; - - -class GuiFastSelect : public GuiComponent -{ -public: - GuiFastSelect(Window* window, GuiGameList* parent, TextListComponent* list, char startLetter, ThemeComponent* theme); - ~GuiFastSelect(); - - bool input(InputConfig* config, Input input) override; - void update(int deltaTime) override; - void render(const Eigen::Affine3f& parentTrans) override; - -private: - static const std::string LETTERS; - static const int SCROLLSPEED; - static const int SCROLLDELAY; - - static int sortTypeId; - - void setListPos(); - void scroll(); - void setLetterID(int id); - - GuiGameList* mParent; - TextListComponent* mList; - ThemeComponent * mTheme; - NinePatchComponent mBox; - - size_t mLetterID; - - unsigned int mTextColor; - - int mScrollTimer, mScrollOffset; - bool mScrolling; - - std::shared_ptr mScrollSound; -}; - -#endif diff --git a/src/components/GuiGameList.cpp b/src/components/GuiGameList.cpp deleted file mode 100644 index 59e21c545..000000000 --- a/src/components/GuiGameList.cpp +++ /dev/null @@ -1,505 +0,0 @@ -#include "GuiGameList.h" -#include "../InputManager.h" -#include -#include "GuiMenu.h" -#include "GuiFastSelect.h" -#include -#include "../Log.h" -#include "../Settings.h" - -#include "GuiMetaDataEd.h" -#include "GuiScraperStart.h" - -Eigen::Vector3f GuiGameList::getImagePos() -{ - return Eigen::Vector3f(Renderer::getScreenWidth() * mTheme->getFloat("gameImageOffsetX"), Renderer::getScreenHeight() * mTheme->getFloat("gameImageOffsetY"), 0.0f); -} - -bool GuiGameList::isDetailed() const -{ - if(!mFolder) - return false; - - //return true if any game has an image specified - for(auto it = mFolder->getChildren().begin(); it != mFolder->getChildren().end(); it++) - { - if(!(*it)->getThumbnailPath().empty()) - return true; - } - - return false; -} - -GuiGameList::GuiGameList(Window* window) : GuiComponent(window), - mTheme(new ThemeComponent(mWindow)), - mList(window, 0.0f, 0.0f, Font::get(FONT_SIZE_MEDIUM)), - mScreenshot(window), - mDescription(window), - mRating(window), - mReleaseDateLabel(window), - mReleaseDate(window), - mDescContainer(window), - mTransitionImage(window, 0.0f, 0.0f, "", (float)Renderer::getScreenWidth(), (float)Renderer::getScreenHeight(), true), - mHeaderText(mWindow), - mLockInput(false), - mEffectFunc(NULL), mEffectTime(0), mGameLaunchEffectLength(700) -{ - mImageAnimation.addChild(&mScreenshot); - mDescContainer.addChild(&mReleaseDateLabel); - mDescContainer.addChild(&mReleaseDate); - mDescContainer.addChild(&mRating); - mDescContainer.addChild(&mDescription); - - //scale delay with screen width (higher width = more text per line) - //the scroll speed is automatically scaled by component size - mDescContainer.setAutoScroll((int)(1500 + (Renderer::getScreenWidth() * 0.5)), 0.025f); - - mTransitionImage.setPosition((float)Renderer::getScreenWidth(), 0); - mTransitionImage.setOrigin(0, 0); - - mHeaderText.setColor(0xFF0000FF); - mHeaderText.setFont(Font::get(FONT_SIZE_LARGE)); - mHeaderText.setPosition(0, 1); - mHeaderText.setSize((float)Renderer::getScreenWidth(), 0); - mHeaderText.setCentered(true); - - addChild(mTheme); - addChild(&mHeaderText); - addChild(&mScreenshot); - addChild(&mDescContainer); - addChild(&mList); - addChild(&mTransitionImage); - - mTransitionAnimation.addChild(this); - - setSystemId(0); -} - -GuiGameList::~GuiGameList() -{ - delete mTheme; -} - -void GuiGameList::setSystemId(int id) -{ - if(SystemData::sSystemVector.size() == 0) - { - LOG(LogError) << "Error - no systems found!"; - return; - } - - //make sure the id is within range - if(id >= (int)SystemData::sSystemVector.size()) - id -= SystemData::sSystemVector.size(); - if(id < 0) - id += SystemData::sSystemVector.size(); - - mSystemId = id; - mSystem = SystemData::sSystemVector.at(mSystemId); - - //clear the folder stack - while(mFolderStack.size()){ mFolderStack.pop(); } - - mFolder = mSystem->getRootFolder(); - - updateTheme(); - updateList(); - updateDetailData(); - mWindow->normalizeNextUpdate(); //image loading can be slow -} - -void GuiGameList::render(const Eigen::Affine3f& parentTrans) -{ - Eigen::Affine3f trans = parentTrans * getTransform(); - renderChildren(trans); -} - -bool GuiGameList::input(InputConfig* config, Input input) -{ - if(mLockInput) - return false; - - mList.input(config, input); - - if(input.id == SDLK_F3) - { - FileData* game = mList.getSelectedObject(); - if(game->getType() == GAME) - { - FileData* root = mSystem->getRootFolder(); - ScraperSearchParams searchParams; - searchParams.game = game; - searchParams.system = mSystem; - mWindow->pushGui(new GuiMetaDataEd(mWindow, &game->metadata, game->metadata.getMDD(), searchParams, game->getPath().stem().string(), - [&] { updateDetailData(); }, - [game, root, this] { - boost::filesystem::remove(game->getPath()); - root->removeChild(game); - updateList(); - })); - } - return true; - } - - if(input.id == SDLK_F5) - { - mWindow->pushGui(new GuiScraperStart(mWindow)); - return true; - } - - if(config->isMappedTo("a", input) && input.value != 0) - { - //play select sound - mTheme->getSound("menuSelect")->play(); - - FileData* file = mList.getSelectedObject(); - if(file->getType() == FOLDER) //if you selected a folder, add this directory to the stack, and use the selected one - { - mFolderStack.push(mFolder); - mFolder = file; - updateList(); - updateDetailData(); - return true; - }else{ - mList.stopScrolling(); - - mEffectFunc = &GuiGameList::updateGameLaunchEffect; - mEffectTime = 0; - mGameLaunchEffectLength = (int)mTheme->getSound("menuSelect")->getLengthMS(); - if(mGameLaunchEffectLength < 800) - mGameLaunchEffectLength = 800; - - mLockInput = true; - - return true; - } - } - - //if there's something on the directory stack, return to it - if(config->isMappedTo("b", input) && input.value != 0 && mFolderStack.size()) - { - mFolder = mFolderStack.top(); - mFolderStack.pop(); - updateList(); - updateDetailData(); - - //play the back sound - mTheme->getSound("menuBack")->play(); - - return true; - } - - //only allow switching systems if more than one exists (otherwise it'll reset your position when you switch and it's annoying) - if(SystemData::sSystemVector.size() > 1 && input.value != 0) - { - if(config->isMappedTo("right", input)) - { - setSystemId(mSystemId + 1); - doTransition(-1); - return true; - } - if(config->isMappedTo("left", input)) - { - setSystemId(mSystemId - 1); - doTransition(1); - return true; - } - } - - //open the "start menu" - if(config->isMappedTo("menu", input) && input.value != 0) - { - mWindow->pushGui(new GuiMenu(mWindow, this)); - return true; - } - - //open the fast select menu - if(config->isMappedTo("select", input) && input.value != 0) - { - mWindow->pushGui(new GuiFastSelect(mWindow, this, &mList, mList.getSelectedObject()->getName()[0], mTheme)); - return true; - } - - if(isDetailed()) - { - if(config->isMappedTo("up", input) || config->isMappedTo("down", input) || config->isMappedTo("pageup", input) || config->isMappedTo("pagedown", input)) - { - if(input.value == 0) - updateDetailData(); - else - hideDetailData(); - } - return true; - } - - return false; -} - -void GuiGameList::sort(const FileData::SortType& type) -{ - //resort list and update it - mFolder->sort(type); - updateList(); - updateDetailData(); -} - -void GuiGameList::updateList() -{ - mList.clear(); - - for(auto it = mFolder->getChildren().begin(); it != mFolder->getChildren().end(); it++) - { - if((*it)->getType() == FOLDER) - mList.addObject((*it)->getName(), *it, mTheme->getColor("secondary")); - else - mList.addObject((*it)->getName(), *it, mTheme->getColor("primary")); - } -} - -std::string GuiGameList::getThemeFile() -{ - std::string themePath; - - themePath = getHomePath(); - themePath += "/.emulationstation/" + mSystem->getName() + "/theme.xml"; - if(boost::filesystem::exists(themePath)) - return themePath; - - themePath = mSystem->getStartPath() + "/theme.xml"; - if(boost::filesystem::exists(themePath)) - return themePath; - - themePath = getHomePath(); - themePath += "/.emulationstation/es_theme.xml"; - if(boost::filesystem::exists(themePath)) - return themePath; - - return ""; -} - -void GuiGameList::updateTheme() -{ - mTheme->readXML(getThemeFile(), isDetailed()); - - mList.setSelectorColor(mTheme->getColor("selector")); - mList.setSelectedTextColor(mTheme->getColor("selected")); - mList.setScrollSound(mTheme->getSound("menuScroll")); - - mList.setFont(mTheme->getListFont()); - mList.setPosition(0.0f, Font::get(FONT_SIZE_LARGE)->getHeight() + 2.0f); - - if(!mTheme->getBool("hideHeader")) - { - mHeaderText.setText(mSystem->getFullName()); - }else{ - mHeaderText.setText(""); - } - - if(isDetailed()) - { - mList.setCentered(mTheme->getBool("listCentered")); - - mList.setPosition(mTheme->getFloat("listOffsetX") * Renderer::getScreenWidth(), mList.getPosition().y()); - mList.setTextOffsetX((int)(mTheme->getFloat("listTextOffsetX") * Renderer::getScreenWidth())); - - mScreenshot.setPosition(mTheme->getFloat("gameImageOffsetX") * Renderer::getScreenWidth(), mTheme->getFloat("gameImageOffsetY") * Renderer::getScreenHeight()); - mScreenshot.setOrigin(mTheme->getFloat("gameImageOriginX"), mTheme->getFloat("gameImageOriginY")); - mScreenshot.setResize(mTheme->getFloat("gameImageWidth") * Renderer::getScreenWidth(), mTheme->getFloat("gameImageHeight") * Renderer::getScreenHeight(), false); - - mReleaseDateLabel.setColor(mTheme->getColor("description")); - mReleaseDateLabel.setFont(mTheme->getDescriptionFont()); - mReleaseDate.setColor(mTheme->getColor("description")); - mReleaseDate.setFont(mTheme->getDescriptionFont()); - - mDescription.setColor(mTheme->getColor("description")); - mDescription.setFont(mTheme->getDescriptionFont()); - }else{ - mList.setCentered(true); - mList.setPosition(0, mList.getPosition().y()); - mList.setTextOffsetX(0); - } -} - -void GuiGameList::updateDetailData() -{ - if(!isDetailed() || !mList.getSelectedObject() || mList.getSelectedObject()->getType() == FOLDER) - { - hideDetailData(); - }else{ - if(mDescContainer.getParent() != this) - addChild(&mDescContainer); - - FileData* game = mList.getSelectedObject(); - - //set image to either "not found" image or metadata image - if(!boost::filesystem::exists(game->metadata.get("image"))) - { - //image doesn't exist - if(mTheme->getString("imageNotFoundPath").empty()) - { - //"not found" image doesn't exist - mScreenshot.setImage(""); - mScreenshot.setSize(0, 0); //clear old size - }else{ - mScreenshot.setImage(mTheme->getString("imageNotFoundPath")); - } - }else{ - mScreenshot.setImage(game->metadata.get("image")); - } - - Eigen::Vector3f imgOffset = Eigen::Vector3f(Renderer::getScreenWidth() * 0.10f, 0, 0); - mScreenshot.setPosition(getImagePos() - imgOffset); - - mImageAnimation.fadeIn(35); - mImageAnimation.move((int)imgOffset.x(), (int)imgOffset.y(), 20); - - mDescContainer.setPosition(Eigen::Vector3f(Renderer::getScreenWidth() * 0.03f, getImagePos().y() + mScreenshot.getSize().y() + 12, 0)); - mDescContainer.setSize(Eigen::Vector2f(Renderer::getScreenWidth() * (mTheme->getFloat("listOffsetX") - 0.03f), Renderer::getScreenHeight() - mDescContainer.getPosition().y())); - mDescContainer.setScrollPos(Eigen::Vector2d(0, 0)); - mDescContainer.resetAutoScrollTimer(); - - const float colwidth = mDescContainer.getSize().x(); - float ratingHeight = colwidth * 0.3f / 5.0f; - mRating.setSize(ratingHeight * 5.0f, ratingHeight); - - mReleaseDateLabel.setPosition(0, 0); - mReleaseDateLabel.setText("Released: "); - mReleaseDate.setPosition(mReleaseDateLabel.getPosition().x() + mReleaseDateLabel.getSize().x(), mReleaseDateLabel.getPosition().y()); - mReleaseDate.setValue(game->metadata.get("releasedate")); - - mRating.setPosition(colwidth - mRating.getSize().x() - 12, 0); - mRating.setValue(game->metadata.get("rating")); - - mDescription.setPosition(0, mRating.getSize().y()); - mDescription.setSize(Eigen::Vector2f(Renderer::getScreenWidth() * (mTheme->getFloat("listOffsetX") - 0.03f), 0)); - mDescription.setText(game->metadata.get("desc")); - } -} - -void GuiGameList::hideDetailData() -{ - if(mDescContainer.getParent() == this) - removeChild(&mDescContainer); - - mImageAnimation.fadeOut(35); -} - -GuiGameList* GuiGameList::create(Window* window) -{ - GuiGameList* list = new GuiGameList(window); - window->pushGui(list); - return list; -} - -void GuiGameList::update(int deltaTime) -{ - mTransitionAnimation.update(deltaTime); - mImageAnimation.update(deltaTime); - - if(mEffectFunc != NULL) - { - mEffectTime += deltaTime; - (this->*mEffectFunc)(mEffectTime); - } - - GuiComponent::update(deltaTime); -} - -void GuiGameList::doTransition(int dir) -{ - mTransitionImage.copyScreen(); - mTransitionImage.setOpacity(255); - - //put the image of what's currently onscreen at what will be (in screen coords) 0, 0 - mTransitionImage.setPosition((float)Renderer::getScreenWidth() * dir, 0); - - //move the entire thing offscreen so we'll move into place - setPosition((float)Renderer::getScreenWidth() * -dir, mPosition[1]); - - mTransitionAnimation.move(Renderer::getScreenWidth() * dir, 0, 50); -} - -float lerpFloat(const float& start, const float& end, float t) -{ - if(t <= 0) - return start; - if(t >= 1) - return end; - - return (start * (1 - t) + end * t); -} - -Eigen::Vector2f lerpVector2f(const Eigen::Vector2f& start, const Eigen::Vector2f& end, float t) -{ - if(t <= 0) - return start; - if(t >= 1) - return end; - - return (start * (1 - t) + end * t); -} - -float clamp(float min, float max, float val) -{ - if(val < min) - val = min; - else if(val > max) - val = max; - - return val; -} - -//http://en.wikipedia.org/wiki/Smoothstep -float smoothStep(float edge0, float edge1, float x) -{ - // Scale, and clamp x to 0..1 range - x = clamp(0, 1, (x - edge0)/(edge1 - edge0)); - - // Evaluate polynomial - return x*x*x*(x*(x*6 - 15) + 10); -} - -void GuiGameList::updateGameLaunchEffect(int t) -{ - const int endTime = mGameLaunchEffectLength; - - const int zoomTime = endTime; - const int centerTime = endTime - 50; - - const int fadeDelay = endTime - 600; - const int fadeTime = endTime - fadeDelay - 100; - - Eigen::Vector2f imageCenter(mScreenshot.getCenter()); - if(!isDetailed()) - { - imageCenter << mList.getPosition().x() + mList.getSize().x() / 2, mList.getPosition().y() + mList.getSize().y() / 2; - } - - const Eigen::Vector2f centerStart(Renderer::getScreenWidth() / 2, Renderer::getScreenHeight() / 2); - - //remember to clamp or zoom factor will be incorrect with a negative t because squared - const float tNormalized = clamp(0, 1, (float)t / endTime); - - mWindow->setCenterPoint(lerpVector2f(centerStart, imageCenter, smoothStep(0.0, 1.0, tNormalized))); - mWindow->setZoomFactor(lerpFloat(1.0f, 3.0f, tNormalized*tNormalized)); - mWindow->setFadePercent(lerpFloat(0.0f, 1.0f, (float)(t - fadeDelay) / fadeTime)); - - if(t > endTime) - { - //effect done - mTransitionImage.setImage(""); //fixes "tried to bind uninitialized texture!" since copyScreen()'d textures don't reinit - mSystem->launchGame(mWindow, mList.getSelectedObject()); - mEffectFunc = &GuiGameList::updateGameReturnEffect; - mEffectTime = 0; - mGameLaunchEffectLength = 700; - mLockInput = false; - } -} - -void GuiGameList::updateGameReturnEffect(int t) -{ - updateGameLaunchEffect(mGameLaunchEffectLength - t); - - if(t >= mGameLaunchEffectLength) - mEffectFunc = NULL; -} diff --git a/src/components/GuiGameList.h b/src/components/GuiGameList.h deleted file mode 100644 index 7d29923b3..000000000 --- a/src/components/GuiGameList.h +++ /dev/null @@ -1,81 +0,0 @@ -#ifndef _GUIGAMELIST_H_ -#define _GUIGAMELIST_H_ - -#include "../GuiComponent.h" -#include "TextListComponent.h" -#include "ImageComponent.h" -#include "ThemeComponent.h" -#include "AnimationComponent.h" -#include "TextComponent.h" -#include -#include -#include "../SystemData.h" -#include "ScrollableContainer.h" -#include "RatingComponent.h" -#include "DateTimeComponent.h" - -//This is where the magic happens - GuiGameList is the parent of almost every graphical element in ES at the moment. -//It has a TextListComponent child that handles the game list, a ThemeComponent that handles the theming system, and an ImageComponent for game images. -class GuiGameList : public GuiComponent -{ -public: - GuiGameList(Window* window); - virtual ~GuiGameList(); - - void setSystemId(int id); - - bool input(InputConfig* config, Input input) override; - void update(int deltaTime) override; - void render(const Eigen::Affine3f& parentTrans) override; - - void updateDetailData(); - - void sort(const FileData::SortType& type); - - static GuiGameList* create(Window* window); - - bool isDetailed() const; - - static const float sInfoWidth; -private: - void updateList(); - void updateTheme(); - void hideDetailData(); - void doTransition(int dir); - - std::string getThemeFile(); - - SystemData* mSystem; - FileData* mFolder; - std::stack mFolderStack; - int mSystemId; - - TextListComponent mList; - ImageComponent mScreenshot; - - TextComponent mDescription; - RatingComponent mRating; - TextComponent mReleaseDateLabel; - DateTimeComponent mReleaseDate; - - ScrollableContainer mDescContainer; - AnimationComponent mImageAnimation; - ThemeComponent* mTheme; - TextComponent mHeaderText; - - ImageComponent mTransitionImage; - AnimationComponent mTransitionAnimation; - - Eigen::Vector3f getImagePos(); - - bool mLockInput; - - void (GuiGameList::*mEffectFunc)(int); - int mEffectTime; - int mGameLaunchEffectLength; - - void updateGameLaunchEffect(int t); - void updateGameReturnEffect(int t); -}; - -#endif diff --git a/src/components/GuiInputConfig.cpp b/src/components/GuiInputConfig.cpp index 3d8439c8f..c09fbad05 100644 --- a/src/components/GuiInputConfig.cpp +++ b/src/components/GuiInputConfig.cpp @@ -2,8 +2,8 @@ #include "../Window.h" #include "../Renderer.h" #include "../resources/Font.h" -#include "GuiGameList.h" #include "../Log.h" +#include "../views/ViewController.h" static const int inputCount = 10; static std::string inputName[inputCount] = { "Up", "Down", "Left", "Right", "A", "B", "Menu", "Select", "PageUp", "PageDown"}; @@ -38,7 +38,7 @@ bool GuiInputConfig::input(InputConfig* config, Input input) mWindow->pushGui(new GuiInputConfig(mWindow, mWindow->getInputManager()->getInputConfigByPlayer(mTargetConfig->getPlayerNum() + 1))); }else{ mWindow->getInputManager()->writeConfig(); - GuiGameList::create(mWindow); + mWindow->getViewController()->goToSystemSelect(); } delete this; return true; diff --git a/src/components/GuiMenu.cpp b/src/components/GuiMenu.cpp deleted file mode 100644 index 18eaffd9f..000000000 --- a/src/components/GuiMenu.cpp +++ /dev/null @@ -1,102 +0,0 @@ -#include "GuiMenu.h" -#include -#include -#include "../Log.h" -#include "../SystemData.h" -#include "GuiGameList.h" -#include "../Settings.h" -#include "GuiSettingsMenu.h" - -GuiMenu::GuiMenu(Window* window, GuiGameList* parent) : GuiComponent(window) -{ - mParent = parent; - - std::shared_ptr font = Font::get(FONT_SIZE_LARGE); - mList = new TextListComponent(mWindow, 0.0f, font->getHeight() + 2.0f, font); - mList->setSelectedTextColor(0x0000FFFF); - populateList(); -} - -GuiMenu::~GuiMenu() -{ - delete mList; -} - -bool GuiMenu::input(InputConfig* config, Input input) -{ - mList->input(config, input); - - if(config->isMappedTo("menu", input) && input.value != 0) - { - delete this; - return true; - } - - if(config->isMappedTo("a", input) && input.value != 0) - { - executeCommand(mList->getSelectedObject()); - return true; - } - - return false; -} - -void GuiMenu::executeCommand(std::string command) -{ - if(command == "exit") - { - //push SDL quit event - SDL_Event* event = new SDL_Event(); - event->type = SDL_QUIT; - SDL_PushEvent(event); - }else if(command == "es_reload") - { - //reload the game list - SystemData::loadConfig(SystemData::getConfigPath(), false); - mParent->setSystemId(0); - }else if(command == "es_settings") - { - mWindow->pushGui(new GuiSettingsMenu(mWindow)); - delete this; - }else{ - if(system(command.c_str()) != 0) - { - LOG(LogWarning) << "(warning: command terminated with nonzero result!)"; - } - } -} - -void GuiMenu::populateList() -{ - mList->clear(); - - //if you want to add your own commands to the menu, here is where you need to change! - //commands added here are called with system() when selected (so are executed as shell commands) - //the method is GuiList::addObject(std::string displayString, std::string commandString, unsigned int displayHexColor); - //the list will automatically adjust as items are added to it, this should be the only area you need to change - //if you want to do something special within ES, override your command in the executeComand() method - - mList->addObject("Settings", "es_settings", 0x0000FFFF); - - mList->addObject("Restart", "sudo shutdown -r now", 0x0000FFFF); - mList->addObject("Shutdown", "sudo shutdown -h now", 0x0000FFFF); - - mList->addObject("Reload", "es_reload", 0x0000FFFF); - - if(!Settings::getInstance()->getBool("DONTSHOWEXIT")) - mList->addObject("Exit", "exit", 0xFF0000FF); //a special case; pushes an SDL quit event to the event stack instead of being called by system() -} - -void GuiMenu::update(int deltaTime) -{ - mList->update(deltaTime); -} - -void GuiMenu::render(const Eigen::Affine3f& parentTrans) -{ - Eigen::Affine3f trans = parentTrans; - Renderer::setMatrix(trans); - - Renderer::drawRect(Renderer::getScreenWidth() / 4, 0, Renderer::getScreenWidth() / 2, Renderer::getScreenHeight(), 0x999999); - mList->render(trans); -} diff --git a/src/components/GuiMenu.h b/src/components/GuiMenu.h deleted file mode 100644 index 4f533a717..000000000 --- a/src/components/GuiMenu.h +++ /dev/null @@ -1,27 +0,0 @@ -#ifndef _GUIMENU_H_ -#define _GUIMENU_H_ - -#include "../GuiComponent.h" -#include "TextListComponent.h" - -class GuiGameList; - -class GuiMenu : public GuiComponent -{ -public: - GuiMenu(Window* window, GuiGameList* parent); - virtual ~GuiMenu(); - - bool input(InputConfig* config, Input input) override; - void update(int deltaTime) override; - void render(const Eigen::Affine3f& parentTrans) override; - -private: - GuiGameList* mParent; - TextListComponent* mList; - - void populateList(); - void executeCommand(std::string command); -}; - -#endif diff --git a/src/components/ImageComponent.cpp b/src/components/ImageComponent.cpp index 6c129099e..41e39502e 100644 --- a/src/components/ImageComponent.cpp +++ b/src/components/ImageComponent.cpp @@ -20,7 +20,7 @@ Eigen::Vector2f ImageComponent::getCenter() const } ImageComponent::ImageComponent(Window* window, float offsetX, float offsetY, std::string path, float targetWidth, float targetHeight, bool allowUpscale) : GuiComponent(window), - mTiled(false), mAllowUpscale(allowUpscale), mFlipX(false), mFlipY(false), mOrigin(0.5, 0.5), mTargetSize(targetWidth, targetHeight), mColorShift(0xFFFFFFFF) + mTiled(false), mAllowUpscale(allowUpscale), mFlipX(false), mFlipY(false), mOrigin(0.0, 0.0), mTargetSize(targetWidth, targetHeight), mColorShift(0xFFFFFFFF) { setPosition(offsetX, offsetY); @@ -70,16 +70,20 @@ void ImageComponent::resize() void ImageComponent::setImage(std::string path) { - mPath = path; - - if(mPath.empty() || !ResourceManager::getInstance()->fileExists(mPath)) + if(path.empty() || !ResourceManager::getInstance()->fileExists(path)) mTexture.reset(); else - mTexture = TextureResource::get(mPath); + mTexture = TextureResource::get(path); resize(); } +void ImageComponent::setImage(const std::shared_ptr& texture) +{ + mTexture = texture; + resize(); +} + void ImageComponent::setImage(const char* path, size_t length) { mTexture.reset(); @@ -227,7 +231,7 @@ void ImageComponent::drawImageArray(GLfloat* points, GLfloat* texs, GLubyte* col bool ImageComponent::hasImage() { - return !mPath.empty(); + return (bool)mTexture; } void ImageComponent::copyScreen() diff --git a/src/components/ImageComponent.h b/src/components/ImageComponent.h index df0d1ece8..ce3dd9c3c 100644 --- a/src/components/ImageComponent.h +++ b/src/components/ImageComponent.h @@ -21,6 +21,7 @@ public: void copyScreen(); //Copy the entire screen into a texture for us to use. void setImage(std::string path); //Loads the image at the given filepath. void setImage(const char* image, size_t length); //Loads image from memory. + void setImage(const std::shared_ptr& texture); //Use an already existing texture. void setOrigin(float originX, float originY); //Sets the origin as a percentage of this image (e.g. (0, 0) is top left, (0.5, 0.5) is the center) void setTiling(bool tile); //Enables or disables tiling. Must be called before loading an image or resizing will be weird. void setResize(float width, float height, bool allowUpscale); @@ -48,8 +49,6 @@ private: void buildImageArray(int x, int y, GLfloat* points, GLfloat* texs, float percentageX = 1, float percentageY = 1); //writes 12 GLfloat points and 12 GLfloat texture coordinates to a given array at a given position void drawImageArray(GLfloat* points, GLfloat* texs, GLubyte* colors, unsigned int count = 6); //draws the given set of points and texture coordinates, number of coordinate pairs may be specified (default 6) - std::string mPath; - unsigned int mColorShift; std::shared_ptr mTexture; diff --git a/src/components/TextListComponent.h b/src/components/TextListComponent.h index df9bb222d..9a9e9f906 100644 --- a/src/components/TextListComponent.h +++ b/src/components/TextListComponent.h @@ -10,102 +10,98 @@ #include #include "../Sound.h" #include "../Log.h" +#include "../ThemeData.h" +#include -#define MARQUEE_DELAY 900 -#define MARQUEE_SPEED 16 -#define MARQUEE_RATE 3 +#define THEME_FONT "listFont" +#define THEME_SELECTOR_COLOR "listSelectorColor" +#define THEME_HIGHLIGHTED_COLOR "listSelectedColor" +#define THEME_SCROLL_SOUND "listScrollSound" +static const int THEME_COLOR_ID_COUNT = 2; +static const char* const THEME_ENTRY_COLOR[THEME_COLOR_ID_COUNT] = { "listPrimaryColor", "listSecondaryColor" }; //A graphical list. Supports multiple colors for rows and scrolling. template class TextListComponent : public GuiComponent { public: - TextListComponent(Window* window, float offsetX, float offsetY, std::shared_ptr font); + TextListComponent(Window* window); virtual ~TextListComponent(); bool input(InputConfig* config, Input input) override; void update(int deltaTime) override; void render(const Eigen::Affine3f& parentTrans) override; - void onPositionChanged() override; - - void addObject(std::string name, T obj, unsigned int color = 0xFF0000); - void clear(); - - std::string getSelectedName(); - T getSelectedObject(); - int getSelection(); - void stopScrolling(); - bool isScrolling(); - - void setSelectorColor(unsigned int selectorColor); - void setSelectedTextColor(unsigned int selectedColor); - void setCentered(bool centered); - void setScrollSound(std::shared_ptr & sound); - void setTextOffsetX(int textoffsetx); - - int getObjectCount(); - T getObject(int i); - void setSelection(int i); - - void setFont(std::shared_ptr f); - -private: - static const int SCROLLDELAY = 507; - static const int SCROLLTIME = 200; - - void scroll(); //helper method, scrolls in whatever direction scrollDir is - void setScrollDir(int val); //helper method, set mScrollDir as well as reset marquee stuff - - int mScrollDir, mScrollAccumulator; - bool mScrolling; - - int mMarqueeOffset; - int mMarqueeTime; - - std::shared_ptr mFont; - unsigned int mSelectorColor, mSelectedTextColorOverride; - bool mDrawCentered; - - int mTextOffsetX; - struct ListRow { std::string name; T object; - unsigned int color; + unsigned int colorId; }; + void add(const std::string& name, const T& obj, unsigned int colorId); + void remove(const T& obj); + void clear(); + + inline const std::string& getSelectedName() const { return mRowVector.at(mCursor).name; } + inline T getSelected() const { return mRowVector.at(mCursor).object; } + inline const std::vector& getList() const { return mRowVector; } + + void setCursor(const T& select); + + void stopScrolling(); + inline bool isScrolling() const { return mScrollDir != 0; } + + inline void setTheme(const std::shared_ptr& theme) { mTheme = theme; } + inline void setCentered(bool centered) { mCentered = centered; } + + enum CursorState { + CURSOR_STOPPED, + CURSOR_SCROLLING + }; + + inline void setCursorChangedCallback(const std::function& func) { mCursorChangedCallback = func; } + +private: + static const int MARQUEE_DELAY = 900; + static const int MARQUEE_SPEED = 16; + static const int MARQUEE_RATE = 3; + + static const int SCROLL_DELAY = 507; + static const int SCROLL_TIME = 150; + + void scroll(); //helper method, scrolls in whatever direction scrollDir is + void setScrollDir(int val); //helper method, set mScrollDir as well as reset marquee stuff + void onCursorChanged(CursorState state); + + int mScrollDir, mScrollAccumulator; + + int mMarqueeOffset; + int mMarqueeTime; + + std::shared_ptr mTheme; + bool mCentered; + std::vector mRowVector; - int mSelection; - std::shared_ptr mScrollSound; + int mCursor; + + std::function mCursorChangedCallback; }; template -TextListComponent::TextListComponent(Window* window, float offsetX, float offsetY, std::shared_ptr font) : GuiComponent(window) +TextListComponent::TextListComponent(Window* window) : + GuiComponent(window) { - mSelection = 0; + mCursor = 0; mScrollDir = 0; - mScrolling = 0; mScrollAccumulator = 0; - setPosition(offsetX, offsetY); - mMarqueeOffset = 0; mMarqueeTime = -MARQUEE_DELAY; - mTextOffsetX = 0; - mFont = font; - mSelectorColor = 0x000000FF; - mSelectedTextColorOverride = 0; - mScrollSound = NULL; - mDrawCentered = true; -} + mCentered = true; -template -void TextListComponent::onPositionChanged() -{ - setSize(Renderer::getScreenWidth() - getPosition().x(), Renderer::getScreenHeight() - getPosition().y()); + mTheme = ThemeData::getDefault(); } template @@ -119,8 +115,10 @@ void TextListComponent::render(const Eigen::Affine3f& parentTrans) Eigen::Affine3f trans = parentTrans * getTransform(); Renderer::setMatrix(trans); + std::shared_ptr font = mTheme->getFont(THEME_FONT); + const int cutoff = 0; - const int entrySize = mFont->getHeight() + 5; + const int entrySize = font->getHeight() + 5; int startEntry = 0; @@ -129,7 +127,7 @@ void TextListComponent::render(const Eigen::Affine3f& parentTrans) if((int)mRowVector.size() >= screenCount) { - startEntry = mSelection - (int)(screenCount * 0.5); + startEntry = mCursor - (int)(screenCount * 0.5); if(startEntry < 0) startEntry = 0; if(startEntry >= (int)mRowVector.size() - screenCount) @@ -140,7 +138,7 @@ void TextListComponent::render(const Eigen::Affine3f& parentTrans) if(mRowVector.size() == 0) { - mFont->drawCenteredText("The list is empty.", 0, y, 0xFF0000FF); + font->drawCenteredText("The list is empty.", 0, y, 0xFF0000FF); return; } @@ -152,27 +150,28 @@ void TextListComponent::render(const Eigen::Affine3f& parentTrans) dim = trans * dim - trans.translation(); Renderer::pushClipRect(Eigen::Vector2i((int)trans.translation().x(), (int)trans.translation().y()), Eigen::Vector2i((int)dim.x(), (int)dim.y())); - //Renderer::pushClipRect(pos, dim); - //Renderer::pushClipRect(Eigen::Vector2i((int)trans.translation().x(), (int)trans.translation().y()), Eigen::Vector2i((int)getSize().x() * trans., (int)getSize().y() * trans.scale().y())); - //Renderer::pushClipRect(getGlobalOffset(), getSize()); - for(int i = startEntry; i < listCutoff; i++) { //draw selector bar - if(mSelection == i) + if(mCursor == i) { - Renderer::drawRect(0, (int)y, (int)getSize().x(), mFont->getHeight(), mSelectorColor); + Renderer::drawRect(0, (int)y, (int)getSize().x(), font->getHeight(), mTheme->getColor(THEME_SELECTOR_COLOR)); } ListRow row = mRowVector.at((unsigned int)i); - float x = (float)mTextOffsetX - (mSelection == i ? mMarqueeOffset : 0); - unsigned int color = (mSelection == i && mSelectedTextColorOverride != 0) ? mSelectedTextColorOverride : row.color; + float x = (float)(mCursor == i ? -mMarqueeOffset : 0); - if(mDrawCentered) - mFont->drawCenteredText(row.name, x, y, color); + unsigned int color; + if(mCursor == i && mTheme->getColor(THEME_HIGHLIGHTED_COLOR)) + color = mTheme->getColor(THEME_HIGHLIGHTED_COLOR); else - mFont->drawText(row.name, Eigen::Vector2f(x, y), color); + color = mTheme->getColor(THEME_ENTRY_COLOR[row.colorId]); + + if(mCentered) + font->drawCenteredText(row.name, x, y, color); + else + font->drawText(row.name, Eigen::Vector2f(x, y), color); y += entrySize; } @@ -216,7 +215,8 @@ bool TextListComponent::input(InputConfig* config, Input input) return true; } }else{ - if(config->isMappedTo("down", input) || config->isMappedTo("up", input) || config->isMappedTo("pagedown", input) || config->isMappedTo("pageup", input)) + if(config->isMappedTo("down", input) || config->isMappedTo("up", input) || + config->isMappedTo("pagedown", input) || config->isMappedTo("pageup", input)) { stopScrolling(); } @@ -232,14 +232,15 @@ void TextListComponent::setScrollDir(int val) mScrollDir = val; mMarqueeOffset = 0; mMarqueeTime = -MARQUEE_DELAY; + mScrollAccumulator = -SCROLL_DELAY; } template void TextListComponent::stopScrolling() { mScrollAccumulator = 0; - mScrolling = false; mScrollDir = 0; + onCursorChanged(CURSOR_STOPPED); } template @@ -249,31 +250,17 @@ void TextListComponent::update(int deltaTime) { mScrollAccumulator += deltaTime; - if(!mScrolling) + while(mScrollAccumulator >= SCROLL_TIME) { - if(mScrollAccumulator >= SCROLLDELAY) - { - mScrollAccumulator = SCROLLTIME; - mScrolling = true; - } + mScrollAccumulator -= SCROLL_TIME; + scroll(); } - if(mScrolling) - { - mScrollAccumulator += deltaTime; - - while(mScrollAccumulator >= SCROLLTIME) - { - mScrollAccumulator -= SCROLLTIME; - - scroll(); - } - } }else{ //if we're not scrolling and this object's text goes outside our size, marquee it! std::string text = getSelectedName(); - Eigen::Vector2f textSize = mFont->sizeText(text); + Eigen::Vector2f textSize = mTheme->getFont(THEME_FONT)->sizeText(text); //it's long enough to marquee if(textSize.x() - mMarqueeOffset > getSize().x() - 12) @@ -293,126 +280,94 @@ void TextListComponent::update(int deltaTime) template void TextListComponent::scroll() { - mSelection += mScrollDir; + mCursor += mScrollDir; - if(mSelection < 0) + if(mCursor < 0) { if(mScrollDir < -1) - mSelection = 0; + mCursor = 0; else - mSelection += mRowVector.size(); + mCursor += mRowVector.size(); } - if(mSelection >= (int)mRowVector.size()) + if(mCursor >= (int)mRowVector.size()) { if(mScrollDir > 1) - mSelection = (int)mRowVector.size() - 1; + mCursor = (int)mRowVector.size() - 1; else - mSelection -= mRowVector.size(); + mCursor -= mRowVector.size(); } - if(mScrollSound) - mScrollSound->play(); + onCursorChanged(CURSOR_SCROLLING); + mTheme->playSound("scrollSound"); } //list management stuff template -void TextListComponent::addObject(std::string name, T obj, unsigned int color) +void TextListComponent::add(const std::string& name, const T& obj, unsigned int color) { + if(color >= THEME_COLOR_ID_COUNT) + { + LOG(LogError) << "Invalid row color Id (" << color << ")"; + color = 0; + } + ListRow row = {name, obj, color}; mRowVector.push_back(row); } +template +void TextListComponent::remove(const T& obj) +{ + for(auto it = mRowVector.begin(); it != mRowVector.end(); it++) + { + if((*it).object == obj) + { + if(mCursor > 0 && it - mRowVector.begin() >= mCursor) + { + mCursor--; + onCursorChanged(CURSOR_STOPPED); + } + + mRowVector.erase(it); + return; + } + } + + LOG(LogError) << "Tried to remove an object we couldn't find"; +} + template void TextListComponent::clear() { mRowVector.clear(); - mSelection = 0; + mCursor = 0; + mScrollDir = 0; mMarqueeOffset = 0; mMarqueeTime = -MARQUEE_DELAY; + onCursorChanged(CURSOR_STOPPED); } template -std::string TextListComponent::getSelectedName() +void TextListComponent::setCursor(const T& obj) { - if((int)mRowVector.size() > mSelection) - return mRowVector.at(mSelection).name; - else - return ""; + for(auto it = mRowVector.begin(); it != mRowVector.end(); it++) + { + if((*it).object == obj) + { + mCursor = it - mRowVector.begin(); + onCursorChanged(CURSOR_STOPPED); + return; + } + } + + LOG(LogError) << "Tried to set cursor to object we couldn't find"; } template -T TextListComponent::getSelectedObject() +void TextListComponent::onCursorChanged(CursorState state) { - if((int)mRowVector.size() > mSelection) - return mRowVector.at(mSelection).object; - else - return NULL; -} - -template -int TextListComponent::getSelection() -{ - return mSelection; -} - -template -bool TextListComponent::isScrolling() -{ - return mScrollDir != 0; -} - -template -void TextListComponent::setSelectorColor(unsigned int selectorColor) -{ - mSelectorColor = selectorColor; -} - -template -void TextListComponent::setSelectedTextColor(unsigned int selectedColor) -{ - mSelectedTextColorOverride = selectedColor; -} - -template -void TextListComponent::setCentered(bool centered) -{ - mDrawCentered = centered; -} - -template -void TextListComponent::setTextOffsetX(int textoffsetx) -{ - mTextOffsetX = textoffsetx; -} - -template -int TextListComponent::getObjectCount() -{ - return mRowVector.size(); -} - -template -T TextListComponent::getObject(int i) -{ - return mRowVector.at(i).object; -} - -template -void TextListComponent::setSelection(int i) -{ - mSelection = i; -} - -template -void TextListComponent::setScrollSound(std::shared_ptr & sound) -{ - mScrollSound = sound; -} - -template -void TextListComponent::setFont(std::shared_ptr font) -{ - mFont = font; + if(mCursorChangedCallback) + mCursorChangedCallback(state); } #endif diff --git a/src/components/ThemeComponent.cpp b/src/components/ThemeComponent.cpp deleted file mode 100644 index 43703f214..000000000 --- a/src/components/ThemeComponent.cpp +++ /dev/null @@ -1,393 +0,0 @@ -#include "ThemeComponent.h" -#include "../MathExp.h" -#include -#include "GuiGameList.h" -#include "ImageComponent.h" -#include -#include -#include "../Renderer.h" -#include "../Log.h" - -unsigned int ThemeComponent::getColor(std::string name) -{ - return mColorMap[name]; -} - -bool ThemeComponent::getBool(std::string name) -{ - return mBoolMap[name]; -} - -float ThemeComponent::getFloat(std::string name) -{ - return mFloatMap[name]; -} - -std::shared_ptr & ThemeComponent::getSound(std::string name) -{ - return mSoundMap[name]; -} - -std::string ThemeComponent::getString(std::string name) -{ - return mStringMap[name]; -} - -std::shared_ptr ThemeComponent::getListFont() -{ - if(mListFont) - return mListFont; - else - return Font::get(FONT_SIZE_MEDIUM); -} - -std::shared_ptr ThemeComponent::getDescriptionFont() -{ - if(mDescFont) - return mDescFont; - else - return Font::get(FONT_SIZE_SMALL); -} - -std::shared_ptr ThemeComponent::getFastSelectFont() -{ - if(mFastSelectFont) - return mFastSelectFont; - else - return Font::get(FONT_SIZE_LARGE); -} - -ThemeComponent::ThemeComponent(Window* window) : GuiComponent(window) -{ - mSoundMap["menuScroll"] = std::shared_ptr(new Sound); - mSoundMap["menuSelect"] = std::shared_ptr(new Sound); - mSoundMap["menuBack"] = std::shared_ptr(new Sound); - mSoundMap["menuOpen"] = std::shared_ptr(new Sound); - - //register all sound with the audiomanager - AudioManager::getInstance()->registerSound(mSoundMap["menuScroll"]); - AudioManager::getInstance()->registerSound(mSoundMap["menuSelect"]); - AudioManager::getInstance()->registerSound(mSoundMap["menuBack"]); - AudioManager::getInstance()->registerSound(mSoundMap["menuOpen"]); - - setDefaults(); -} - -ThemeComponent::~ThemeComponent() -{ - deleteComponents(); -} - -void ThemeComponent::setDefaults() -{ - mColorMap["primary"] = 0x0000FFFF; - mColorMap["secondary"] = 0x00FF00FF; - mColorMap["selector"] = 0x000000FF; - mColorMap["selected"] = 0x00000000; - mColorMap["description"] = 0x0000FFFF; - mColorMap["fastSelect"] = 0xFF0000FF; - - mBoolMap["hideHeader"] = false; - mBoolMap["hideDividers"] = false; - mBoolMap["listCentered"] = false; - - mFloatMap["listOffsetX"] = 0.5; - mFloatMap["listTextOffsetX"] = 0.005f; - mFloatMap["gameImageOriginX"] = 0.5; - mFloatMap["gameImageOriginY"] = 0; - mFloatMap["gameImageOffsetX"] = mFloatMap["listOffsetX"] / 2; - mFloatMap["gameImageOffsetY"] = (float)FONT_SIZE_LARGE / (float)Renderer::getScreenHeight(); - mFloatMap["gameImageWidth"] = mFloatMap["listOffsetX"]; - mFloatMap["gameImageHeight"] = 0; - - mSoundMap["menuScroll"]->loadFile(""); - mSoundMap["menuSelect"]->loadFile(""); - mSoundMap["menuBack"]->loadFile(""); - mSoundMap["menuOpen"]->loadFile(""); - - mStringMap["imageNotFoundPath"] = ""; - mStringMap["fastSelectFrame"] = ""; - - mListFont.reset(); - mDescFont.reset(); - mFastSelectFont.reset(); -} - -void ThemeComponent::deleteComponents() -{ - for(unsigned int i = 0; i < getChildCount(); i++) - { - delete getChild(i); - } - - clearChildren(); - - setDefaults(); -} - - -void ThemeComponent::readXML(std::string path, bool detailed) -{ - if(mPath == path) - return; - - setDefaults(); - deleteComponents(); - - mPath = path; - - if(path.empty()) - return; - - LOG(LogInfo) << "Loading theme \"" << path << "\"..."; - - pugi::xml_document doc; - pugi::xml_parse_result result = doc.load_file(path.c_str()); - - if(!result) - { - LOG(LogError) << "Error parsing theme \"" << path << "\"!\n" << " " << result.description(); - return; - } - - pugi::xml_node root; - - if(!detailed) - { - //if we're using the basic view, check if there's a basic version of the theme - root = doc.child("basicTheme"); - } - - if(!root) - { - root = doc.child("theme"); - } - - if(!root) - { - LOG(LogError) << "No theme tag found in theme \"" << path << "\"!"; - return; - } - - //load non-component theme stuff - mColorMap["primary"] = resolveColor(root.child("listPrimaryColor").text().get(), mColorMap["primary"]); - mColorMap["secondary"] = resolveColor(root.child("listSecondaryColor").text().get(), mColorMap["secondary"]); - mColorMap["selector"] = resolveColor(root.child("listSelectorColor").text().get(), mColorMap["selector"]); - mColorMap["selected"] = resolveColor(root.child("listSelectedColor").text().get(), mColorMap["selected"]); - mColorMap["description"] = resolveColor(root.child("descColor").text().get(), mColorMap["description"]); - mColorMap["fastSelect"] = resolveColor(root.child("fastSelectColor").text().get(), mColorMap["fastSelect"]); - - mBoolMap["hideHeader"] = root.child("hideHeader") != 0; - mBoolMap["hideDividers"] = root.child("hideDividers") != 0; - - //list stuff - mBoolMap["listCentered"] = !root.child("listLeftAlign"); - mFloatMap["listOffsetX"] = strToFloat(root.child("listOffsetX").text().get(), mFloatMap["listOffsetX"]); - mFloatMap["listTextOffsetX"] = strToFloat(root.child("listTextOffsetX").text().get(), mFloatMap["listTextOffsetX"]); - - //game image stuff - std::string artPos = root.child("gameImagePos").text().get(); - std::string artDim = root.child("gameImageDim").text().get(); - std::string artOrigin = root.child("gameImageOrigin").text().get(); - - std::string artPosX, artPosY, artWidth, artHeight, artOriginX, artOriginY; - splitString(artPos, ' ', &artPosX, &artPosY); - splitString(artDim, ' ', &artWidth, &artHeight); - splitString(artOrigin, ' ', &artOriginX, &artOriginY); - - mFloatMap["gameImageOffsetX"] = resolveExp(artPosX, mFloatMap["gameImageOffsetX"]); - mFloatMap["gameImageOffsetY"] = resolveExp(artPosY, mFloatMap["gameImageOffsetY"]); - mFloatMap["gameImageWidth"] = resolveExp(artWidth, mFloatMap["gameImageWidth"]); - mFloatMap["gameImageHeight"] = resolveExp(artHeight, mFloatMap["gameImageHeight"]); - mFloatMap["gameImageOriginX"] = resolveExp(artOriginX, mFloatMap["gameImageOriginX"]); - mFloatMap["gameImageOriginY"] = resolveExp(artOriginY, mFloatMap["gameImageOriginY"]); - - mStringMap["imageNotFoundPath"] = expandPath(root.child("gameImageNotFound").text().get()); - mStringMap["fastSelectFrame"] = expandPath(root.child("fastSelectFrame").text().get()); - - //sounds - mSoundMap["menuScroll"]->loadFile(expandPath(root.child("menuScrollSound").text().get())); - mSoundMap["menuSelect"]->loadFile(expandPath(root.child("menuSelectSound").text().get())); - mSoundMap["menuBack"]->loadFile(expandPath(root.child("menuBackSound").text().get())); - mSoundMap["menuOpen"]->loadFile(expandPath(root.child("menuOpenSound").text().get())); - - //fonts - mListFont = resolveFont(root.child("listFont"), Font::getDefaultPath(), FONT_SIZE_MEDIUM); - mDescFont = resolveFont(root.child("descriptionFont"), Font::getDefaultPath(), FONT_SIZE_SMALL); - mFastSelectFont = resolveFont(root.child("fastSelectFont"), Font::getDefaultPath(), FONT_SIZE_LARGE); - - //actually read the components - createComponentChildren(root, this); - - LOG(LogInfo) << "Theme loading complete."; -} - -//recursively creates components -void ThemeComponent::createComponentChildren(pugi::xml_node node, GuiComponent* parent) -{ - for(pugi::xml_node data = node.child("component"); data; data = data.next_sibling("component")) - { - GuiComponent* nextComp = createElement(data, parent); - - if(nextComp) - createComponentChildren(data, nextComp); - } -} - -//takes an XML element definition and creates an object from it -GuiComponent* ThemeComponent::createElement(pugi::xml_node data, GuiComponent* parent) -{ - std::string type = data.child("type").text().get(); - - if(type == "image") - { - std::string path = expandPath(data.child("path").text().get()); - - if(!boost::filesystem::exists(path)) - { - LOG(LogError) << "Error - theme image \"" << path << "\" does not exist."; - return NULL; - } - - std::string pos = data.child("pos").text().get(); - std::string dim = data.child("dim").text().get(); - std::string origin = data.child("origin").text().get(); - - bool tiled = data.child("tiled") != 0; - - //split position and dimension information - std::string posX, posY; - splitString(pos, ' ', &posX, &posY); - - std::string dimW, dimH; - splitString(dim, ' ', &dimW, &dimH); - - std::string originX, originY; - splitString(origin, ' ', &originX, &originY); - - //resolve to pixels from percentages/variables - float x = resolveExp(posX) * Renderer::getScreenWidth(); - float y = resolveExp(posY) * Renderer::getScreenHeight(); - float w = resolveExp(dimW) * Renderer::getScreenWidth(); - float h = resolveExp(dimH) * Renderer::getScreenHeight(); - - float ox = strToFloat(originX); - float oy = strToFloat(originY); - - ImageComponent* comp = new ImageComponent(mWindow, x, y, "", w, h, true); - comp->setOrigin(ox, oy); - comp->setTiling(tiled); - comp->setImage(path); - - addChild(comp); - return comp; - } - - - LOG(LogError) << "Theme component type \"" << type << "\" unknown!"; - return NULL; -} - -//expands a file path (./ becomes the directory of this theme file, ~/ becomes $HOME/) -std::string ThemeComponent::expandPath(std::string path) -{ - if(path.empty()) - return ""; - - if(path[0] == '~') - path = getHomePath() + path.substr(1, path.length() - 1); - else if(path[0] == '.') - path = boost::filesystem::path(mPath).parent_path().string() + path.substr(1, path.length() - 1); - - return path; -} - -//takes a string containing a mathematical expression (possibly including variables) and resolves it to a float value -float ThemeComponent::resolveExp(std::string str, float defaultVal) -{ - if(str.empty()) - return defaultVal; - - MathExp exp; - exp.setExpression(str); - - //set variables - exp.setVariable("headerHeight", (float)(FONT_SIZE_LARGE / Renderer::getScreenHeight())); - exp.setVariable("infoWidth", mFloatMap["listOffsetX"]); - - return exp.eval(); -} - -//takes a string of hex and resolves it to an integer -unsigned int ThemeComponent::resolveColor(std::string str, unsigned int defaultColor) -{ - if(str.empty()) - return defaultColor; - - if(str.length() != 6 && str.length() != 8) - { - LOG(LogError) << "Color \"" << str << "\" is not a valid hex color! Must be 6 or 8 characters."; - return defaultColor; - } - - //if there's no alpha specified, assume FF - if(str.length() == 6) - str += "FF"; - - unsigned int ret; - std::stringstream ss; - ss << std::hex << str; - ss >> ret; - - return ret; -} - -//splits a string in two at the first instance of the delimiter -void ThemeComponent::splitString(std::string str, char delim, std::string* before, std::string* after) -{ - if(str.empty()) - return; - - size_t split = str.find(delim); - if(split != std::string::npos) - { - *before = str.substr(0, split); - *after = str.substr(split + 1, str.length() - split - 1); - }else{ - LOG(LogError) << "Tried to splt string \"" << str << "\" with delimiter '" << delim << "', but delimiter was not found!"; - } -} - -//converts a string to a float -float ThemeComponent::strToFloat(std::string str, float defaultVal) -{ - if(str.empty()) - return defaultVal; - - float ret; - std::stringstream ss; - ss << str; - ss >> ret; - return ret; -} - -std::shared_ptr ThemeComponent::resolveFont(pugi::xml_node node, std::string defaultPath, unsigned int defaultSize) -{ - if(!node) - return NULL; - - std::string path = expandPath(node.child("path").text().get()); - unsigned int size = (unsigned int)(strToFloat(node.child("size").text().get()) * Renderer::getScreenHeight()); - - if(!boost::filesystem::exists(path)) - { - path = defaultPath; - } - - if(size == 0) - { - size = defaultSize; - } - - return Font::get(size, path); -} diff --git a/src/components/ThemeComponent.h b/src/components/ThemeComponent.h deleted file mode 100644 index 7a3057860..000000000 --- a/src/components/ThemeComponent.h +++ /dev/null @@ -1,57 +0,0 @@ -#ifndef _THEMECOMPONENT_H_ -#define _THEMECOMPONENT_H_ - -#include - -#include "../GuiComponent.h" -#include "../pugiXML/pugixml.hpp" -#include "../AudioManager.h" -#include "../resources/Font.h" - -//This class loads an XML-defined list of GuiComponents. -class ThemeComponent : public GuiComponent -{ -public: - ThemeComponent(Window* window); - virtual ~ThemeComponent(); - - void readXML(std::string path, bool detailed); - - unsigned int getColor(std::string name); - bool getBool(std::string name); - float getFloat(std::string name); - std::shared_ptr & getSound(std::string name); - std::string getString(std::string name); - - std::shared_ptr getListFont(); - std::shared_ptr getDescriptionFont(); - std::shared_ptr getFastSelectFont(); - -private: - void setDefaults(); - void deleteComponents(); - void createComponentChildren(pugi::xml_node node, GuiComponent* parent); - GuiComponent* createElement(pugi::xml_node data, GuiComponent* parent); - - //utility functions - std::string expandPath(std::string path); - float resolveExp(std::string str, float defaultVal = 0.0); - unsigned int resolveColor(std::string str, unsigned int defaultColor = 0x000000FF); - void splitString(std::string str, char delim, std::string* before, std::string* after); - float strToFloat(std::string str, float defaultVal = 0.0f); - std::shared_ptr resolveFont(pugi::xml_node node, std::string defaultPath, unsigned int defaultSize); - - std::string mPath; - - std::map mColorMap; - std::map mBoolMap; - std::map mFloatMap; - std::map> mSoundMap; - std::map mStringMap; - - std::shared_ptr mListFont; - std::shared_ptr mDescFont; - std::shared_ptr mFastSelectFont; -}; - -#endif diff --git a/src/main.cpp b/src/main.cpp index 0c905fd38..0921740b7 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -5,7 +5,7 @@ #include #include #include "Renderer.h" -#include "components/GuiGameList.h" +#include "views/ViewController.h" #include "SystemData.h" #include #include "components/GuiDetectDevice.h" @@ -127,9 +127,16 @@ int main(int argc, char* argv[]) Log::open(); LOG(LogInfo) << "EmulationStation - " << PROGRAM_VERSION_STRING; - //always close the log and deinit the BCM library on exit + //always close the log on exit atexit(&onExit); + Window window; + if(!scrape_cmdline && !window.init(width, height)) + { + LOG(LogError) << "Window failed to initialize!"; + return 1; + } + //try loading the system config file if(!SystemData::loadConfig(SystemData::getConfigPath(), true)) { @@ -144,26 +151,19 @@ int main(int argc, char* argv[]) return 1; } - //run the command line scraper ui then quit + //run the command line scraper then quit if(scrape_cmdline) { return run_scraper_cmdline(); } - Window window; - if(!window.init(width, height)) - { - LOG(LogError) << "Window failed to initialize!"; - return 1; - } - //dont generate joystick events while we're loading (hopefully fixes "automatically started emulator" bug) SDL_JoystickEventState(SDL_DISABLE); //choose which GUI to open depending on if an input configuration already exists if(fs::exists(InputManager::getConfigPath())) { - GuiGameList::create(&window); + window.getViewController()->goToSystemSelect(); }else{ window.pushGui(new GuiDetectDevice(&window)); } diff --git a/src/resources/Font.cpp b/src/resources/Font.cpp index b463160b2..08a9487f2 100644 --- a/src/resources/Font.cpp +++ b/src/resources/Font.cpp @@ -101,13 +101,7 @@ void Font::unload(std::shared_ptr& rm) std::shared_ptr Font::get(int size, const std::string& path) { - if(path.empty()) - { - LOG(LogError) << "Tried to get font with no path!"; - return std::shared_ptr(); - } - - std::pair def(path, size); + std::pair def(path.empty() ? getDefaultPath() : path, size); auto foundFont = sFontMap.find(def); if(foundFont != sFontMap.end()) { diff --git a/src/views/BasicGameListView.cpp b/src/views/BasicGameListView.cpp new file mode 100644 index 000000000..f338da262 --- /dev/null +++ b/src/views/BasicGameListView.cpp @@ -0,0 +1,174 @@ +#include "BasicGameListView.h" +#include "ViewController.h" +#include "../Renderer.h" +#include "../Window.h" +#include "../ThemeData.h" + +BasicGameListView::BasicGameListView(Window* window, FileData* root) + : GameListView(window, root), + mHeaderText(window), mHeaderImage(window), mBackground(window), mList(window) +{ + mHeaderText.setText("Header"); + mHeaderText.setSize(mSize.x(), 0); + mHeaderText.setPosition(0, 0); + mHeaderText.setCentered(true); + + mHeaderImage.setResize(0, mSize.y() * 0.2f, false); + mHeaderImage.setOrigin(0.5f, 0.0f); + mHeaderImage.setPosition(mSize.x() / 2, 0); + + mBackground.setResize(mSize.x(), mSize.y(), true); + + mList.setSize(mSize.x(), mSize.y() * 0.8f); + mList.setPosition(0, mSize.y() * 0.2f); + + populateList(root); + + addChild(&mBackground); + addChild(&mList); + addChild(&mHeaderText); +} + +void BasicGameListView::setTheme(const std::shared_ptr& theme) +{ + const ImageDef& bg = theme->getImage("backgroundImage"); + mBackground.setTiling(bg.tile); + mBackground.setImage(bg.getTexture()); + + const ImageDef& hdr = theme->getImage("headerImage"); + mHeaderImage.setTiling(hdr.tile); + mHeaderImage.setImage(hdr.getTexture()); + + if(mHeaderImage.hasImage()) + { + removeChild(&mHeaderText); + addChild(&mHeaderImage); + }else{ + addChild(&mHeaderText); + removeChild(&mHeaderImage); + } + + mList.setTheme(theme); +} + +void BasicGameListView::onFileChanged(FileData* file, FileChangeType change) +{ + // we don't care about metadata changes (since we don't display metadata), + // so we just ignore the FILE_METADATA_CHANGED case + + // if it's immediately inside our current folder + if(file->getParent() == getCurrentFolder()) + { + if(change == FILE_REMOVED) + { + mList.remove(file); // will automatically make sure cursor ends up in a "safe" place + }else if(change == FILE_ADDED) + { + FileData* cursor = getCursor(); + populateList(cursor->getParent()); + mList.setCursor(cursor); + } + } +} + +void buildHeader(FileData* from, std::stringstream& ss) +{ + if(from->getParent()) + { + buildHeader(from->getParent(), ss); + ss << " -> "; + } + + ss << from->getName(); +} + +void BasicGameListView::populateList(FileData* root) +{ + mList.clear(); + + std::stringstream ss; + buildHeader(root, ss); + mHeaderText.setText(ss.str()); + + for(auto it = root->getChildren().begin(); it != root->getChildren().end(); it++) + { + mList.add((*it)->getName(), *it, ((*it)->getType() == FOLDER)); + } +} + +void BasicGameListView::setCursor(FileData* cursor) +{ + if(cursor->getParent() != getCursor()->getParent()) + { + // Rebuild the folder stack + std::stack path; + FileData* cur = cursor; + while((cur = cur->getParent()) != mRoot) + path.push(cur); + + while(!mCursorStack.empty()) + mCursorStack.pop(); + + while(!path.empty()) // put back in reverse order (flip) + { + mCursorStack.push(path.top()); + path.pop(); + } + + populateList(cursor->getParent()); + } + + mList.setCursor(cursor); +} + +bool BasicGameListView::input(InputConfig* config, Input input) +{ + if(input.value != 0) + { + if(config->isMappedTo("a", input)) + { + if(mList.getList().size() > 0) + { + FileData* cursor = getCursor(); + if(cursor->getType() == GAME) + { + mWindow->getViewController()->launch(cursor); + }else{ + // it's a folder + if(cursor->getChildren().size() > 0) + { + mCursorStack.push(cursor); + populateList(cursor); + } + } + + return true; + } + }else if(config->isMappedTo("b", input)) + { + if(mCursorStack.size()) + { + populateList(mCursorStack.top()->getParent()); + mList.setCursor(mCursorStack.top()); + mCursorStack.pop(); + }else{ + mList.stopScrolling(); + mWindow->getViewController()->goToSystemSelect(); + } + + return true; + }else if(config->isMappedTo("right", input)) + { + mList.stopScrolling(); + mWindow->getViewController()->goToNextSystem(); + return true; + }else if(config->isMappedTo("left", input)) + { + mList.stopScrolling(); + mWindow->getViewController()->goToPrevSystem(); + return true; + } + } + + return GameListView::input(config, input); +} diff --git a/src/views/BasicGameListView.h b/src/views/BasicGameListView.h new file mode 100644 index 000000000..0eaed66ca --- /dev/null +++ b/src/views/BasicGameListView.h @@ -0,0 +1,34 @@ +#pragma once + +#include "GameListView.h" +#include "../components/TextListComponent.h" +#include "../components/TextComponent.h" +#include "../components/ImageComponent.h" + +class BasicGameListView : public GameListView +{ +public: + BasicGameListView(Window* window, FileData* root); + + // Called when a FileData* is added, has its metadata changed, or is removed + virtual void onFileChanged(FileData* file, FileChangeType change); + + virtual bool input(InputConfig* config, Input input) override; + + virtual void setTheme(const std::shared_ptr& theme) override; + + inline FileData* getCursor() { return mList.getSelected(); } + virtual void setCursor(FileData* file) override; + +protected: + void populateList(FileData* root); + + TextComponent mHeaderText; + ImageComponent mHeaderImage; + ImageComponent mBackground; + TextListComponent mList; + + inline FileData* getCurrentFolder() { return getCursor()->getParent(); } + + std::stack mCursorStack; +}; diff --git a/src/views/DetailedGameListView.cpp b/src/views/DetailedGameListView.cpp new file mode 100644 index 000000000..11ee74389 --- /dev/null +++ b/src/views/DetailedGameListView.cpp @@ -0,0 +1,64 @@ +#include "DetailedGameListView.h" + +DetailedGameListView::DetailedGameListView(Window* window, FileData* root) : + BasicGameListView(window, root), + mDescContainer(window), mDescription(window), + mImage(window) +{ + const float padding = 0.02f; + + mList.setPosition(mSize.x() * (0.50f + padding), mList.getPosition().y()); + mList.setSize(mSize.x() * (0.50f - 2*padding), mList.getSize().y()); + mList.setCentered(false); + mList.setCursorChangedCallback([&](TextListComponent::CursorState state) { updateInfoPanel(); }); + + mImage.setOrigin(0.5f, 0.0f); + mImage.setPosition(mSize.x() * 0.25f, mList.getPosition().y()); + mImage.setResize(mSize.x() * (0.50f - 2*padding), 0, false); + addChild(&mImage); + + mDescContainer.setPosition(mSize.x() * padding, mSize.y() * 0.2f); + mDescContainer.setSize(mSize.x() * (0.50f - 2*padding), 0); + mDescContainer.setAutoScroll((int)(1600 + mDescContainer.getSize().x()), 0.025f); + addChild(&mDescContainer); + + mDescription.setSize(mDescContainer.getSize().x(), 0); + mDescContainer.addChild(&mDescription); + + updateInfoPanel(); +} + +void DetailedGameListView::setTheme(const std::shared_ptr& theme) +{ + BasicGameListView::setTheme(theme); + + mDescription.setFont(theme->getFont("descriptionFont")); + mDescription.setColor(theme->getColor("descriptionColor")); +} + +void DetailedGameListView::updateInfoPanel() +{ + FileData* file = (mList.getList().size() == 0 || mList.isScrolling()) ? NULL : mList.getSelected(); + + if(file == NULL) + { + mImage.setImage(""); + mDescription.setText(""); + }else{ + mImage.setImage(file->metadata.get("image")); + + mDescContainer.setPosition(mDescContainer.getPosition().x(), mImage.getPosition().y() + mImage.getSize().y() * 1.02f); + mDescContainer.setSize(mDescContainer.getSize().x(), mSize.y() - mDescContainer.getPosition().y()); + mDescContainer.resetAutoScrollTimer(); + + mDescription.setText(file->metadata.get("desc")); + } +} + +void DetailedGameListView::onFileChanged(FileData* file, FileChangeType type) +{ + BasicGameListView::onFileChanged(file, type); + + if(type == FILE_METADATA_CHANGED && file == getCursor()) + updateInfoPanel(); +} diff --git a/src/views/DetailedGameListView.h b/src/views/DetailedGameListView.h new file mode 100644 index 000000000..0bf988828 --- /dev/null +++ b/src/views/DetailedGameListView.h @@ -0,0 +1,25 @@ +#pragma once + +#include "BasicGameListView.h" +#include "../components/ImageComponent.h" +#include "../components/TextComponent.h" +#include "../components/ScrollableContainer.h" + +class DetailedGameListView : public BasicGameListView +{ +public: + DetailedGameListView(Window* window, FileData* root); + + virtual void setTheme(const std::shared_ptr& theme) override; + + virtual void onFileChanged(FileData* file, FileChangeType change); + +private: + void updateInfoPanel(); + + ImageComponent mImage; + + ScrollableContainer mDescContainer; + TextComponent mDescription; +}; + diff --git a/src/views/GameListView.cpp b/src/views/GameListView.cpp new file mode 100644 index 000000000..123677e9a --- /dev/null +++ b/src/views/GameListView.cpp @@ -0,0 +1,28 @@ +#include "GameListView.h" +#include "../Window.h" +#include "../components/GuiMetaDataEd.h" + +bool GameListView::input(InputConfig* config, Input input) +{ + if(config->getDeviceId() == DEVICE_KEYBOARD && input.id == SDLK_F3 && input.value != 0) + { + // open metadata editor + FileData* file = getCursor(); + ScraperSearchParams p; + p.game = file; + p.system = file->getSystem(); + mWindow->pushGui(new GuiMetaDataEd(mWindow, &file->metadata, file->metadata.getMDD(), p, file->getPath().filename().string(), + std::bind(&GameListView::onFileChanged, this, file, FILE_METADATA_CHANGED), [file, this] { + boost::filesystem::remove(file->getPath()); //actually delete the file on the filesystem + file->getParent()->removeChild(file); //unlink it so list repopulations triggered from onFileChanged won't see it + onFileChanged(file, FILE_REMOVED); //tell the view + delete file; //free it + })); + return true; + }else if(config->isMappedTo("start", input) && input.value != 0) + { + // open menu + } + + return GuiComponent::input(config, input); +} diff --git a/src/views/GameListView.h b/src/views/GameListView.h new file mode 100644 index 000000000..ad3ef603b --- /dev/null +++ b/src/views/GameListView.h @@ -0,0 +1,36 @@ +#pragma once + +#include "../FileData.h" +#include "../Renderer.h" + +class Window; +class GuiComponent; +class FileData; +class ThemeData; + +//GameListView needs to know: +// What theme data to use +// The root FileData for the tree it should explore + +class GameListView : public GuiComponent +{ +public: + GameListView(Window* window, FileData* root) : GuiComponent(window), mRoot(root) + { setSize((float)Renderer::getScreenWidth(), (float)Renderer::getScreenHeight()); } + + virtual ~GameListView() {} + + // Called when a new file is added, a file is removed, or a file's metadata changes. + virtual void onFileChanged(FileData* file, FileChangeType change) = 0; + + // Called to set or update theme. + virtual void setTheme(const std::shared_ptr& theme) = 0; + + virtual bool input(InputConfig* config, Input input) override; + + virtual FileData* getCursor() = 0; + virtual void setCursor(FileData*) = 0; + +protected: + FileData* mRoot; +}; diff --git a/src/views/ViewController.cpp b/src/views/ViewController.cpp new file mode 100644 index 000000000..de3f1f5ea --- /dev/null +++ b/src/views/ViewController.cpp @@ -0,0 +1,168 @@ +#include "ViewController.h" +#include "../Log.h" +#include "../SystemData.h" + +#include "BasicGameListView.h" +#include "DetailedGameListView.h" + +ViewController::ViewController(Window* window) + : GuiComponent(window), mCurrentView(nullptr), mCameraPos(Eigen::Affine3f::Identity()) +{ + mState.viewing = START_SCREEN; +} + +void ViewController::goToSystemSelect() +{ + mState.viewing = SYSTEM_SELECT; + goToSystem(SystemData::sSystemVector.at(0)); +} + +SystemData* getSystemCyclic(SystemData* from, bool reverse) +{ + std::vector& sysVec = SystemData::sSystemVector; + + if(reverse) + { + auto it = std::find(sysVec.rbegin(), sysVec.rend(), from); + assert(it != sysVec.rend()); + it++; + if(it == sysVec.rend()) + it = sysVec.rbegin(); + return *it; + }else{ + auto it = std::find(sysVec.begin(), sysVec.end(), from); + assert(it != sysVec.end()); + it++; + if(it == sysVec.end()) + it = sysVec.begin(); + return *it; + } +} + +void ViewController::goToNextSystem() +{ + assert(mState.viewing == SYSTEM); + + SystemData* system = mState.data.system; + if(system == NULL) + return; + + goToSystem(getSystemCyclic(system, false)); +} + +void ViewController::goToPrevSystem() +{ + assert(mState.viewing == SYSTEM); + + SystemData* system = mState.data.system; + if(system == NULL) + return; + + goToSystem(getSystemCyclic(system, true)); +} + +void ViewController::goToSystem(SystemData* system) +{ + mState.viewing = SYSTEM; + mState.data.system = system; + + mCurrentView = getSystemView(system); +} + +void ViewController::onFileChanged(FileData* file, FileChangeType change) +{ + for(auto it = mSystemViews.begin(); it != mSystemViews.end(); it++) + { + it->second->onFileChanged(file, change); + } +} + +void ViewController::launch(FileData* game) +{ + if(game->getType() != GAME) + { + LOG(LogError) << "tried to launch something that isn't a game"; + return; + } + + // Effect TODO + game->getSystem()->launchGame(mWindow, game); +} + +std::shared_ptr ViewController::getSystemView(SystemData* system) +{ + //if we already made one, return that one + auto exists = mSystemViews.find(system); + if(exists != mSystemViews.end()) + return exists->second; + + //if we didn't, make it, remember it, and return it + std::shared_ptr view; + + if(system != NULL) + { + view = std::shared_ptr(new DetailedGameListView(mWindow, system->getRootFolder())); + view->setTheme(system->getTheme()); + }else{ + LOG(LogError) << "null system"; // should eventually return an "all games" gamelist view + } + + std::vector& sysVec = SystemData::sSystemVector; + int id = std::find(sysVec.begin(), sysVec.end(), system) - sysVec.begin(); + view->setPosition(id * (float)Renderer::getScreenWidth(), 0); + + mSystemViews[system] = view; + return view; +} + + + +bool ViewController::input(InputConfig* config, Input input) +{ + if(mCurrentView) + return mCurrentView->input(config, input); + + return false; +} + + +float clamp(float min, float max, float val) +{ + if(val < min) + val = min; + else if(val > max) + val = max; + + return val; +} + +//http://en.wikipedia.org/wiki/Smoothstep +float smoothStep(float edge0, float edge1, float x) +{ + // Scale, and clamp x to 0..1 range + x = clamp(0, 1, (x - edge0)/(edge1 - edge0)); + + // Evaluate polynomial + return x*x*x*(x*(x*6 - 15) + 10); +} + +void ViewController::update(int deltaTime) +{ + if(mCurrentView) + { + mCurrentView->update(deltaTime); + + // move camera towards current view (should use smoothstep) + Eigen::Vector3f diff = (mCurrentView->getPosition() + mCameraPos.translation()) * 0.0075f * (float)deltaTime; + mCameraPos.translate(-diff); + } +} + +void ViewController::render(const Eigen::Affine3f& parentTrans) +{ + Eigen::Affine3f trans = parentTrans * mCameraPos; + + //should really do some clipping here + for(auto it = mSystemViews.begin(); it != mSystemViews.end(); it++) + it->second->render(trans); +} diff --git a/src/views/ViewController.h b/src/views/ViewController.h new file mode 100644 index 000000000..e678d5cab --- /dev/null +++ b/src/views/ViewController.h @@ -0,0 +1,61 @@ +#pragma once + +#include "GameListView.h" + +class SystemData; + +class ViewController : public GuiComponent +{ +public: + ViewController(Window* window); + + // Navigation. + void goToNextSystem(); + void goToPrevSystem(); + void goToSystem(SystemData* system); + void goToSystemSelect(); + void showQuickSystemSelect(); + + void onFileChanged(FileData* file, FileChangeType change); + + // Plays a nice launch effect and launches the game at the end of it. + // Once the game terminates, plays a return effect. + void launch(FileData* game); + + bool input(InputConfig* config, Input input) override; + void update(int deltaTime) override; + void render(const Eigen::Affine3f& parentTrans) override; + + enum ViewMode + { + START_SCREEN, + SYSTEM_SELECT, + SYSTEM + }; + + struct State + { + ViewMode viewing; + + inline SystemData* getSystem() const { assert(viewing == SYSTEM); return data.system; } + + private: + friend ViewController; + union + { + SystemData* system; + } data; + }; + + inline const State& getState() const { return mState; } + +private: + std::shared_ptr getSystemView(SystemData* system); + + std::shared_ptr mCurrentView; + std::map< SystemData*, std::shared_ptr > mSystemViews; + + Eigen::Affine3f mCameraPos; + + State mState; +};