diff --git a/Common/UI/ViewGroup.cpp b/Common/UI/ViewGroup.cpp index 1f8ebd0436f9..7f133a25b62f 100644 --- a/Common/UI/ViewGroup.cpp +++ b/Common/UI/ViewGroup.cpp @@ -1288,7 +1288,7 @@ void TabHolder::SetCurrentTab(int tab, bool skipTween) { currentTab_ = tab; } - tabStrip_->SetSelection(tab); + tabStrip_->SetSelection(tab, false); } EventReturn TabHolder::OnTabClick(EventParams &e) { @@ -1369,7 +1369,7 @@ EventReturn ChoiceStrip::OnChoiceClick(EventParams &e) { return OnChoice.Dispatch(e2); } -void ChoiceStrip::SetSelection(int sel) { +void ChoiceStrip::SetSelection(int sel, bool triggerClick) { int prevSelected = selected_; StickyChoice *prevChoice = Choice(selected_); if (prevChoice) @@ -1384,7 +1384,7 @@ void ChoiceStrip::SetSelection(int sel) { e.v = views_[selected_]; e.a = selected_; // Set to 0 to indicate a selection change (not a click.) - e.b = 0; + e.b = triggerClick ? 1 : 0; OnChoice.Trigger(e); } } @@ -1398,16 +1398,16 @@ void ChoiceStrip::HighlightChoice(unsigned int choice){ bool ChoiceStrip::Key(const KeyInput &input) { bool ret = false; - if (input.flags & KEY_DOWN) { + if (topTabs_ && (input.flags & KEY_DOWN)) { if (IsTabLeftKey(input)) { if (selected_ > 0) { - SetSelection(selected_ - 1); + SetSelection(selected_ - 1, true); UI::PlayUISound(UI::UISound::TOGGLE_OFF); // Maybe make specific sounds for this at some point? } ret = true; } else if (IsTabRightKey(input)) { if (selected_ < (int)views_.size() - 1) { - SetSelection(selected_ + 1); + SetSelection(selected_ + 1, true); UI::PlayUISound(UI::UISound::TOGGLE_ON); } ret = true; diff --git a/Common/UI/ViewGroup.h b/Common/UI/ViewGroup.h index dfedc175d813..5bbac5e06066 100644 --- a/Common/UI/ViewGroup.h +++ b/Common/UI/ViewGroup.h @@ -300,7 +300,7 @@ class ChoiceStrip : public LinearLayout { void AddChoice(ImageID buttonImage); int GetSelection() const { return selected_; } - void SetSelection(int sel); + void SetSelection(int sel, bool triggerClick); void HighlightChoice(unsigned int choice); diff --git a/UI/ComboKeyMappingScreen.cpp b/UI/ComboKeyMappingScreen.cpp index d5742c575a4d..f1582ef9aa1d 100644 --- a/UI/ComboKeyMappingScreen.cpp +++ b/UI/ComboKeyMappingScreen.cpp @@ -52,7 +52,7 @@ void ComboKeyScreen::CreateViews() { for (int i = 0; i < 5; i++) { comboselect->AddChoice(comboKeyImages[i]); } - comboselect->SetSelection(*mode); + comboselect->SetSelection(*mode, false); comboselect->OnChoice.Handle(this, &ComboKeyScreen::onCombo); leftColumn->Add(comboselect); root__->Add(leftColumn); diff --git a/UI/DisplayLayoutScreen.cpp b/UI/DisplayLayoutScreen.cpp index 0ab23394ec57..2211cb6cbbf6 100644 --- a/UI/DisplayLayoutScreen.cpp +++ b/UI/DisplayLayoutScreen.cpp @@ -319,7 +319,7 @@ void DisplayLayoutScreen::CreateViews() { mode_ = new ChoiceStrip(ORIENT_VERTICAL, new AnchorLayoutParams(leftColumnWidth, WRAP_CONTENT, 10 + leftInset, NONE, NONE, 158 + 64 + 10)); mode_->AddChoice(di->T("Move")); mode_->AddChoice(di->T("Resize")); - mode_->SetSelection(0); + mode_->SetSelection(0, false); } displayRepresentation_ = new DragDropDisplay(g_Config.fSmallDisplayOffsetX, g_Config.fSmallDisplayOffsetY, ImageID("I_PSP_DISPLAY"), ScaleSettingToUI(), bounds); displayRepresentation_->SetVisibility(V_VISIBLE); diff --git a/UI/MainScreen.cpp b/UI/MainScreen.cpp index 6b4ab43351e0..8ab0288b8be7 100644 --- a/UI/MainScreen.cpp +++ b/UI/MainScreen.cpp @@ -664,7 +664,7 @@ void GameBrowser::Refresh() { ChoiceStrip *layoutChoice = topBar->Add(new ChoiceStrip(ORIENT_HORIZONTAL)); layoutChoice->AddChoice(ImageID("I_GRID")); layoutChoice->AddChoice(ImageID("I_LINES")); - layoutChoice->SetSelection(*gridStyle_ ? 0 : 1); + layoutChoice->SetSelection(*gridStyle_ ? 0 : 1, false); layoutChoice->OnChoice.Handle(this, &GameBrowser::LayoutChange); topBar->Add(new Choice(ImageID("I_GEAR"), new LayoutParams(64.0f, 64.0f)))->OnClick.Handle(this, &GameBrowser::GridSettingsClick); Add(topBar); diff --git a/UI/SavedataScreen.cpp b/UI/SavedataScreen.cpp index 494af9494184..245897b8f526 100644 --- a/UI/SavedataScreen.cpp +++ b/UI/SavedataScreen.cpp @@ -20,10 +20,12 @@ #include "Common/Data/Color/RGBAUtil.h" #include "Common/Render/DrawBuffer.h" +#include "Common/Data/Encoding/Utf8.h" #include "Common/Data/Text/I18n.h" #include "Common/Math/curves.h" +#include "Common/System/NativeApp.h" +#include "Common/System/System.h" #include "Common/Thread/PrioritizedWorkQueue.h" -#include "Common/Data/Encoding/Utf8.h" #include "Common/UI/Context.h" #include "Common/UI/View.h" #include "Common/UI/ViewGroup.h" @@ -174,6 +176,7 @@ class SavedataButton : public UI::Clickable { } void Draw(UIContext &dc) override; + bool UpdateText(); std::string DescribeText() const override; void GetContentDimensions(const UIContext &dc, float &w, float &h) const override { w = 500; @@ -183,6 +186,8 @@ class SavedataButton : public UI::Clickable { const std::string &GamePath() const { return savePath_; } private: + void UpdateText(const std::shared_ptr &ginfo); + std::string savePath_; std::string title_; std::string subtitle_; @@ -202,6 +207,26 @@ static std::string CleanSaveString(std::string str) { return s; } +bool SavedataButton::UpdateText() { + std::shared_ptr ginfo = g_gameInfoCache->GetInfo(nullptr, savePath_, GAMEINFO_WANTSIZE); + if (!ginfo->pending) { + UpdateText(ginfo); + return true; + } + return false; +} + +void SavedataButton::UpdateText(const std::shared_ptr &ginfo) { + const std::string currentTitle = ginfo->GetTitle(); + if (!currentTitle.empty()) { + title_ = CleanSaveString(currentTitle); + } + if (subtitle_.empty() && ginfo->gameSize > 0) { + std::string savedata_title = ginfo->paramSFO.GetValueString("SAVEDATA_TITLE"); + subtitle_ = CleanSaveString(savedata_title) + StringFromFormat(" (%lld kB)", ginfo->gameSize / 1024); + } +} + void SavedataButton::Draw(UIContext &dc) { std::shared_ptr ginfo = g_gameInfoCache->GetInfo(dc.GetDrawContext(), savePath_, GAMEINFO_WANTSIZE); Draw::Texture *texture = 0; @@ -287,15 +312,7 @@ void SavedataButton::Draw(UIContext &dc) { dc.Draw()->Flush(); dc.PushScissor(bounds_); - const std::string currentTitle = ginfo->GetTitle(); - if (!currentTitle.empty()) { - title_ = CleanSaveString(currentTitle); - } - if (subtitle_.empty() && ginfo->gameSize > 0) { - std::string savedata_title = ginfo->paramSFO.GetValueString("SAVEDATA_TITLE"); - subtitle_ = CleanSaveString(savedata_title) + StringFromFormat(" (%lld kB)", ginfo->gameSize / 1024); - } - + UpdateText(ginfo); dc.MeasureText(dc.GetFontStyle(), 1.0f, 1.0f, title_.c_str(), &tw, &th, 0); int availableWidth = bounds_.w - 150; @@ -334,6 +351,58 @@ SavedataBrowser::SavedataBrowser(std::string path, UI::LayoutParams *layoutParam Refresh(); } +void SavedataBrowser::Update() { + if (searchPending_) { + searchPending_ = false; + + int n = gameList_->GetNumSubviews(); + bool matches = searchFilter_.empty(); + for (int i = 0; i < n; ++i) { + SavedataButton *v = static_cast(gameList_->GetViewByIndex(i)); + + // Note: might be resetting to empty string. Can do that right away. + if (searchFilter_.empty()) { + v->SetVisibility(UI::V_VISIBLE); + continue; + } + + if (!v->UpdateText()) { + // We'll need to wait until the text is loaded. + searchPending_ = true; + v->SetVisibility(UI::V_GONE); + continue; + } + + std::string label = v->DescribeText(); + std::transform(label.begin(), label.end(), label.begin(), tolower); + bool match = label.find(searchFilter_) != label.npos; + matches = matches || match; + v->SetVisibility(match ? UI::V_VISIBLE : UI::V_GONE); + } + + if (searchingView_) { + bool show = !searchFilter_.empty() && (matches || searchPending_); + searchingView_->SetVisibility(show ? UI::V_VISIBLE : UI::V_GONE); + } + if (noMatchView_) + noMatchView_->SetVisibility(matches || searchPending_ ? UI::V_GONE : UI::V_VISIBLE); + } +} + +void SavedataBrowser::SetSearchFilter(const std::string &filter) { + auto sa = GetI18NCategory("Savedata"); + + searchFilter_.resize(filter.size()); + std::transform(filter.begin(), filter.end(), searchFilter_.begin(), tolower); + + if (gameList_) + searchPending_ = true; + if (noMatchView_) + noMatchView_->SetText(ReplaceAll(sa->T("Nothing matching '%1' was found."), "%1", filter)); + if (searchingView_) + searchingView_->SetText(ReplaceAll(sa->T("Showing matches for '%1'."), "%1", filter)); +} + void SavedataBrowser::SetSortOption(SavedataSortOption opt) { sortOption_ = opt; if (gameList_) { @@ -415,16 +484,10 @@ void SavedataBrowser::Refresh() { auto mm = GetI18NCategory("MainMenu"); auto sa = GetI18NCategory("Savedata"); - SortedLinearLayout *gl = new SortedLinearLayout(UI::ORIENT_VERTICAL, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT)); - gl->SetSpacing(4.0f); - gameList_ = gl; - Add(gameList_); - // Find games in the current directory and create new ones. std::vector savedataButtons; std::vector fileInfo; - getFilesInDir(path_.c_str(), &fileInfo, "ppst:"); for (size_t i = 0; i < fileInfo.size(); i++) { @@ -439,19 +502,35 @@ void SavedataBrowser::Refresh() { } } - for (size_t i = 0; i < savedataButtons.size(); i++) { - SavedataButton *b = gameList_->Add(savedataButtons[i]); - b->OnClick.Handle(this, &SavedataBrowser::SavedataButtonClick); - } + ViewGroup *group = new LinearLayout(ORIENT_VERTICAL, new UI::LinearLayoutParams(UI::Margins(12, 0))); + Add(group); if (savedataButtons.empty()) { - ViewGroup *group = new LinearLayout(ORIENT_VERTICAL, new UI::LinearLayoutParams(UI::Margins(12, 0))); group->Add(new TextView(sa->T("None yet. Things will appear here after you save."))); - gameList_->Add(group); + gameList_ = nullptr; + noMatchView_ = nullptr; + searchingView_ = nullptr; + } else { + noMatchView_ = group->Add(new TextView(sa->T("Nothing matching '%1' was found"))); + noMatchView_->SetVisibility(UI::V_GONE); + searchingView_ = group->Add(new TextView(sa->T("Showing matches for '%1'"))); + searchingView_->SetVisibility(UI::V_GONE); + + SortedLinearLayout *gl = new SortedLinearLayout(UI::ORIENT_VERTICAL, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT)); + gl->SetSpacing(4.0f); + gameList_ = gl; + Add(gameList_); + + for (size_t i = 0; i < savedataButtons.size(); i++) { + SavedataButton *b = gameList_->Add(savedataButtons[i]); + b->OnClick.Handle(this, &SavedataBrowser::SavedataButtonClick); + } } // Reapply. SetSortOption(sortOption_); + if (!searchFilter_.empty()) + SetSearchFilter(searchFilter_); } UI::EventReturn SavedataBrowser::SavedataButtonClick(UI::EventParams &e) { @@ -484,7 +563,8 @@ void SavedataScreen::CreateViews() { gridStyle_ = false; root_ = new AnchorLayout(); - LinearLayout *main = new LinearLayout(ORIENT_VERTICAL, new AnchorLayoutParams(FILL_PARENT, FILL_PARENT)); + // Make space for buttons. + LinearLayout *main = new LinearLayout(ORIENT_VERTICAL, new AnchorLayoutParams(FILL_PARENT, FILL_PARENT, 0, 0, 0, 84.0f)); TabHolder *tabs = new TabHolder(ORIENT_HORIZONTAL, 64, new LinearLayoutParams(FILL_PARENT, FILL_PARENT, 1.0f)); tabs->SetTag("Savedata"); @@ -492,6 +572,8 @@ void SavedataScreen::CreateViews() { scroll->SetTag("SavedataBrowser"); dataBrowser_ = scroll->Add(new SavedataBrowser(savedata_dir, new LayoutParams(FILL_PARENT, FILL_PARENT))); dataBrowser_->SetSortOption(sortOption_); + if (!searchFilter_.empty()) + dataBrowser_->SetSearchFilter(searchFilter_); dataBrowser_->OnChoice.Handle(this, &SavedataScreen::OnSavedataButtonClick); tabs->AddTab(sa->T("Save Data"), scroll); @@ -500,19 +582,25 @@ void SavedataScreen::CreateViews() { scroll2->SetTag("SavedataStatesBrowser"); stateBrowser_ = scroll2->Add(new SavedataBrowser(savestate_dir)); stateBrowser_->SetSortOption(sortOption_); + if (!searchFilter_.empty()) + stateBrowser_->SetSearchFilter(searchFilter_); stateBrowser_->OnChoice.Handle(this, &SavedataScreen::OnSavedataButtonClick); tabs->AddTab(sa->T("Save States"), scroll2); main->Add(tabs); - main->Add(new Button(di->T("Back"), new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT, 0.0f)))->OnClick.Handle(this, &UIScreen::OnBack); ChoiceStrip *sortStrip = new ChoiceStrip(ORIENT_HORIZONTAL, new AnchorLayoutParams(NONE, 0, 0, NONE)); sortStrip->AddChoice(sa->T("Filename")); sortStrip->AddChoice(sa->T("Size")); sortStrip->AddChoice(sa->T("Date")); - sortStrip->SetSelection((int)sortOption_); + sortStrip->SetSelection((int)sortOption_, false); sortStrip->OnChoice.Handle(this, &SavedataScreen::OnSortClick); + AddStandardBack(root_); +#if PPSSPP_PLATFORM(WINDOWS) || defined(USING_QT_UI) || defined(__ANDROID__) + root_->Add(new Choice(di->T("Search"), "", false, new AnchorLayoutParams(WRAP_CONTENT, 64, NONE, NONE, 10, 10)))->OnClick.Handle(this, &SavedataScreen::OnSearch); +#endif + root_->Add(main); root_->Add(sortStrip); } @@ -526,6 +614,18 @@ UI::EventReturn SavedataScreen::OnSortClick(UI::EventParams &e) { return UI::EVENT_DONE; } +UI::EventReturn SavedataScreen::OnSearch(UI::EventParams &e) { + auto di = GetI18NCategory("Dialog"); +#if PPSSPP_PLATFORM(WINDOWS) || defined(USING_QT_UI) || defined(__ANDROID__) + System_InputBoxGetString(di->T("Filter"), searchFilter_, [](bool result, const std::string &value) { + if (result) { + NativeMessageReceived("savedatascreen_search", value.c_str()); + } + }); +#endif + return UI::EVENT_DONE; +} + UI::EventReturn SavedataScreen::OnSavedataButtonClick(UI::EventParams &e) { std::shared_ptr ginfo = g_gameInfoCache->GetInfo(screenManager()->getDrawContext(), e.s, 0); SavedataPopupScreen *popupScreen = new SavedataPopupScreen(e.s, ginfo->GetTitle()); @@ -542,3 +642,12 @@ void SavedataScreen::dialogFinished(const Screen *dialog, DialogResult result) { RecreateViews(); } } + +void SavedataScreen::sendMessage(const char *message, const char *value) { + UIDialogScreenWithGameBackground::sendMessage(message, value); + if (!strcmp(message, "savedatascreen_search")) { + searchFilter_ = value; + dataBrowser_->SetSearchFilter(searchFilter_); + stateBrowser_->SetSearchFilter(searchFilter_); + } +} diff --git a/UI/SavedataScreen.h b/UI/SavedataScreen.h index 9e02f8c9cb14..c0909bf32164 100644 --- a/UI/SavedataScreen.h +++ b/UI/SavedataScreen.h @@ -36,7 +36,10 @@ class SavedataBrowser : public UI::LinearLayout { public: SavedataBrowser(std::string path, UI::LayoutParams *layoutParams = 0); + void Update() override; + void SetSortOption(SavedataSortOption opt); + void SetSearchFilter(const std::string &filter); UI::Event OnChoice; @@ -51,7 +54,11 @@ class SavedataBrowser : public UI::LinearLayout { SavedataSortOption sortOption_ = SavedataSortOption::FILENAME; UI::ViewGroup *gameList_ = nullptr; + UI::TextView *noMatchView_ = nullptr; + UI::TextView *searchingView_ = nullptr; std::string path_; + std::string searchFilter_; + bool searchPending_ = false; }; class SavedataScreen : public UIDialogScreenWithGameBackground { @@ -61,13 +68,17 @@ class SavedataScreen : public UIDialogScreenWithGameBackground { ~SavedataScreen(); void dialogFinished(const Screen *dialog, DialogResult result) override; + void sendMessage(const char *message, const char *value) override; protected: UI::EventReturn OnSavedataButtonClick(UI::EventParams &e); UI::EventReturn OnSortClick(UI::EventParams &e); + UI::EventReturn OnSearch(UI::EventParams &e); void CreateViews() override; + bool gridStyle_; SavedataSortOption sortOption_ = SavedataSortOption::FILENAME; SavedataBrowser *dataBrowser_; SavedataBrowser *stateBrowser_; + std::string searchFilter_; }; diff --git a/UI/TouchControlLayoutScreen.cpp b/UI/TouchControlLayoutScreen.cpp index 9a6b7d061593..25a5a07df020 100644 --- a/UI/TouchControlLayoutScreen.cpp +++ b/UI/TouchControlLayoutScreen.cpp @@ -511,7 +511,7 @@ void TouchControlLayoutScreen::CreateViews() { mode_ = new ChoiceStrip(ORIENT_VERTICAL, new AnchorLayoutParams(leftColumnWidth, WRAP_CONTENT, 10, NONE, NONE, 140 + 158 + 64 + 10)); mode_->AddChoice(di->T("Move")); mode_->AddChoice(di->T("Resize")); - mode_->SetSelection(0); + mode_->SetSelection(0, false); mode_->OnChoice.Handle(this, &TouchControlLayoutScreen::OnMode); reset->OnClick.Handle(this, &TouchControlLayoutScreen::OnReset);