diff --git a/UI/MainScreen.cpp b/UI/MainScreen.cpp index 9d0b5dbd94..c100266afa 100644 --- a/UI/MainScreen.cpp +++ b/UI/MainScreen.cpp @@ -429,6 +429,9 @@ void GameButton::Draw(UIContext &dc) { std::string GameButton::DescribeText() const { std::shared_ptr ginfo = g_gameInfoCache->GetInfo(nullptr, gamePath_, 0); + if (ginfo->pending) + return "..."; + auto u = GetI18NCategory(I18NCat::UI_ELEMENTS); return ReplaceAll(u->T("%1 button"), "%1", ginfo->GetTitle()); } @@ -528,6 +531,67 @@ void GameBrowser::SetPath(const Path &path) { Refresh(); } +void GameBrowser::ApplySearchFilter(const std::string &filter) { + searchFilter_ = filter; + std::transform(searchFilter_.begin(), searchFilter_.end(), searchFilter_.begin(), tolower); + + // We don't refresh because game info loads asynchronously anyway. + ApplySearchFilter(); +} + +void GameBrowser::ApplySearchFilter() { + if (searchFilter_.empty() && searchStates_.empty()) { + // We haven't hidden anything, and we're not searching, so do nothing. + searchPending_ = false; + return; + } + + searchPending_ = false; + // By default, everything is matching. + searchStates_.resize(gameList_->GetNumSubviews(), SearchState::MATCH); + + if (searchFilter_.empty()) { + // Just quickly mark anything we hid as visible again. + for (int i = 0; i < gameList_->GetNumSubviews(); ++i) { + UI::View *v = gameList_->GetViewByIndex(i); + if (searchStates_[i] != SearchState::MATCH) + v->SetVisibility(UI::V_VISIBLE); + } + + searchStates_.clear(); + return; + } + + for (int i = 0; i < gameList_->GetNumSubviews(); ++i) { + UI::View *v = gameList_->GetViewByIndex(i); + std::string label = v->DescribeText(); + // TODO: Maybe we should just save the gameButtons list, though nice to search dirs too? + // This is a bit of a hack to recognize a pending game title. + if (label == "...") { + searchPending_ = true; + // Hide anything pending while, we'll pop-in search results as they match. + // Note: we leave it at MATCH if gone before, so we don't show it again. + if (v->GetVisibility() == UI::V_VISIBLE) { + if (searchStates_[i] == SearchState::MATCH) + v->SetVisibility(UI::V_GONE); + searchStates_[i] = SearchState::PENDING; + } + continue; + } + + std::transform(label.begin(), label.end(), label.begin(), tolower); + bool match = v->CanBeFocused() && label.find(searchFilter_) != label.npos; + if (match && searchStates_[i] != SearchState::MATCH) { + // It was previously visible and force hidden, so show it again. + v->SetVisibility(UI::V_VISIBLE); + searchStates_[i] = SearchState::MATCH; + } else if (!match && searchStates_[i] == SearchState::MATCH && v->GetVisibility() == UI::V_VISIBLE) { + v->SetVisibility(UI::V_GONE); + searchStates_[i] = SearchState::MISMATCH; + } + } +} + UI::EventReturn GameBrowser::LayoutChange(UI::EventParams &e) { *gridStyle_ = e.a == 0 ? true : false; Refresh(); @@ -622,6 +686,9 @@ void GameBrowser::Update() { if (listingPending_ && path_.IsListingReady()) { Refresh(); } + if (searchPending_) { + ApplySearchFilter(); + } } void GameBrowser::Draw(UIContext &dc) { @@ -686,6 +753,7 @@ void GameBrowser::Refresh() { // Kill all the contents Clear(); + searchStates_.clear(); Add(new Spacer(1.0f)); auto mm = GetI18NCategory(I18NCat::MAINMENU); @@ -1219,6 +1287,27 @@ void MainScreen::CreateViews() { } } +bool MainScreen::key(const KeyInput &touch) { + if (touch.flags & KEY_DOWN) { + if (touch.keyCode == NKCODE_CTRL_LEFT || touch.keyCode == NKCODE_CTRL_RIGHT) + searchKeyModifier_ = true; + if (touch.keyCode == NKCODE_F && searchKeyModifier_) { +#if PPSSPP_PLATFORM(WINDOWS) || defined(USING_QT_UI) || defined(__ANDROID__) + auto se = GetI18NCategory(I18NCat::SEARCH); + System_InputBoxGetString(se->T("Search term"), searchFilter_, [&](const std::string &value, int) { + searchFilter_ = StripSpaces(value); + searchChanged_ = true; + }); +#endif + } + } else if (touch.flags & KEY_UP) { + if (touch.keyCode == NKCODE_CTRL_LEFT || touch.keyCode == NKCODE_CTRL_RIGHT) + searchKeyModifier_ = false; + } + + return UIScreenWithBackground::key(touch); +} + UI::EventReturn MainScreen::OnAllowStorage(UI::EventParams &e) { System_AskForPermission(SYSTEM_PERMISSION_STORAGE); return UI::EVENT_DONE; @@ -1276,6 +1365,12 @@ void MainScreen::sendMessage(const char *message, const char *value) { void MainScreen::update() { UIScreen::update(); UpdateUIState(UISTATE_MENU); + + if (searchChanged_) { + for (auto browser : gameBrowsers_) + browser->ApplySearchFilter(searchFilter_); + searchChanged_ = false; + } } UI::EventReturn MainScreen::OnLoadFile(UI::EventParams &e) { diff --git a/UI/MainScreen.h b/UI/MainScreen.h index ee79725fd1..09681f1001 100644 --- a/UI/MainScreen.h +++ b/UI/MainScreen.h @@ -51,6 +51,7 @@ public: void FocusGame(const Path &gamePath); void SetPath(const Path &path); + void ApplySearchFilter(const std::string &filter); void Draw(UIContext &dc) override; void Update() override; @@ -58,6 +59,7 @@ protected: virtual bool DisplayTopBar(); virtual bool HasSpecialFiles(std::vector &filenames); virtual Path HomePath(); + void ApplySearchFilter(); void Refresh(); @@ -80,14 +82,23 @@ private: UI::EventReturn OnRecentClear(UI::EventParams &e); UI::EventReturn OnHomebrewStore(UI::EventParams &e); + enum class SearchState { + MATCH, + MISMATCH, + PENDING, + }; + UI::ViewGroup *gameList_ = nullptr; PathBrowser path_; bool *gridStyle_ = nullptr; BrowseFlags browseFlags_; std::string lastText_; std::string lastLink_; + std::string searchFilter_; + std::vector searchStates_; Path focusGamePath_; bool listingPending_ = false; + bool searchPending_ = false; float lastScale_ = 1.0f; bool lastLayoutWasGrid_ = true; ScreenManager *screenManager_; @@ -107,6 +118,8 @@ public: // Horrible hack to show the demos & homebrew tab after having installed a game from a zip file. static bool showHomebrewTab; + bool key(const KeyInput &touch) override; + protected: void CreateViews() override; void DrawBackground(UIContext &dc) override; @@ -148,6 +161,9 @@ protected: bool lastVertical_; bool confirmedTemporary_ = false; UI::ScrollView *scrollAllGames_ = nullptr; + bool searchKeyModifier_ = false; + bool searchChanged_ = false; + std::string searchFilter_; friend class RemoteISOBrowseScreen; };