diff --git a/CMakeLists.txt b/CMakeLists.txt index c287b8781..057868be7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -174,6 +174,7 @@ set(ES_HEADERS ${CMAKE_CURRENT_SOURCE_DIR}/src/components/TextEditComponent.h ${CMAKE_CURRENT_SOURCE_DIR}/src/components/TextListComponent.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 @@ -239,6 +240,7 @@ set(ES_SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/src/components/TextComponent.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/components/TextEditComponent.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 diff --git a/THEMES.md b/THEMES.md index f05a345fb..d8e2ecd9d 100644 --- a/THEMES.md +++ b/THEMES.md @@ -65,6 +65,7 @@ Fonts are defined like so: `` - Default size: 0.045. `` - Default size: 0.035. +`` - Default size: 0.15. Colors ====== @@ -81,6 +82,8 @@ or `` - Default: 000000FF. `` - Default: 00000000. `` - Default: 48474DFF. +`` - Default: FFFFFFFF. +`` - Default: DDDDDDFF. Images ====== @@ -98,6 +101,7 @@ Pretty much any image format is supported. `` - No default. `` - No default. `` - No default. +`` - No default. Sounds ====== diff --git a/src/FileData.h b/src/FileData.h index 9d9ef0175..b5f221093 100644 --- a/src/FileData.h +++ b/src/FileData.h @@ -17,7 +17,8 @@ enum FileChangeType { FILE_ADDED, FILE_METADATA_CHANGED, - FILE_REMOVED + FILE_REMOVED, + FILE_SORTED }; // Used for loading/saving gamelist.xml. diff --git a/src/ThemeData.cpp b/src/ThemeData.cpp index 8827c9abd..62b60949e 100644 --- a/src/ThemeData.cpp +++ b/src/ThemeData.cpp @@ -11,14 +11,17 @@ // Defaults std::map ThemeData::sDefaultFonts = boost::assign::map_list_of ("listFont", FontDef(0.045f, "")) - ("descriptionFont", FontDef(0.035f, "")); + ("descriptionFont", FontDef(0.035f, "")) + ("fastSelectLetterFont", FontDef(0.15f, "")); std::map ThemeData::sDefaultColors = boost::assign::map_list_of ("listPrimaryColor", 0x0000FFFF) ("listSecondaryColor", 0x00FF00FF) ("listSelectorColor", 0x000000FF) ("listSelectedColor", 0x00000000) - ("descriptionColor", 0x48474DFF); + ("descriptionColor", 0x48474DFF) + ("fastSelectLetterColor", 0xFFFFFFFF) + ("fastSelectTextColor", 0xDDDDDDFF); std::map ThemeData::sDefaultImages = boost::assign::map_list_of ("backgroundImage", ImageDef("", true)) diff --git a/src/components/GuiFastSelect.cpp b/src/components/GuiFastSelect.cpp new file mode 100644 index 000000000..f56acf0ef --- /dev/null +++ b/src/components/GuiFastSelect.cpp @@ -0,0 +1,159 @@ +#include "GuiFastSelect.h" +#include "../ThemeData.h" +#include "../FileSorts.h" +#include "../SystemData.h" + +static const std::string LETTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; + +GuiFastSelect::GuiFastSelect(Window* window, GameListView* gamelist) : GuiComponent(window), + mBackground(window, ":/button.png"), mSortText(window), mLetterText(window), mGameList(gamelist) +{ + setPosition(Renderer::getScreenWidth() * 0.2f, Renderer::getScreenHeight() * 0.2f); + setSize(Renderer::getScreenWidth() * 0.6f, Renderer::getScreenHeight() * 0.6f); + + const std::shared_ptr& theme = mGameList->getTheme(); + + mBackground.fitTo(mSize); + addChild(&mBackground); + + mLetterText.setSize(mSize.x(), mSize.y() * 0.75f); + mLetterText.setCentered(true); + mLetterText.setFromTheme(theme, "fastSelectLetterFont", "fastSelectLetterColor"); + addChild(&mLetterText); + + mSortText.setPosition(0, mSize.y() * 0.75f); + mSortText.setSize(mSize.x(), mSize.y() * 0.25f); + mSortText.setCentered(true); + mSortText.setFromTheme(theme, "descriptionFont", "fastSelectTextColor"); + addChild(&mSortText); + + mSortId = 0; // TODO + updateSortText(); + + mLetterId = LETTERS.find(mGameList->getCursor()->getName()[0]); + if(mLetterId == std::string::npos) + mLetterId = 0; + + mScrollDir = 0; + mScrollAccumulator = 0; + scroll(); // initialize the letter value +} + +bool GuiFastSelect::input(InputConfig* config, Input input) +{ + if(input.value == 0 && config->isMappedTo("select", input)) + { + // the user let go of select; make our changes to the gamelist and close this gui + updateGameListSort(); + updateGameListCursor(); + delete this; + return true; + } + + if(config->isMappedTo("up", input)) + { + if(input.value != 0) + setScrollDir(-1); + else + setScrollDir(0); + + return true; + }else if(config->isMappedTo("down", input)) + { + if(input.value != 0) + setScrollDir(1); + else + setScrollDir(0); + + return true; + }else if(config->isMappedTo("left", input) && input.value != 0) + { + mSortId = (mSortId + 1) % FileSorts::SortTypes.size(); + updateSortText(); + return true; + }else if(config->isMappedTo("right", input) && input.value != 0) + { + mSortId--; + if(mSortId < 0) + mSortId += FileSorts::SortTypes.size(); + + updateSortText(); + return true; + } + + return GuiComponent::input(config, input); +} + +void GuiFastSelect::setScrollDir(int dir) +{ + mScrollDir = dir; + scroll(); + mScrollAccumulator = -500; +} + +void GuiFastSelect::update(int deltaTime) +{ + if(mScrollDir != 0) + { + mScrollAccumulator += deltaTime; + while(mScrollAccumulator >= 150) + { + scroll(); + mScrollAccumulator -= 150; + } + } + + GuiComponent::update(deltaTime); +} + +void GuiFastSelect::scroll() +{ + mLetterId += mScrollDir; + if(mLetterId < 0) + mLetterId += LETTERS.length(); + else if(mLetterId >= (int)LETTERS.length()) + mLetterId -= LETTERS.length(); + + mLetterText.setText(LETTERS.substr(mLetterId, 1)); +} + +void GuiFastSelect::updateSortText() +{ + std::stringstream ss; + ss << "<- " << FileSorts::SortTypes.at(mSortId).description << " ->"; + mSortText.setText(ss.str()); +} + +void GuiFastSelect::updateGameListSort() +{ + const FileData::SortType& sort = FileSorts::SortTypes.at(mSortId); + + FileData* root = mGameList->getCursor()->getSystem()->getRootFolder(); + root->sort(sort); // will also recursively sort children + + // notify that the root folder was sorted + mGameList->onFileChanged(root, FILE_SORTED); +} + +void GuiFastSelect::updateGameListCursor() +{ + const std::vector& list = mGameList->getCursor()->getParent()->getChildren(); + + // only skip by letter when the sort mode is alphabetical + const FileData::SortType& sort = FileSorts::SortTypes.at(mSortId); + if(sort.comparisonFunction != &FileSorts::compareFileName) + return; + + // find the first entry in the list that either exactly matches our target letter or is beyond our target letter + for(auto it = list.cbegin(); it != list.cend(); it++) + { + char check = (*it)->getName().empty() ? 'A' : (*it)->getName()[0]; + + // if there's an exact match or we've passed it, set the cursor to this one + if(check == LETTERS[mLetterId] || (sort.ascending && check > LETTERS[mLetterId]) || (!sort.ascending && check < LETTERS[mLetterId])) + { + mGameList->setCursor(*it); + break; + } + } +} diff --git a/src/components/GuiFastSelect.h b/src/components/GuiFastSelect.h new file mode 100644 index 000000000..dfee83433 --- /dev/null +++ b/src/components/GuiFastSelect.h @@ -0,0 +1,35 @@ +#pragma once + +#include "../GuiComponent.h" +#include "../views/GameListView.h" + +#include "NinePatchComponent.h" +#include "TextComponent.h" + +class GuiFastSelect : public GuiComponent +{ +public: + GuiFastSelect(Window* window, GameListView* gamelist); + + bool input(InputConfig* config, Input input); + void update(int deltaTime); + +private: + void setScrollDir(int dir); + void scroll(); + void updateGameListCursor(); + void updateGameListSort(); + void updateSortText(); + + int mSortId; + int mLetterId; + + int mScrollDir; + int mScrollAccumulator; + + NinePatchComponent mBackground; + TextComponent mSortText; + TextComponent mLetterText; + + GameListView* mGameList; +}; diff --git a/src/components/TextComponent.cpp b/src/components/TextComponent.cpp index 4808b9d7d..c3ae71ba2 100644 --- a/src/components/TextComponent.cpp +++ b/src/components/TextComponent.cpp @@ -2,6 +2,7 @@ #include "../Renderer.h" #include "../Log.h" #include "../Window.h" +#include "../ThemeData.h" TextComponent::TextComponent(Window* window) : GuiComponent(window), mFont(NULL), mColor(0x000000FF), mAutoCalcExtent(true, true), mCentered(false) @@ -137,3 +138,9 @@ std::string TextComponent::getValue() const { return mText; } + +void TextComponent::setFromTheme(const std::shared_ptr& theme, const std::string& fontIdentifier, const std::string& colorIdentifier) +{ + setFont(theme->getFont(fontIdentifier)); + setColor(theme->getColor(colorIdentifier)); +} diff --git a/src/components/TextComponent.h b/src/components/TextComponent.h index 4cac3da4f..0b73d88e9 100644 --- a/src/components/TextComponent.h +++ b/src/components/TextComponent.h @@ -4,6 +4,8 @@ #include "../GuiComponent.h" #include "../resources/Font.h" +class ThemeData; + class TextComponent : public GuiComponent { public: @@ -26,6 +28,8 @@ public: std::shared_ptr getFont() const; + void setFromTheme(const std::shared_ptr& theme, const std::string& fontIdentifier, const std::string& colorIdentifier); + private: void calculateExtent(); diff --git a/src/views/BasicGameListView.cpp b/src/views/BasicGameListView.cpp index 3050e6a23..008411544 100644 --- a/src/views/BasicGameListView.cpp +++ b/src/views/BasicGameListView.cpp @@ -29,7 +29,7 @@ BasicGameListView::BasicGameListView(Window* window, FileData* root) addChild(&mHeaderText); } -void BasicGameListView::setTheme(const std::shared_ptr& theme) +void BasicGameListView::onThemeChanged(const std::shared_ptr& theme) { const ImageDef& bg = theme->getImage("backgroundImage"); mBackground.setTiling(bg.tile); @@ -69,6 +69,14 @@ void BasicGameListView::onFileChanged(FileData* file, FileChangeType change) mList.setCursor(cursor); } } + + // the root file was sorted (so children were sorted too) + if(file == mRoot && change == FILE_SORTED) + { + FileData* cursor = getCursor(); + populateList(cursor->getParent()); + mList.setCursor(cursor); + } } void buildHeader(FileData* from, std::stringstream& ss) diff --git a/src/views/BasicGameListView.h b/src/views/BasicGameListView.h index 0eaed66ca..f59beab37 100644 --- a/src/views/BasicGameListView.h +++ b/src/views/BasicGameListView.h @@ -15,7 +15,7 @@ public: virtual bool input(InputConfig* config, Input input) override; - virtual void setTheme(const std::shared_ptr& theme) override; + virtual void onThemeChanged(const std::shared_ptr& theme) override; inline FileData* getCursor() { return mList.getSelected(); } virtual void setCursor(FileData* file) override; diff --git a/src/views/DetailedGameListView.cpp b/src/views/DetailedGameListView.cpp index 88cf7158b..e21c8edcc 100644 --- a/src/views/DetailedGameListView.cpp +++ b/src/views/DetailedGameListView.cpp @@ -39,10 +39,10 @@ DetailedGameListView::DetailedGameListView(Window* window, FileData* root) : updateInfoPanel(); } -void DetailedGameListView::setTheme(const std::shared_ptr& theme) +void DetailedGameListView::onThemeChanged(const std::shared_ptr& theme) { mHeaderImage.setResize(mSize.x() * 0.5f, 0, true); - BasicGameListView::setTheme(theme); + BasicGameListView::onThemeChanged(theme); if(mHeaderImage.getPosition().y() + mHeaderImage.getSize().y() > mImage.getPosition().y()) mHeaderImage.setResize(0, mSize.y() * 0.185f, true); diff --git a/src/views/DetailedGameListView.h b/src/views/DetailedGameListView.h index 57a6ca06f..7f7ab50d3 100644 --- a/src/views/DetailedGameListView.h +++ b/src/views/DetailedGameListView.h @@ -10,7 +10,7 @@ class DetailedGameListView : public BasicGameListView public: DetailedGameListView(Window* window, FileData* root); - virtual void setTheme(const std::shared_ptr& theme) override; + virtual void onThemeChanged(const std::shared_ptr& theme) override; virtual void onFileChanged(FileData* file, FileChangeType change); diff --git a/src/views/GameListView.cpp b/src/views/GameListView.cpp index e2153ef45..56f9d7655 100644 --- a/src/views/GameListView.cpp +++ b/src/views/GameListView.cpp @@ -2,6 +2,8 @@ #include "../Window.h" #include "../components/GuiMetaDataEd.h" #include "../components/GuiMenu.h" +#include "../components/GuiFastSelect.h" +#include "ViewController.h" bool GameListView::input(InputConfig* config, Input input) { @@ -24,7 +26,17 @@ bool GameListView::input(InputConfig* config, Input input) { // open menu mWindow->pushGui(new GuiMenu(mWindow)); + }else if(config->isMappedTo("select", input) && input.value != 0) + { + // open fast select + mWindow->pushGui(new GuiFastSelect(mWindow, this)); } return GuiComponent::input(config, input); } + +void GameListView::setTheme(const std::shared_ptr& theme) +{ + mTheme = theme; + onThemeChanged(theme); +} diff --git a/src/views/GameListView.h b/src/views/GameListView.h index ad3ef603b..1ea16bd7a 100644 --- a/src/views/GameListView.h +++ b/src/views/GameListView.h @@ -20,17 +20,23 @@ public: virtual ~GameListView() {} - // Called when a new file is added, a file is removed, or a file's metadata changes. + // Called when a new file is added, a file is removed, a file's metadata changes, or a file's children are sorted. + // NOTE: FILE_SORTED is only reported for the topmost FileData, where the sort started. + // Since sorts are recursive, that FileData's children probably changed too. virtual void onFileChanged(FileData* file, FileChangeType change) = 0; - // Called to set or update theme. - virtual void setTheme(const std::shared_ptr& theme) = 0; + // Called whenever the theme changes. + virtual void onThemeChanged(const std::shared_ptr& theme) = 0; - virtual bool input(InputConfig* config, Input input) override; + void setTheme(const std::shared_ptr& theme); + inline const std::shared_ptr& getTheme() const { return mTheme; } virtual FileData* getCursor() = 0; virtual void setCursor(FileData*) = 0; + virtual bool input(InputConfig* config, Input input) override; + protected: FileData* mRoot; + std::shared_ptr mTheme; };