1 // Copyright (c) 2013- PPSSPP Project.
2
3 // This program is free software: you can redistribute it and/or modify
4 // it under the terms of the GNU General Public License as published by
5 // the Free Software Foundation, version 2.0 or later versions.
6
7 // This program is distributed in the hope that it will be useful,
8 // but WITHOUT ANY WARRANTY; without even the implied warranty of
9 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 // GNU General Public License 2.0 for more details.
11
12 // A copy of the GPL 2.0 should have been included with the program.
13 // If not, see http://www.gnu.org/licenses/
14
15 // Official git repository and contact information can be found at
16 // https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/.
17
18 #include <algorithm>
19 #include <functional>
20
21 #include "Common/Data/Color/RGBAUtil.h"
22 #include "Common/Render/DrawBuffer.h"
23 #include "Common/Data/Encoding/Utf8.h"
24 #include "Common/Data/Text/I18n.h"
25 #include "Common/Math/curves.h"
26 #include "Common/System/NativeApp.h"
27 #include "Common/System/System.h"
28 #include "Common/Data/Encoding/Utf8.h"
29 #include "Common/UI/Context.h"
30 #include "Common/UI/View.h"
31 #include "Common/UI/ViewGroup.h"
32 #include "UI/SavedataScreen.h"
33 #include "UI/MainScreen.h"
34 #include "UI/GameInfoCache.h"
35 #include "UI/PauseScreen.h"
36
37 #include "Common/File/FileUtil.h"
38 #include "Common/TimeUtil.h"
39 #include "Common/StringUtils.h"
40 #include "Core/Host.h"
41 #include "Core/Config.h"
42 #include "Core/Loaders.h"
43 #include "Core/SaveState.h"
44 #include "Core/System.h"
45 #include "Core/HLE/sceUtility.h"
46
47 class SavedataButton;
48
GetFileDateAsString(const Path & filename)49 std::string GetFileDateAsString(const Path &filename) {
50 tm time;
51 if (File::GetModifTime(filename, time)) {
52 char buf[256];
53 switch (g_Config.iDateFormat) {
54 case PSP_SYSTEMPARAM_DATE_FORMAT_YYYYMMDD:
55 strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", &time);
56 break;
57 case PSP_SYSTEMPARAM_DATE_FORMAT_MMDDYYYY:
58 strftime(buf, sizeof(buf), "%m-%d-%Y %H:%M:%S", &time);
59 break;
60 case PSP_SYSTEMPARAM_DATE_FORMAT_DDMMYYYY:
61 strftime(buf, sizeof(buf), "%d-%m-%Y %H:%M:%S", &time);
62 break;
63 default: // Should never happen
64 return "";
65 }
66 return std::string(buf);
67 }
68 return "";
69 }
70
TrimString(const std::string & str)71 static std::string TrimString(const std::string &str) {
72 size_t pos = str.find_last_not_of(" \r\n\t");
73 if (pos != str.npos) {
74 return str.substr(0, pos + 1);
75 }
76 return str;
77 }
78
79 class SavedataPopupScreen : public PopupScreen {
80 public:
SavedataPopupScreen(std::string savePath,std::string title)81 SavedataPopupScreen(std::string savePath, std::string title) : PopupScreen(TrimString(title)), savePath_(savePath) {
82 }
83
CreatePopupContents(UI::ViewGroup * parent)84 void CreatePopupContents(UI::ViewGroup *parent) override {
85 using namespace UI;
86 UIContext &dc = *screenManager()->getUIContext();
87 const Style &textStyle = dc.theme->popupStyle;
88
89 std::shared_ptr<GameInfo> ginfo = g_gameInfoCache->GetInfo(screenManager()->getDrawContext(), savePath_, GAMEINFO_WANTBG | GAMEINFO_WANTSIZE);
90 if (!ginfo)
91 return;
92
93 ScrollView *contentScroll = new ScrollView(ORIENT_VERTICAL, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT, 1.0f));
94 LinearLayout *content = new LinearLayout(ORIENT_VERTICAL);
95 parent->Add(contentScroll);
96 contentScroll->Add(content);
97 LinearLayout *toprow = new LinearLayout(ORIENT_HORIZONTAL, new LayoutParams(FILL_PARENT, WRAP_CONTENT));
98 content->Add(toprow);
99
100 auto sa = GetI18NCategory("Savedata");
101 if (ginfo->fileType == IdentifiedFileType::PSP_SAVEDATA_DIRECTORY) {
102 std::string savedata_detail = ginfo->paramSFO.GetValueString("SAVEDATA_DETAIL");
103 std::string savedata_title = ginfo->paramSFO.GetValueString("SAVEDATA_TITLE");
104
105 if (ginfo->icon.texture) {
106 toprow->Add(new GameIconView(savePath_, 2.0f, new LinearLayoutParams(Margins(10, 5))));
107 }
108 LinearLayout *topright = new LinearLayout(ORIENT_VERTICAL, new LinearLayoutParams(WRAP_CONTENT, WRAP_CONTENT, 1.0f));
109 topright->SetSpacing(1.0f);
110 topright->Add(new TextView(savedata_title, ALIGN_LEFT | FLAG_WRAP_TEXT, false))->SetTextColor(textStyle.fgColor);
111 topright->Add(new TextView(StringFromFormat("%lld kB", ginfo->gameSize / 1024), 0, true))->SetTextColor(textStyle.fgColor);
112 topright->Add(new TextView(GetFileDateAsString(savePath_ / "PARAM.SFO"), 0, true))->SetTextColor(textStyle.fgColor);
113 toprow->Add(topright);
114 content->Add(new Spacer(3.0));
115 content->Add(new TextView(ReplaceAll(savedata_detail, "\r", ""), ALIGN_LEFT | FLAG_WRAP_TEXT, true, new LinearLayoutParams(Margins(10, 0))))->SetTextColor(textStyle.fgColor);
116 content->Add(new Spacer(3.0));
117 } else {
118 Path image_path = savePath_.WithReplacedExtension(".ppst", ".jpg");
119 if (File::Exists(image_path)) {
120 toprow->Add(new AsyncImageFileView(image_path, IS_KEEP_ASPECT, new LinearLayoutParams(480, 272, Margins(10, 0))));
121 } else {
122 toprow->Add(new TextView(sa->T("No screenshot"), new LinearLayoutParams(Margins(10, 5))))->SetTextColor(textStyle.fgColor);
123 }
124 content->Add(new TextView(GetFileDateAsString(savePath_), 0, true, new LinearLayoutParams(Margins(10, 5))))->SetTextColor(textStyle.fgColor);
125 }
126
127 auto di = GetI18NCategory("Dialog");
128 LinearLayout *buttons = new LinearLayout(ORIENT_HORIZONTAL, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT));
129 buttons->SetSpacing(0);
130 Margins buttonMargins(5, 5);
131
132 buttons->Add(new Button(di->T("Back"), new LinearLayoutParams(1.0f, buttonMargins)))->OnClick.Handle<UIScreen>(this, &UIScreen::OnBack);
133 buttons->Add(new Button(di->T("Delete"), new LinearLayoutParams(1.0f, buttonMargins)))->OnClick.Handle(this, &SavedataPopupScreen::OnDeleteButtonClick);
134 parent->Add(buttons);
135 }
136
137 protected:
PopupWidth() const138 UI::Size PopupWidth() const override { return 500; }
139
140 private:
141 UI::EventReturn OnDeleteButtonClick(UI::EventParams &e);
142 Path savePath_;
143 };
144
145 class SortedLinearLayout : public UI::LinearLayoutList {
146 public:
147 typedef std::function<void(View *)> PrepFunc;
148 typedef std::function<bool(const View *, const View *)> CompareFunc;
149
SortedLinearLayout(UI::Orientation orientation,UI::LayoutParams * layoutParams=nullptr)150 SortedLinearLayout(UI::Orientation orientation, UI::LayoutParams *layoutParams = nullptr)
151 : UI::LinearLayoutList(orientation, layoutParams) {
152 }
153
SetCompare(const PrepFunc & prepFunc,const CompareFunc & lessFunc)154 void SetCompare(const PrepFunc &prepFunc, const CompareFunc &lessFunc) {
155 prepIndex_ = 0;
156 prepFunc_ = prepFunc;
157 lessFunc_ = lessFunc;
158 }
159
160 void Update() override;
161
162 private:
163 size_t prepIndex_ = 0;
164 PrepFunc prepFunc_;
165 CompareFunc lessFunc_;
166 };
167
Update()168 void SortedLinearLayout::Update() {
169 if (prepFunc_) {
170 // Try to avoid dropping more than a frame, prefer items shift.
171 constexpr double ALLOWED_TIME = 0.95 / 60.0;
172 double start_time = time_now_d();
173 for (; prepIndex_ < views_.size(); ++prepIndex_) {
174 prepFunc_(views_[prepIndex_]);
175 if (time_now_d() > start_time + ALLOWED_TIME) {
176 break;
177 }
178 }
179 }
180 if (lessFunc_) {
181 // We may sort several times while calculating.
182 std::stable_sort(views_.begin(), views_.end(), lessFunc_);
183 }
184 // We're done if we got through all items.
185 if (prepIndex_ >= views_.size()) {
186 prepFunc_ = PrepFunc();
187 lessFunc_ = CompareFunc();
188 }
189
190 UI::LinearLayout::Update();
191 }
192
193 class SavedataButton : public UI::Clickable {
194 public:
SavedataButton(const Path & gamePath,UI::LayoutParams * layoutParams=0)195 SavedataButton(const Path &gamePath, UI::LayoutParams *layoutParams = 0)
196 : UI::Clickable(layoutParams), savePath_(gamePath) {
197 SetTag(gamePath.ToString());
198 }
199
200 void Draw(UIContext &dc) override;
201 bool UpdateText();
202 std::string DescribeText() const override;
GetContentDimensions(const UIContext & dc,float & w,float & h) const203 void GetContentDimensions(const UIContext &dc, float &w, float &h) const override {
204 w = 500;
205 h = 74;
206 }
207
GamePath() const208 const Path &GamePath() const { return savePath_; }
209
GetTotalSize() const210 uint64_t GetTotalSize() const {
211 return totalSize_;
212 }
GetDateSeconds() const213 int64_t GetDateSeconds() const {
214 return dateSeconds_;
215 }
216
217 void UpdateTotalSize();
218 void UpdateDateSeconds();
219
220 private:
221 void UpdateText(const std::shared_ptr<GameInfo> &ginfo);
222
223 Path savePath_;
224 std::string title_;
225 std::string subtitle_;
226 uint64_t totalSize_ = 0;
227 int64_t dateSeconds_ = 0;
228 bool hasTotalSize_ = false;
229 bool hasDateSeconds_ = false;
230 };
231
UpdateTotalSize()232 void SavedataButton::UpdateTotalSize() {
233 if (hasTotalSize_)
234 return;
235
236 File::FileInfo info;
237 if (File::GetFileInfo(savePath_, &info)) {
238 totalSize_ = info.size;
239 if (info.isDirectory)
240 totalSize_ = File::ComputeRecursiveDirectorySize(savePath_);
241 }
242
243 hasTotalSize_ = true;
244 }
245
UpdateDateSeconds()246 void SavedataButton::UpdateDateSeconds() {
247 if (hasDateSeconds_)
248 return;
249
250 File::FileInfo info;
251 if (File::GetFileInfo(savePath_, &info)) {
252 dateSeconds_ = info.mtime;
253 if (info.isDirectory && File::GetFileInfo(savePath_ / "PARAM.SFO", &info)) {
254 dateSeconds_ = info.mtime;
255 }
256 }
257
258 hasDateSeconds_ = true;
259 }
260
OnDeleteButtonClick(UI::EventParams & e)261 UI::EventReturn SavedataPopupScreen::OnDeleteButtonClick(UI::EventParams &e) {
262 std::shared_ptr<GameInfo> ginfo = g_gameInfoCache->GetInfo(nullptr, savePath_, GAMEINFO_WANTSIZE);
263 ginfo->Delete();
264 TriggerFinish(DR_NO);
265 return UI::EVENT_DONE;
266 }
267
CleanSaveString(std::string str)268 static std::string CleanSaveString(std::string str) {
269 std::string s = ReplaceAll(str, "&", "&&");
270 s = ReplaceAll(s, "\n", " ");
271 s = ReplaceAll(s, "\r", " ");
272 return s;
273 }
274
UpdateText()275 bool SavedataButton::UpdateText() {
276 std::shared_ptr<GameInfo> ginfo = g_gameInfoCache->GetInfo(nullptr, savePath_, GAMEINFO_WANTSIZE);
277 if (!ginfo->pending) {
278 UpdateText(ginfo);
279 return true;
280 }
281 return false;
282 }
283
UpdateText(const std::shared_ptr<GameInfo> & ginfo)284 void SavedataButton::UpdateText(const std::shared_ptr<GameInfo> &ginfo) {
285 const std::string currentTitle = ginfo->GetTitle();
286 if (!currentTitle.empty()) {
287 title_ = CleanSaveString(currentTitle);
288 }
289 if (subtitle_.empty() && ginfo->gameSize > 0) {
290 std::string savedata_title = ginfo->paramSFO.GetValueString("SAVEDATA_TITLE");
291 subtitle_ = CleanSaveString(savedata_title) + StringFromFormat(" (%lld kB)", ginfo->gameSize / 1024);
292 }
293 }
294
Draw(UIContext & dc)295 void SavedataButton::Draw(UIContext &dc) {
296 std::shared_ptr<GameInfo> ginfo = g_gameInfoCache->GetInfo(dc.GetDrawContext(), savePath_, GAMEINFO_WANTSIZE);
297 Draw::Texture *texture = 0;
298 u32 color = 0, shadowColor = 0;
299 using namespace UI;
300
301 if (ginfo->icon.texture) {
302 texture = ginfo->icon.texture->GetTexture();
303 }
304
305 int x = bounds_.x;
306 int y = bounds_.y;
307 int w = 144;
308 int h = bounds_.h;
309
310 UI::Style style = dc.theme->itemStyle;
311 if (down_)
312 style = dc.theme->itemDownStyle;
313
314 h = bounds_.h;
315 if (HasFocus())
316 style = down_ ? dc.theme->itemDownStyle : dc.theme->itemFocusedStyle;
317
318 Drawable bg = style.background;
319
320 dc.Draw()->Flush();
321 dc.RebindTexture();
322 dc.FillRect(bg, bounds_);
323 dc.Draw()->Flush();
324
325 if (texture) {
326 color = whiteAlpha(ease((time_now_d() - ginfo->icon.timeLoaded) * 2));
327 shadowColor = blackAlpha(ease((time_now_d() - ginfo->icon.timeLoaded) * 2));
328 float tw = texture->Width();
329 float th = texture->Height();
330
331 // Adjust position so we don't stretch the image vertically or horizontally.
332 // TODO: Add a param to specify fit? The below assumes it's never too wide.
333 float nw = h * tw / th;
334 x += (w - nw) / 2.0f;
335 w = nw;
336 }
337
338 int txOffset = down_ ? 4 : 0;
339 txOffset = 0;
340
341 Bounds overlayBounds = bounds_;
342
343 // Render button
344 int dropsize = 10;
345 if (texture) {
346 if (txOffset) {
347 dropsize = 3;
348 y += txOffset * 2;
349 overlayBounds.y += txOffset * 2;
350 }
351 if (HasFocus()) {
352 dc.Draw()->Flush();
353 dc.RebindTexture();
354 float pulse = sin(time_now_d() * 7.0) * 0.25 + 0.8;
355 dc.Draw()->DrawImage4Grid(dc.theme->dropShadow4Grid, x - dropsize*1.5f, y - dropsize*1.5f, x + w + dropsize*1.5f, y + h + dropsize*1.5f, alphaMul(color, pulse), 1.0f);
356 dc.Draw()->Flush();
357 } else {
358 dc.Draw()->Flush();
359 dc.RebindTexture();
360 dc.Draw()->DrawImage4Grid(dc.theme->dropShadow4Grid, x - dropsize, y - dropsize*0.5f, x + w + dropsize, y + h + dropsize*1.5, alphaMul(shadowColor, 0.5f), 1.0f);
361 dc.Draw()->Flush();
362 }
363 }
364
365 if (texture) {
366 dc.Draw()->Flush();
367 dc.GetDrawContext()->BindTexture(0, texture);
368 dc.Draw()->DrawTexRect(x, y, x + w, y + h, 0, 0, 1, 1, color);
369 dc.Draw()->Flush();
370 }
371
372 dc.Draw()->Flush();
373 dc.RebindTexture();
374 dc.SetFontStyle(dc.theme->uiFont);
375
376 float tw, th;
377 dc.Draw()->Flush();
378 dc.PushScissor(bounds_);
379
380 UpdateText(ginfo);
381 dc.MeasureText(dc.GetFontStyle(), 1.0f, 1.0f, title_.c_str(), &tw, &th, 0);
382
383 int availableWidth = bounds_.w - 150;
384 float sineWidth = std::max(0.0f, (tw - availableWidth)) / 2.0f;
385
386 float tx = 150.0f;
387 if (availableWidth < tw) {
388 float overageRatio = 1.5f * availableWidth * 1.0f / tw;
389 tx -= (1.0f + sin(time_now_d() * overageRatio)) * sineWidth;
390 Bounds tb = bounds_;
391 tb.x = bounds_.x + 150.0f;
392 tb.w = std::max(1.0f, bounds_.w - 150.0f);
393 dc.PushScissor(tb);
394 }
395 dc.DrawText(title_.c_str(), bounds_.x + tx, bounds_.y + 4, style.fgColor, ALIGN_TOPLEFT);
396 dc.SetFontScale(0.6f, 0.6f);
397 dc.DrawText(subtitle_.c_str(), bounds_.x + tx, bounds_.y2() - 7, style.fgColor, ALIGN_BOTTOM);
398 dc.SetFontScale(1.0f, 1.0f);
399
400 if (availableWidth < tw) {
401 dc.PopScissor();
402 }
403 dc.Draw()->Flush();
404 dc.PopScissor();
405
406 dc.RebindTexture();
407 }
408
DescribeText() const409 std::string SavedataButton::DescribeText() const {
410 auto u = GetI18NCategory("UI Elements");
411 return ReplaceAll(u->T("%1 button"), "%1", title_) + "\n" + subtitle_;
412 }
413
SavedataBrowser(const Path & path,UI::LayoutParams * layoutParams)414 SavedataBrowser::SavedataBrowser(const Path &path, UI::LayoutParams *layoutParams)
415 : LinearLayout(UI::ORIENT_VERTICAL, layoutParams), path_(path) {
416 Refresh();
417 }
418
Update()419 void SavedataBrowser::Update() {
420 LinearLayout::Update();
421 if (searchPending_) {
422 searchPending_ = false;
423
424 int n = gameList_->GetNumSubviews();
425 bool matches = searchFilter_.empty();
426 for (int i = 0; i < n; ++i) {
427 SavedataButton *v = static_cast<SavedataButton *>(gameList_->GetViewByIndex(i));
428
429 // Note: might be resetting to empty string. Can do that right away.
430 if (searchFilter_.empty()) {
431 v->SetVisibility(UI::V_VISIBLE);
432 continue;
433 }
434
435 if (!v->UpdateText()) {
436 // We'll need to wait until the text is loaded.
437 searchPending_ = true;
438 v->SetVisibility(UI::V_GONE);
439 continue;
440 }
441
442 std::string label = v->DescribeText();
443 std::transform(label.begin(), label.end(), label.begin(), tolower);
444 bool match = label.find(searchFilter_) != label.npos;
445 matches = matches || match;
446 v->SetVisibility(match ? UI::V_VISIBLE : UI::V_GONE);
447 }
448
449 if (searchingView_) {
450 bool show = !searchFilter_.empty() && (matches || searchPending_);
451 searchingView_->SetVisibility(show ? UI::V_VISIBLE : UI::V_GONE);
452 }
453 if (noMatchView_)
454 noMatchView_->SetVisibility(matches || searchPending_ ? UI::V_GONE : UI::V_VISIBLE);
455 }
456 }
457
SetSearchFilter(const std::string & filter)458 void SavedataBrowser::SetSearchFilter(const std::string &filter) {
459 auto sa = GetI18NCategory("Savedata");
460
461 searchFilter_.resize(filter.size());
462 std::transform(filter.begin(), filter.end(), searchFilter_.begin(), tolower);
463
464 if (gameList_)
465 searchPending_ = true;
466 if (noMatchView_)
467 noMatchView_->SetText(ReplaceAll(sa->T("Nothing matching '%1' was found."), "%1", filter));
468 if (searchingView_)
469 searchingView_->SetText(ReplaceAll(sa->T("Showing matches for '%1'."), "%1", filter));
470 }
471
SetSortOption(SavedataSortOption opt)472 void SavedataBrowser::SetSortOption(SavedataSortOption opt) {
473 sortOption_ = opt;
474 if (gameList_) {
475 SortedLinearLayout *gl = static_cast<SortedLinearLayout *>(gameList_);
476 if (sortOption_ == SavedataSortOption::FILENAME) {
477 gl->SetCompare(&PrepFilename, &ByFilename);
478 } else if (sortOption_ == SavedataSortOption::SIZE) {
479 gl->SetCompare(&PrepSize, &BySize);
480 } else if (sortOption_ == SavedataSortOption::DATE) {
481 gl->SetCompare(&PrepDate, &ByDate);
482 }
483 }
484 }
485
PrepFilename(UI::View * v)486 void SavedataBrowser::PrepFilename(UI::View *v) {
487 // Nothing needed.
488 }
489
ByFilename(const UI::View * v1,const UI::View * v2)490 bool SavedataBrowser::ByFilename(const UI::View *v1, const UI::View *v2) {
491 const SavedataButton *b1 = static_cast<const SavedataButton *>(v1);
492 const SavedataButton *b2 = static_cast<const SavedataButton *>(v2);
493
494 return strcmp(b1->GamePath().c_str(), b2->GamePath().c_str()) < 0;
495 }
496
PrepSize(UI::View * v)497 void SavedataBrowser::PrepSize(UI::View *v) {
498 SavedataButton *b = static_cast<SavedataButton *>(v);
499 b->UpdateTotalSize();
500 }
501
BySize(const UI::View * v1,const UI::View * v2)502 bool SavedataBrowser::BySize(const UI::View *v1, const UI::View *v2) {
503 const SavedataButton *b1 = static_cast<const SavedataButton *>(v1);
504 const SavedataButton *b2 = static_cast<const SavedataButton *>(v2);
505 const uint64_t size1 = b1->GetTotalSize();
506 const uint64_t size2 = b2->GetTotalSize();
507
508 if (size1 > size2)
509 return true;
510 else if (size1 < size2)
511 return false;
512 return strcmp(b1->GamePath().c_str(), b2->GamePath().c_str()) < 0;
513 }
514
PrepDate(UI::View * v)515 void SavedataBrowser::PrepDate(UI::View *v) {
516 SavedataButton *b = static_cast<SavedataButton *>(v);
517 b->UpdateDateSeconds();
518 }
519
ByDate(const UI::View * v1,const UI::View * v2)520 bool SavedataBrowser::ByDate(const UI::View *v1, const UI::View *v2) {
521 const SavedataButton *b1 = static_cast<const SavedataButton *>(v1);
522 const SavedataButton *b2 = static_cast<const SavedataButton *>(v2);
523 const int64_t time1 = b1->GetDateSeconds();
524 const int64_t time2 = b2->GetDateSeconds();
525
526 if (time1 > time2)
527 return true;
528 if (time1 < time2)
529 return false;
530 return strcmp(b1->GamePath().c_str(), b2->GamePath().c_str()) < 0;
531 }
532
Refresh()533 void SavedataBrowser::Refresh() {
534 using namespace UI;
535
536 // Kill all the contents
537 Clear();
538
539 Add(new Spacer(1.0f));
540 auto mm = GetI18NCategory("MainMenu");
541 auto sa = GetI18NCategory("Savedata");
542
543 // Find games in the current directory and create new ones.
544 std::vector<SavedataButton *> savedataButtons;
545
546 std::vector<File::FileInfo> fileInfo;
547 GetFilesInDir(path_, &fileInfo, "ppst:");
548
549 for (size_t i = 0; i < fileInfo.size(); i++) {
550 bool isState = !fileInfo[i].isDirectory;
551 bool isSaveData = false;
552
553 if (!isState && File::Exists(path_ / fileInfo[i].name / "PARAM.SFO"))
554 isSaveData = true;
555
556 if (isSaveData || isState) {
557 savedataButtons.push_back(new SavedataButton(fileInfo[i].fullName, new UI::LinearLayoutParams(UI::FILL_PARENT, UI::WRAP_CONTENT)));
558 }
559 }
560
561 ViewGroup *group = new LinearLayout(ORIENT_VERTICAL, new UI::LinearLayoutParams(UI::Margins(12, 0)));
562 Add(group);
563
564 if (savedataButtons.empty()) {
565 group->Add(new TextView(sa->T("None yet. Things will appear here after you save.")));
566 gameList_ = nullptr;
567 noMatchView_ = nullptr;
568 searchingView_ = nullptr;
569 } else {
570 noMatchView_ = group->Add(new TextView(sa->T("Nothing matching '%1' was found")));
571 noMatchView_->SetVisibility(UI::V_GONE);
572 searchingView_ = group->Add(new TextView(sa->T("Showing matches for '%1'")));
573 searchingView_->SetVisibility(UI::V_GONE);
574
575 SortedLinearLayout *gl = new SortedLinearLayout(UI::ORIENT_VERTICAL, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT));
576 gl->SetSpacing(4.0f);
577 gameList_ = gl;
578 Add(gameList_);
579
580 for (size_t i = 0; i < savedataButtons.size(); i++) {
581 SavedataButton *b = gameList_->Add(savedataButtons[i]);
582 b->OnClick.Handle(this, &SavedataBrowser::SavedataButtonClick);
583 }
584 }
585
586 // Reapply.
587 SetSortOption(sortOption_);
588 if (!searchFilter_.empty())
589 SetSearchFilter(searchFilter_);
590 }
591
SavedataButtonClick(UI::EventParams & e)592 UI::EventReturn SavedataBrowser::SavedataButtonClick(UI::EventParams &e) {
593 SavedataButton *button = static_cast<SavedataButton *>(e.v);
594 UI::EventParams e2{};
595 e2.v = e.v;
596 e2.s = button->GamePath().ToString();
597 // Insta-update - here we know we are already on the right thread.
598 OnChoice.Trigger(e2);
599 return UI::EVENT_DONE;
600 }
601
SavedataScreen(const Path & gamePath)602 SavedataScreen::SavedataScreen(const Path &gamePath) : UIDialogScreenWithGameBackground(gamePath) {
603 }
604
~SavedataScreen()605 SavedataScreen::~SavedataScreen() {
606 if (g_gameInfoCache) {
607 g_gameInfoCache->PurgeType(IdentifiedFileType::PPSSPP_SAVESTATE);
608 g_gameInfoCache->PurgeType(IdentifiedFileType::PSP_SAVEDATA_DIRECTORY);
609 }
610 }
611
CreateViews()612 void SavedataScreen::CreateViews() {
613 using namespace UI;
614 auto sa = GetI18NCategory("Savedata");
615 auto di = GetI18NCategory("Dialog");
616 Path savedata_dir = GetSysDirectory(DIRECTORY_SAVEDATA);
617 Path savestate_dir = GetSysDirectory(DIRECTORY_SAVESTATE);
618
619 gridStyle_ = false;
620 root_ = new AnchorLayout();
621
622 // Make space for buttons.
623 LinearLayout *main = new LinearLayout(ORIENT_VERTICAL, new AnchorLayoutParams(FILL_PARENT, FILL_PARENT, 0, 0, 0, 84.0f));
624
625 TabHolder *tabs = new TabHolder(ORIENT_HORIZONTAL, 64, new LinearLayoutParams(FILL_PARENT, FILL_PARENT, 1.0f));
626 tabs->SetTag("Savedata");
627 ScrollView *scroll = new ScrollView(ORIENT_VERTICAL, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT));
628 scroll->SetTag("SavedataBrowser");
629 dataBrowser_ = scroll->Add(new SavedataBrowser(savedata_dir, new LayoutParams(FILL_PARENT, FILL_PARENT)));
630 dataBrowser_->SetSortOption(sortOption_);
631 if (!searchFilter_.empty())
632 dataBrowser_->SetSearchFilter(searchFilter_);
633 dataBrowser_->OnChoice.Handle(this, &SavedataScreen::OnSavedataButtonClick);
634
635 tabs->AddTab(sa->T("Save Data"), scroll);
636
637 ScrollView *scroll2 = new ScrollView(ORIENT_VERTICAL, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT));
638 scroll2->SetTag("SavedataStatesBrowser");
639 stateBrowser_ = scroll2->Add(new SavedataBrowser(savestate_dir));
640 stateBrowser_->SetSortOption(sortOption_);
641 if (!searchFilter_.empty())
642 stateBrowser_->SetSearchFilter(searchFilter_);
643 stateBrowser_->OnChoice.Handle(this, &SavedataScreen::OnSavedataButtonClick);
644 tabs->AddTab(sa->T("Save States"), scroll2);
645
646 main->Add(tabs);
647
648 ChoiceStrip *sortStrip = new ChoiceStrip(ORIENT_HORIZONTAL, new AnchorLayoutParams(NONE, 0, 0, NONE));
649 sortStrip->AddChoice(sa->T("Filename"));
650 sortStrip->AddChoice(sa->T("Size"));
651 sortStrip->AddChoice(sa->T("Date"));
652 sortStrip->SetSelection((int)sortOption_, false);
653 sortStrip->OnChoice.Handle<SavedataScreen>(this, &SavedataScreen::OnSortClick);
654
655 AddStandardBack(root_);
656 #if PPSSPP_PLATFORM(WINDOWS) || defined(USING_QT_UI) || defined(__ANDROID__)
657 root_->Add(new Choice(di->T("Search"), "", false, new AnchorLayoutParams(WRAP_CONTENT, 64, NONE, NONE, 10, 10)))->OnClick.Handle<SavedataScreen>(this, &SavedataScreen::OnSearch);
658 #endif
659
660 root_->Add(main);
661 root_->Add(sortStrip);
662 }
663
OnSortClick(UI::EventParams & e)664 UI::EventReturn SavedataScreen::OnSortClick(UI::EventParams &e) {
665 sortOption_ = SavedataSortOption(e.a);
666
667 dataBrowser_->SetSortOption(sortOption_);
668 stateBrowser_->SetSortOption(sortOption_);
669
670 return UI::EVENT_DONE;
671 }
672
OnSearch(UI::EventParams & e)673 UI::EventReturn SavedataScreen::OnSearch(UI::EventParams &e) {
674 auto di = GetI18NCategory("Dialog");
675 #if PPSSPP_PLATFORM(WINDOWS) || defined(USING_QT_UI) || defined(__ANDROID__)
676 System_InputBoxGetString(di->T("Filter"), searchFilter_, [](bool result, const std::string &value) {
677 if (result) {
678 NativeMessageReceived("savedatascreen_search", value.c_str());
679 }
680 });
681 #endif
682 return UI::EVENT_DONE;
683 }
684
OnSavedataButtonClick(UI::EventParams & e)685 UI::EventReturn SavedataScreen::OnSavedataButtonClick(UI::EventParams &e) {
686 std::shared_ptr<GameInfo> ginfo = g_gameInfoCache->GetInfo(screenManager()->getDrawContext(), Path(e.s), 0);
687 SavedataPopupScreen *popupScreen = new SavedataPopupScreen(e.s, ginfo->GetTitle());
688 if (e.v) {
689 popupScreen->SetPopupOrigin(e.v);
690 }
691 screenManager()->push(popupScreen);
692 // the game path: e.s;
693 return UI::EVENT_DONE;
694 }
695
dialogFinished(const Screen * dialog,DialogResult result)696 void SavedataScreen::dialogFinished(const Screen *dialog, DialogResult result) {
697 if (result == DR_NO) {
698 RecreateViews();
699 }
700 }
701
sendMessage(const char * message,const char * value)702 void SavedataScreen::sendMessage(const char *message, const char *value) {
703 UIDialogScreenWithGameBackground::sendMessage(message, value);
704 if (!strcmp(message, "savedatascreen_search")) {
705 searchFilter_ = value;
706 dataBrowser_->SetSearchFilter(searchFilter_);
707 stateBrowser_->SetSearchFilter(searchFilter_);
708 }
709 }
710