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