Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
hrydgard
GitHub Repository: hrydgard/ppsspp
Path: blob/master/UI/MainScreen.cpp
3185 views
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 <cmath>
20
#include <sstream>
21
22
#include "ppsspp_config.h"
23
24
#include "Common/System/Display.h"
25
#include "Common/System/System.h"
26
#include "Common/System/Request.h"
27
#include "Common/System/NativeApp.h"
28
#include "Common/Render/TextureAtlas.h"
29
#include "Common/Render/DrawBuffer.h"
30
#include "Common/UI/Root.h"
31
#include "Common/UI/Context.h"
32
#include "Common/UI/View.h"
33
#include "Common/UI/ViewGroup.h"
34
35
#include "Common/Data/Color/RGBAUtil.h"
36
#include "Common/Data/Encoding/Utf8.h"
37
#include "Common/File/PathBrowser.h"
38
#include "Common/Math/curves.h"
39
#include "Common/Net/URL.h"
40
#include "Common/File/FileUtil.h"
41
#include "Common/TimeUtil.h"
42
#include "Common/StringUtils.h"
43
#include "Common/System/OSD.h"
44
#include "Core/System.h"
45
#include "Core/Util/RecentFiles.h"
46
#include "Core/Reporting.h"
47
#include "Core/HLE/sceCtrl.h"
48
#include "Core/ELF/PBPReader.h"
49
#include "Core/ELF/ParamSFO.h"
50
#include "Core/Util/GameManager.h"
51
52
#include "UI/BackgroundAudio.h"
53
#include "UI/EmuScreen.h"
54
#include "UI/MainScreen.h"
55
#include "UI/GameScreen.h"
56
#include "UI/GameInfoCache.h"
57
#include "UI/GameSettingsScreen.h"
58
#include "UI/MiscScreens.h"
59
#include "UI/ControlMappingScreen.h"
60
#include "UI/IAPScreen.h"
61
#include "UI/RemoteISOScreen.h"
62
#include "UI/DisplayLayoutScreen.h"
63
#include "UI/SavedataScreen.h"
64
#include "UI/Store.h"
65
#include "UI/InstallZipScreen.h"
66
#include "Core/Config.h"
67
#include "Core/Loaders.h"
68
#include "GPU/GPUCommon.h"
69
#include "Common/Data/Text/I18n.h"
70
71
#if PPSSPP_PLATFORM(IOS) || PPSSPP_PLATFORM(MAC)
72
#include "UI/DarwinFileSystemServices.h" // For the browser
73
#endif
74
75
#include "Core/HLE/sceUmd.h"
76
77
bool MainScreen::showHomebrewTab = false;
78
79
static void LaunchFile(ScreenManager *screenManager, Screen *currentScreen, const Path &path) {
80
if (path.GetFileExtension() == ".zip") {
81
// If it's a zip file, we have a screen for that.
82
screenManager->push(new InstallZipScreen(path));
83
} else {
84
if (currentScreen) {
85
screenManager->cancelScreensAbove(currentScreen);
86
}
87
// Otherwise let the EmuScreen take care of it, including error handling.
88
screenManager->switchScreen(new EmuScreen(path));
89
}
90
}
91
92
static bool IsTempPath(const Path &str) {
93
std::string item = str.ToString();
94
95
#ifdef _WIN32
96
// Normalize slashes.
97
item = ReplaceAll(item, "/", "\\");
98
#endif
99
100
std::vector<std::string> tempPaths = System_GetPropertyStringVec(SYSPROP_TEMP_DIRS);
101
for (auto temp : tempPaths) {
102
#ifdef _WIN32
103
temp = ReplaceAll(temp, "/", "\\");
104
if (!temp.empty() && temp[temp.size() - 1] != '\\')
105
temp += "\\";
106
#else
107
if (!temp.empty() && temp[temp.size() - 1] != '/')
108
temp += "/";
109
#endif
110
if (startsWith(item, temp))
111
return true;
112
}
113
114
return false;
115
}
116
117
class GameButton : public UI::Clickable {
118
public:
119
GameButton(const Path &gamePath, bool gridStyle, UI::LayoutParams *layoutParams = nullptr)
120
: UI::Clickable(layoutParams), gridStyle_(gridStyle), gamePath_(gamePath) {}
121
122
void Draw(UIContext &dc) override;
123
std::string DescribeText() const override;
124
void GetContentDimensions(const UIContext &dc, float &w, float &h) const override {
125
if (gridStyle_) {
126
w = 144*g_Config.fGameGridScale;
127
h = 80*g_Config.fGameGridScale;
128
} else {
129
w = 500;
130
h = 50;
131
}
132
}
133
134
const Path &GamePath() const { return gamePath_; }
135
136
void SetHoldEnabled(bool hold) {
137
holdEnabled_ = hold;
138
}
139
bool Touch(const TouchInput &input) override {
140
bool retval = UI::Clickable::Touch(input);
141
hovering_ = bounds_.Contains(input.x, input.y);
142
if (hovering_ && (input.flags & TOUCH_DOWN)) {
143
holdStart_ = time_now_d();
144
}
145
if (input.flags & TOUCH_UP) {
146
holdStart_ = 0;
147
}
148
return retval;
149
}
150
151
bool Key(const KeyInput &key) override {
152
bool showInfo = false;
153
154
if (HasFocus() && UI::IsInfoKey(key)) {
155
// If the button mapped to triangle, then show the info.
156
if (key.flags & KEY_UP) {
157
showInfo = true;
158
}
159
} else if (hovering_ && key.deviceId == DEVICE_ID_MOUSE && key.keyCode == NKCODE_EXT_MOUSEBUTTON_2) {
160
// If it's the right mouse button, and it's not otherwise mapped, show the info also.
161
if (key.flags & KEY_DOWN) {
162
showInfoPressed_ = true;
163
}
164
if ((key.flags & KEY_UP) && showInfoPressed_) {
165
showInfo = true;
166
showInfoPressed_ = false;
167
}
168
}
169
170
if (showInfo) {
171
TriggerOnHoldClick();
172
return true;
173
}
174
175
return Clickable::Key(key);
176
}
177
178
void Update() override {
179
// Hold button for 1.5 seconds to launch the game options
180
if (holdEnabled_ && holdStart_ != 0.0 && holdStart_ < time_now_d() - 1.5) {
181
TriggerOnHoldClick();
182
}
183
}
184
185
void FocusChanged(int focusFlags) override {
186
UI::Clickable::FocusChanged(focusFlags);
187
TriggerOnHighlight(focusFlags);
188
}
189
190
UI::Event OnHoldClick;
191
UI::Event OnHighlight;
192
193
private:
194
void TriggerOnHoldClick() {
195
holdStart_ = 0.0;
196
UI::EventParams e{};
197
e.v = this;
198
e.s = gamePath_.ToString();
199
down_ = false;
200
OnHoldClick.Trigger(e);
201
}
202
void TriggerOnHighlight(int focusFlags) {
203
UI::EventParams e{};
204
e.v = this;
205
e.s = gamePath_.ToString();
206
e.a = focusFlags;
207
OnHighlight.Trigger(e);
208
}
209
210
bool gridStyle_;
211
Path gamePath_;
212
std::string title_;
213
214
double holdStart_ = 0.0;
215
bool holdEnabled_ = true;
216
bool showInfoPressed_ = false;
217
bool hovering_ = false;
218
};
219
220
void GameButton::Draw(UIContext &dc) {
221
std::shared_ptr<GameInfo> ginfo = g_gameInfoCache->GetInfo(dc.GetDrawContext(), gamePath_, GameInfoFlags::PARAM_SFO | GameInfoFlags::ICON);
222
Draw::Texture *texture = nullptr;
223
u32 color = 0, shadowColor = 0;
224
using namespace UI;
225
226
if (ginfo->Ready(GameInfoFlags::ICON) && ginfo->icon.texture) {
227
texture = ginfo->icon.texture;
228
}
229
230
int x = bounds_.x;
231
int y = bounds_.y;
232
int w = gridStyle_ ? bounds_.w : 144;
233
int h = bounds_.h;
234
235
UI::Style style = dc.theme->itemStyle;
236
if (down_)
237
style = dc.theme->itemDownStyle;
238
239
if (!gridStyle_ || !texture) {
240
h = 50;
241
if (HasFocus())
242
style = down_ ? dc.theme->itemDownStyle : dc.theme->itemFocusedStyle;
243
244
Drawable bg = style.background;
245
246
dc.Draw()->Flush();
247
dc.RebindTexture();
248
dc.FillRect(bg, bounds_);
249
dc.Draw()->Flush();
250
}
251
252
if (texture) {
253
color = whiteAlpha(ease((time_now_d() - ginfo->icon.timeLoaded) * 2));
254
shadowColor = blackAlpha(ease((time_now_d() - ginfo->icon.timeLoaded) * 2));
255
float tw = texture->Width();
256
float th = texture->Height();
257
258
// Adjust position so we don't stretch the image vertically or horizontally.
259
// Make sure it's not wider than 144 (like Doom Legacy homebrew), ugly in the grid mode.
260
float nw = std::min(h * tw / th, (float)w);
261
x += (w - nw) / 2.0f;
262
w = nw;
263
}
264
265
int txOffset = down_ ? 4 : 0;
266
if (!gridStyle_) txOffset = 0;
267
268
Bounds overlayBounds = bounds_;
269
u32 overlayColor = 0;
270
if (holdEnabled_ && holdStart_ != 0.0) {
271
double time_held = time_now_d() - holdStart_;
272
overlayColor = whiteAlpha(time_held / 2.5f);
273
}
274
275
// Render button
276
int dropsize = 10;
277
if (texture) {
278
if (!gridStyle_) {
279
x += 4;
280
}
281
if (txOffset) {
282
dropsize = 3;
283
y += txOffset * 2;
284
overlayBounds.y += txOffset * 2;
285
}
286
if (HasFocus()) {
287
dc.Draw()->Flush();
288
dc.RebindTexture();
289
float pulse = sin(time_now_d() * 7.0) * 0.25 + 0.8;
290
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);
291
dc.Draw()->Flush();
292
} else {
293
dc.Draw()->Flush();
294
dc.RebindTexture();
295
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);
296
dc.Draw()->Flush();
297
}
298
299
dc.Draw()->Flush();
300
dc.GetDrawContext()->BindTexture(0, texture);
301
if (holdStart_ != 0.0) {
302
double time_held = time_now_d() - holdStart_;
303
int holdFrameCount = (int)(time_held * 60.0f);
304
if (holdFrameCount > 60) {
305
// Blink before launching by holding
306
if (((holdFrameCount >> 3) & 1) == 0)
307
color = darkenColor(color);
308
}
309
}
310
dc.Draw()->DrawTexRect(x, y, x+w, y+h, 0, 0, 1, 1, color);
311
dc.Draw()->Flush();
312
}
313
314
char discNumInfo[8];
315
if (ginfo->disc_total > 1)
316
snprintf(discNumInfo, sizeof(discNumInfo), "-DISC%d", ginfo->disc_number);
317
else
318
discNumInfo[0] = '\0';
319
320
dc.Draw()->Flush();
321
dc.RebindTexture();
322
dc.SetFontStyle(dc.theme->uiFont);
323
if (gridStyle_ && ginfo->fileType == IdentifiedFileType::PPSSPP_GE_DUMP) {
324
// Super simple drawing for ge dumps.
325
dc.PushScissor(bounds_);
326
const std::string currentTitle = ginfo->GetTitle();
327
dc.SetFontScale(0.6f, 0.6f);
328
dc.DrawText(title_, bounds_.x + 4.0f, bounds_.centerY(), style.fgColor, ALIGN_VCENTER | ALIGN_LEFT);
329
dc.SetFontScale(1.0f, 1.0f);
330
title_ = currentTitle;
331
dc.Draw()->Flush();
332
dc.PopScissor();
333
} else if (!gridStyle_) {
334
float tw, th;
335
dc.Draw()->Flush();
336
dc.PushScissor(bounds_);
337
const std::string currentTitle = ginfo->GetTitle();
338
if (!currentTitle.empty()) {
339
title_ = ReplaceAll(currentTitle, "\n", " ");
340
}
341
342
dc.MeasureText(dc.GetFontStyle(), 1.0f, 1.0f, title_, &tw, &th, 0);
343
344
int availableWidth = bounds_.w - 150;
345
if (g_Config.bShowIDOnGameIcon) {
346
float vw, vh;
347
dc.MeasureText(dc.GetFontStyle(), 0.7f, 0.7f, ginfo->id_version, &vw, &vh, 0);
348
availableWidth -= vw + 20;
349
dc.SetFontScale(0.7f, 0.7f);
350
dc.DrawText(ginfo->id_version, bounds_.x + availableWidth + 160, bounds_.centerY(), style.fgColor, ALIGN_VCENTER);
351
dc.SetFontScale(1.0f, 1.0f);
352
}
353
float sineWidth = std::max(0.0f, (tw - availableWidth)) / 2.0f;
354
355
float tx = 150;
356
if (availableWidth < tw) {
357
tx -= (1.0f + sin(time_now_d() * 1.5f)) * sineWidth;
358
Bounds tb = bounds_;
359
tb.x = bounds_.x + 150;
360
tb.w = availableWidth;
361
dc.PushScissor(tb);
362
}
363
dc.DrawText(title_, bounds_.x + tx, bounds_.centerY(), style.fgColor, ALIGN_VCENTER);
364
if (availableWidth < tw) {
365
dc.PopScissor();
366
}
367
dc.Draw()->Flush();
368
dc.PopScissor();
369
} else if (!texture) {
370
dc.Draw()->Flush();
371
dc.PushScissor(bounds_);
372
dc.DrawText(title_, bounds_.x + 4, bounds_.centerY(), style.fgColor, ALIGN_VCENTER);
373
dc.Draw()->Flush();
374
dc.PopScissor();
375
} else {
376
dc.Draw()->Flush();
377
}
378
if (ginfo->hasConfig && !ginfo->id.empty()) {
379
const AtlasImage *gearImage = dc.Draw()->GetAtlas()->getImage(ImageID("I_GEAR"));
380
if (gearImage) {
381
if (gridStyle_) {
382
dc.Draw()->DrawImage(ImageID("I_GEAR"), x, y + h - gearImage->h*g_Config.fGameGridScale, g_Config.fGameGridScale);
383
} else {
384
dc.Draw()->DrawImage(ImageID("I_GEAR"), x - gearImage->w, y, 1.0f);
385
}
386
}
387
}
388
const int regionIndex = (int)ginfo->region;
389
if (g_Config.bShowRegionOnGameIcon && regionIndex >= 0 && regionIndex < (int)GameRegion::COUNT) {
390
const ImageID regionIcons[(int)GameRegion::COUNT] = {
391
ImageID("I_FLAG_JP"),
392
ImageID("I_FLAG_US"),
393
ImageID("I_FLAG_EU"),
394
ImageID("I_FLAG_HK"),
395
ImageID("I_FLAG_AS"),
396
ImageID("I_FLAG_KO"),
397
};
398
const AtlasImage *image = dc.Draw()->GetAtlas()->getImage(regionIcons[regionIndex]);
399
if (image) {
400
if (gridStyle_) {
401
dc.Draw()->DrawImage(regionIcons[regionIndex], x + w - (image->w + 5)*g_Config.fGameGridScale,
402
y + h - (image->h + 5)*g_Config.fGameGridScale, g_Config.fGameGridScale);
403
} else {
404
dc.Draw()->DrawImage(regionIcons[regionIndex], x - 2 - image->w - 3, y + h - image->h - 5, 1.0f);
405
}
406
}
407
}
408
if (gridStyle_ && g_Config.bShowIDOnGameIcon) {
409
dc.SetFontScale(0.5f*g_Config.fGameGridScale, 0.5f*g_Config.fGameGridScale);
410
dc.DrawText(ginfo->id_version, x+5, y+1, 0xFF000000, ALIGN_TOPLEFT);
411
dc.DrawText(ginfo->id_version, x+4, y, dc.theme->infoStyle.fgColor, ALIGN_TOPLEFT);
412
dc.SetFontScale(1.0f, 1.0f);
413
}
414
if (overlayColor) {
415
dc.FillRect(Drawable(overlayColor), overlayBounds);
416
}
417
dc.RebindTexture();
418
}
419
420
std::string GameButton::DescribeText() const {
421
std::shared_ptr<GameInfo> ginfo = g_gameInfoCache->GetInfo(nullptr, gamePath_, GameInfoFlags::PARAM_SFO);
422
if (!ginfo->Ready(GameInfoFlags::PARAM_SFO))
423
return "...";
424
auto u = GetI18NCategory(I18NCat::UI_ELEMENTS);
425
return ApplySafeSubstitutions(u->T("%1 button"), ginfo->GetTitle());
426
}
427
428
class DirButton : public UI::Button {
429
public:
430
DirButton(const Path &path, bool gridStyle, UI::LayoutParams *layoutParams)
431
: UI::Button(path.ToString(), layoutParams), path_(path), gridStyle_(gridStyle), absolute_(false) {}
432
DirButton(const Path &path, const std::string &text, bool gridStyle, UI::LayoutParams *layoutParams = 0)
433
: UI::Button(text, layoutParams), path_(path), gridStyle_(gridStyle), absolute_(true) {}
434
435
void Draw(UIContext &dc) override;
436
437
const Path &GetPath() const {
438
return path_;
439
}
440
441
bool PathAbsolute() const {
442
return absolute_;
443
}
444
445
private:
446
Path path_;
447
bool gridStyle_;
448
bool absolute_;
449
};
450
451
void DirButton::Draw(UIContext &dc) {
452
using namespace UI;
453
Style style = dc.theme->itemStyle;
454
455
if (HasFocus()) style = dc.theme->itemFocusedStyle;
456
if (down_) style = dc.theme->itemDownStyle;
457
if (!IsEnabled()) style = dc.theme->itemDisabledStyle;
458
459
dc.FillRect(style.background, bounds_);
460
461
std::string_view text(GetText());
462
463
ImageID image = ImageID("I_FOLDER");
464
if (text == "..") {
465
image = ImageID("I_UP_DIRECTORY");
466
}
467
468
float tw, th;
469
dc.MeasureText(dc.GetFontStyle(), gridStyle_ ? g_Config.fGameGridScale : 1.0, gridStyle_ ? g_Config.fGameGridScale : 1.0, text, &tw, &th, 0);
470
471
bool compact = bounds_.w < 180 * (gridStyle_ ? g_Config.fGameGridScale : 1.0);
472
473
if (gridStyle_) {
474
dc.SetFontScale(g_Config.fGameGridScale, g_Config.fGameGridScale);
475
}
476
if (compact) {
477
// No icon, except "up"
478
dc.PushScissor(bounds_);
479
if (image == ImageID("I_FOLDER")) {
480
dc.DrawText(text, bounds_.x + 5, bounds_.centerY(), style.fgColor, ALIGN_VCENTER);
481
} else {
482
dc.Draw()->DrawImage(image, bounds_.centerX(), bounds_.centerY(), gridStyle_ ? g_Config.fGameGridScale : 1.0, style.fgColor, ALIGN_CENTER);
483
}
484
dc.PopScissor();
485
} else {
486
bool scissor = false;
487
if (tw + 150 > bounds_.w) {
488
dc.PushScissor(bounds_);
489
scissor = true;
490
}
491
dc.Draw()->DrawImage(image, bounds_.x + 72, bounds_.centerY(), 0.88f*(gridStyle_ ? g_Config.fGameGridScale : 1.0), style.fgColor, ALIGN_CENTER);
492
dc.DrawText(text, bounds_.x + 150, bounds_.centerY(), style.fgColor, ALIGN_VCENTER);
493
494
if (scissor) {
495
dc.PopScissor();
496
}
497
}
498
if (gridStyle_) {
499
dc.SetFontScale(1.0, 1.0);
500
}
501
}
502
503
GameBrowser::GameBrowser(int token, const Path &path, BrowseFlags browseFlags, bool *gridStyle, ScreenManager *screenManager, std::string_view lastText, std::string_view lastLink, UI::LayoutParams *layoutParams)
504
: LinearLayout(UI::ORIENT_VERTICAL, layoutParams), gridStyle_(gridStyle), browseFlags_(browseFlags), lastText_(lastText), lastLink_(lastLink), screenManager_(screenManager), token_(token) {
505
using namespace UI;
506
path_.SetUserAgent(StringFromFormat("PPSSPP/%s", PPSSPP_GIT_VERSION));
507
Path memstickRoot = GetSysDirectory(DIRECTORY_MEMSTICK_ROOT);
508
if (memstickRoot == GetSysDirectory(DIRECTORY_PSP)) {
509
path_.SetRootAlias("ms:/PSP/", memstickRoot);
510
} else {
511
path_.SetRootAlias("ms:/", memstickRoot);
512
}
513
if (System_GetPropertyBool(SYSPROP_LIMITED_FILE_BROWSING) &&
514
(path.Type() == PathType::NATIVE || path.Type() == PathType::CONTENT_URI)) {
515
// Note: We don't restrict if the path is HTTPS, otherwise remote disc streaming breaks!
516
path_.RestrictToRoot(GetSysDirectory(DIRECTORY_MEMSTICK_ROOT));
517
}
518
path_.SetPath(path);
519
Refresh();
520
}
521
522
void GameBrowser::FocusGame(const Path &gamePath) {
523
focusGamePath_ = gamePath;
524
Refresh();
525
focusGamePath_.clear();
526
}
527
528
void GameBrowser::SetPath(const Path &path) {
529
path_.SetPath(path);
530
g_Config.currentDirectory = path_.GetPath();
531
Refresh();
532
}
533
534
void GameBrowser::ApplySearchFilter(const std::string &filter) {
535
searchFilter_ = filter;
536
std::transform(searchFilter_.begin(), searchFilter_.end(), searchFilter_.begin(), tolower);
537
538
// We don't refresh because game info loads asynchronously anyway.
539
ApplySearchFilter();
540
}
541
542
void GameBrowser::ApplySearchFilter() {
543
if (searchFilter_.empty() && searchStates_.empty()) {
544
// We haven't hidden anything, and we're not searching, so do nothing.
545
searchPending_ = false;
546
return;
547
}
548
549
searchPending_ = false;
550
// By default, everything is matching.
551
searchStates_.resize(gameList_->GetNumSubviews(), SearchState::MATCH);
552
553
if (searchFilter_.empty()) {
554
// Just quickly mark anything we hid as visible again.
555
for (int i = 0; i < gameList_->GetNumSubviews(); ++i) {
556
UI::View *v = gameList_->GetViewByIndex(i);
557
if (searchStates_[i] != SearchState::MATCH)
558
v->SetVisibility(UI::V_VISIBLE);
559
}
560
561
searchStates_.clear();
562
return;
563
}
564
565
for (int i = 0; i < gameList_->GetNumSubviews(); ++i) {
566
UI::View *v = gameList_->GetViewByIndex(i);
567
std::string label = v->DescribeText();
568
// TODO: Maybe we should just save the gameButtons list, though nice to search dirs too?
569
// This is a bit of a hack to recognize a pending game title.
570
if (label == "...") {
571
searchPending_ = true;
572
// Hide anything pending while, we'll pop-in search results as they match.
573
// Note: we leave it at MATCH if gone before, so we don't show it again.
574
if (v->GetVisibility() == UI::V_VISIBLE) {
575
if (searchStates_[i] == SearchState::MATCH)
576
v->SetVisibility(UI::V_GONE);
577
searchStates_[i] = SearchState::PENDING;
578
}
579
continue;
580
}
581
582
std::transform(label.begin(), label.end(), label.begin(), tolower);
583
bool match = v->CanBeFocused() && label.find(searchFilter_) != label.npos;
584
if (match && searchStates_[i] != SearchState::MATCH) {
585
// It was previously visible and force hidden, so show it again.
586
v->SetVisibility(UI::V_VISIBLE);
587
searchStates_[i] = SearchState::MATCH;
588
} else if (!match && searchStates_[i] == SearchState::MATCH && v->GetVisibility() == UI::V_VISIBLE) {
589
v->SetVisibility(UI::V_GONE);
590
searchStates_[i] = SearchState::MISMATCH;
591
}
592
}
593
}
594
595
UI::EventReturn GameBrowser::LayoutChange(UI::EventParams &e) {
596
*gridStyle_ = e.a == 0 ? true : false;
597
Refresh();
598
return UI::EVENT_DONE;
599
}
600
601
UI::EventReturn GameBrowser::LastClick(UI::EventParams &e) {
602
System_LaunchUrl(LaunchUrlType::BROWSER_URL, lastLink_.c_str());
603
return UI::EVENT_DONE;
604
}
605
606
UI::EventReturn GameBrowser::BrowseClick(UI::EventParams &e) {
607
auto mm = GetI18NCategory(I18NCat::MAINMENU);
608
System_BrowseForFolder(token_, mm->T("Choose folder"), path_.GetPath(), [this](const std::string &filename, int) {
609
this->SetPath(Path(filename));
610
});
611
return UI::EVENT_DONE;
612
}
613
614
UI::EventReturn GameBrowser::StorageClick(UI::EventParams &e) {
615
std::vector<std::string> storageDirs = System_GetPropertyStringVec(SYSPROP_ADDITIONAL_STORAGE_DIRS);
616
if (storageDirs.empty()) {
617
// Shouldn't happen - this button shouldn't be clickable.
618
return UI::EVENT_DONE;
619
}
620
if (storageDirs.size() == 1) {
621
SetPath(Path(storageDirs[0]));
622
} else {
623
// TODO: We should popup a dialog letting the user choose one.
624
SetPath(Path(storageDirs[0]));
625
}
626
return UI::EVENT_DONE;
627
}
628
629
UI::EventReturn GameBrowser::OnHomeClick(UI::EventParams &e) {
630
if (path_.GetPath().Type() == PathType::CONTENT_URI) {
631
Path rootPath = path_.GetPath().GetRootVolume();
632
if (rootPath != path_.GetPath()) {
633
SetPath(rootPath);
634
return UI::EVENT_DONE;
635
}
636
if (System_GetPropertyBool(SYSPROP_ANDROID_SCOPED_STORAGE)) {
637
// There'll be no sensible home, ignore.
638
return UI::EVENT_DONE;
639
}
640
}
641
642
SetPath(HomePath());
643
return UI::EVENT_DONE;
644
}
645
646
// TODO: This doesn't make that much sense for Android, especially after scoped storage..
647
// Maybe we should have no home directory in this case. Or it should just navigate to the root
648
// of the current folder tree.
649
Path GameBrowser::HomePath() {
650
if (!homePath_.empty()) {
651
return homePath_;
652
}
653
#if PPSSPP_PLATFORM(ANDROID) || PPSSPP_PLATFORM(SWITCH) || defined(USING_WIN_UI) || PPSSPP_PLATFORM(UWP) || PPSSPP_PLATFORM(IOS)
654
return g_Config.memStickDirectory;
655
#else
656
return Path(getenv("HOME"));
657
#endif
658
}
659
660
UI::EventReturn GameBrowser::PinToggleClick(UI::EventParams &e) {
661
auto &pinnedPaths = g_Config.vPinnedPaths;
662
const std::string path = File::ResolvePath(path_.GetPath().ToString());
663
if (IsCurrentPathPinned()) {
664
pinnedPaths.erase(std::remove(pinnedPaths.begin(), pinnedPaths.end(), path), pinnedPaths.end());
665
} else {
666
pinnedPaths.push_back(path);
667
}
668
Refresh();
669
return UI::EVENT_DONE;
670
}
671
672
bool GameBrowser::DisplayTopBar() {
673
return path_.GetPath().ToString() != "!RECENT";
674
}
675
676
bool GameBrowser::HasSpecialFiles(std::vector<Path> &filenames) {
677
if (path_.GetPath().ToString() == "!RECENT") {
678
filenames.clear();
679
for (auto &str : g_recentFiles.GetRecentFiles()) {
680
filenames.emplace_back(str);
681
}
682
return true;
683
}
684
return false;
685
}
686
687
void GameBrowser::Update() {
688
LinearLayout::Update();
689
if (refreshPending_) {
690
path_.Refresh();
691
}
692
if ((listingPending_ && path_.IsListingReady()) || refreshPending_) {
693
Refresh();
694
refreshPending_ = false;
695
}
696
if (searchPending_) {
697
ApplySearchFilter();
698
}
699
}
700
701
void GameBrowser::Draw(UIContext &dc) {
702
using namespace UI;
703
704
if (lastScale_ != g_Config.fGameGridScale || lastLayoutWasGrid_ != *gridStyle_) {
705
Refresh();
706
}
707
708
if (hasDropShadow_) {
709
// Darken things behind.
710
dc.FillRect(UI::Drawable(0x60000000), dc.GetBounds().Expand(dropShadowExpand_));
711
float dropsize = 30.0f;
712
dc.Draw()->DrawImage4Grid(dc.theme->dropShadow4Grid,
713
bounds_.x - dropsize, bounds_.y,
714
bounds_.x2() + dropsize, bounds_.y2()+dropsize*1.5f, 0xDF000000, 3.0f);
715
}
716
717
if (clip_) {
718
dc.PushScissor(bounds_);
719
}
720
721
dc.FillRect(bg_, bounds_);
722
for (View *view : views_) {
723
if (view->GetVisibility() == V_VISIBLE) {
724
// Check if bounds are in current scissor rectangle.
725
if (dc.GetScissorBounds().Intersects(dc.TransformBounds(view->GetBounds())))
726
view->Draw(dc);
727
}
728
}
729
if (clip_) {
730
dc.PopScissor();
731
}
732
}
733
734
static bool IsValidPBP(const Path &path, bool allowHomebrew) {
735
if (!File::Exists(path))
736
return false;
737
738
std::unique_ptr<FileLoader> loader(ConstructFileLoader(path));
739
PBPReader pbp(loader.get());
740
std::vector<u8> sfoData;
741
if (!pbp.GetSubFile(PBP_PARAM_SFO, &sfoData))
742
return false;
743
744
ParamSFOData sfo;
745
sfo.ReadSFO(sfoData);
746
if (!allowHomebrew && sfo.GetValueString("DISC_ID").empty())
747
return false;
748
749
if (sfo.GetValueString("CATEGORY") == "ME")
750
return false;
751
752
return true;
753
}
754
755
void GameBrowser::Refresh() {
756
using namespace UI;
757
758
lastScale_ = g_Config.fGameGridScale;
759
lastLayoutWasGrid_ = *gridStyle_;
760
761
// Kill all the contents
762
Clear();
763
searchStates_.clear();
764
765
Add(new Spacer(1.0f));
766
auto mm = GetI18NCategory(I18NCat::MAINMENU);
767
768
// No topbar on recent screen
769
gameList_ = nullptr;
770
if (DisplayTopBar()) {
771
LinearLayout *topBar = new LinearLayout(ORIENT_HORIZONTAL, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT));
772
if (browseFlags_ & BrowseFlags::NAVIGATE) {
773
topBar->Add(new Spacer(2.0f));
774
topBar->Add(new TextView(path_.GetFriendlyPath(), ALIGN_VCENTER | FLAG_WRAP_TEXT, true, new LinearLayoutParams(FILL_PARENT, 64.0f, 1.0f)));
775
topBar->Add(new Choice(ImageID("I_HOME"), new LayoutParams(WRAP_CONTENT, 64.0f)))->OnClick.Handle(this, &GameBrowser::OnHomeClick);
776
if (System_GetPropertyBool(SYSPROP_HAS_ADDITIONAL_STORAGE)) {
777
topBar->Add(new Choice(ImageID("I_SDCARD"), new LayoutParams(WRAP_CONTENT, 64.0f)))->OnClick.Handle(this, &GameBrowser::StorageClick);
778
}
779
#if PPSSPP_PLATFORM(IOS_APP_STORE)
780
// Don't show a browse button, not meaningful to browse outside the documents folder it seems,
781
// as we can't list things like document folders of another app, as far as I can tell.
782
// However, we do show a Load.. button for picking individual files, that seems to work.
783
#elif PPSSPP_PLATFORM(IOS) || PPSSPP_PLATFORM(MAC)
784
// on Darwin, we don't show the 'Browse' text alongside the image
785
// we show just the image, because we don't need to emphasize the button on Darwin
786
topBar->Add(new Choice(ImageID("I_FOLDER_OPEN"), new LayoutParams(WRAP_CONTENT, 64.0f)))->OnClick.Handle(this, &GameBrowser::BrowseClick);
787
#else
788
if ((browseFlags_ & BrowseFlags::BROWSE) && System_GetPropertyBool(SYSPROP_HAS_FOLDER_BROWSER)) {
789
// Collapse the button title on very small screens (Retroid Pocket).
790
std::string_view browseTitle = g_display.pixel_xres <= 640 ? "" : mm->T("Browse");
791
topBar->Add(new Choice(browseTitle, ImageID("I_FOLDER_OPEN"), new LayoutParams(WRAP_CONTENT, 64.0f)))->OnClick.Handle(this, &GameBrowser::BrowseClick);
792
}
793
if (System_GetPropertyInt(SYSPROP_DEVICE_TYPE) == DEVICE_TYPE_TV) {
794
topBar->Add(new Choice(mm->T("Enter Path"), new LayoutParams(WRAP_CONTENT, 64.0f)))->OnClick.Add([=](UI::EventParams &) {
795
auto mm = GetI18NCategory(I18NCat::MAINMENU);
796
System_InputBoxGetString(token_, mm->T("Enter Path"), path_.GetPath().ToString(), false, [=](const char *responseString, int responseValue) {
797
this->SetPath(Path(responseString));
798
});
799
return UI::EVENT_DONE;
800
});
801
}
802
#endif
803
} else {
804
topBar->Add(new Spacer(new LinearLayoutParams(FILL_PARENT, 64.0f, 1.0f)));
805
}
806
807
if (browseFlags_ & BrowseFlags::HOMEBREW_STORE) {
808
topBar->Add(new Choice(mm->T("PPSSPP Homebrew Store"), new UI::LinearLayoutParams(WRAP_CONTENT, 64.0f)))->OnClick.Handle(this, &GameBrowser::OnHomebrewStore);
809
}
810
811
ChoiceStrip *layoutChoice = topBar->Add(new ChoiceStrip(ORIENT_HORIZONTAL));
812
layoutChoice->AddChoice(ImageID("I_GRID"));
813
layoutChoice->AddChoice(ImageID("I_LINES"));
814
layoutChoice->SetSelection(*gridStyle_ ? 0 : 1, false);
815
layoutChoice->OnChoice.Handle(this, &GameBrowser::LayoutChange);
816
topBar->Add(new Choice(ImageID("I_ROTATE_LEFT"), new LayoutParams(64.0f, 64.0f)))->OnClick.Add([=](UI::EventParams &e) {
817
path_.Refresh();
818
Refresh();
819
return UI::EVENT_DONE;
820
});
821
topBar->Add(new Choice(ImageID("I_GEAR"), new LayoutParams(64.0f, 64.0f)))->OnClick.Handle(this, &GameBrowser::GridSettingsClick);
822
Add(topBar);
823
824
if (*gridStyle_) {
825
gameList_ = new UI::GridLayoutList(UI::GridLayoutSettings(150*g_Config.fGameGridScale, 85*g_Config.fGameGridScale), new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT));
826
} else {
827
UI::LinearLayout *gl = new UI::LinearLayoutList(UI::ORIENT_VERTICAL, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT));
828
gl->SetSpacing(4.0f);
829
gameList_ = gl;
830
}
831
} else {
832
if (*gridStyle_) {
833
gameList_ = new UI::GridLayoutList(UI::GridLayoutSettings(150*g_Config.fGameGridScale, 85*g_Config.fGameGridScale), new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT));
834
} else {
835
UI::LinearLayout *gl = new UI::LinearLayout(UI::ORIENT_VERTICAL, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT));
836
gl->SetSpacing(4.0f);
837
gameList_ = gl;
838
}
839
// Until we can come up with a better space to put it (next to the tabs?) let's get rid of the icon config
840
// button on the Recent tab, it's ugly. You can use the button from the other tabs.
841
842
// LinearLayout *gridOptionColumn = new LinearLayout(ORIENT_VERTICAL, new LinearLayoutParams(64.0, 64.0f));
843
// gridOptionColumn->Add(new Spacer(12.0));
844
// gridOptionColumn->Add(new Choice(ImageID("I_GEAR"), new LayoutParams(64.0f, 64.0f)))->OnClick.Handle(this, &GameBrowser::GridSettingsClick);
845
// LinearLayout *grid = new LinearLayout(ORIENT_HORIZONTAL, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT));
846
// gameList_->ReplaceLayoutParams(new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT, 0.75));
847
// grid->Add(gameList_);
848
// grid->Add(gridOptionColumn);
849
// Add(grid);
850
}
851
Add(gameList_);
852
853
// Find games in the current directory and create new ones.
854
std::vector<DirButton *> dirButtons;
855
std::vector<GameButton *> gameButtons;
856
857
listingPending_ = !path_.IsListingReady();
858
859
// TODO: If listing failed, show a special error message.
860
861
std::vector<Path> filenames;
862
if (HasSpecialFiles(filenames)) {
863
for (size_t i = 0; i < filenames.size(); i++) {
864
gameButtons.push_back(new GameButton(filenames[i], *gridStyle_, new UI::LinearLayoutParams(*gridStyle_ == true ? UI::WRAP_CONTENT : UI::FILL_PARENT, UI::WRAP_CONTENT)));
865
}
866
} else if (!listingPending_) {
867
std::vector<File::FileInfo> fileInfo;
868
path_.GetListing(fileInfo, "iso:cso:chd:pbp:elf:prx:ppdmp:");
869
for (size_t i = 0; i < fileInfo.size(); i++) {
870
bool isGame = !fileInfo[i].isDirectory;
871
bool isSaveData = false;
872
// Check if eboot directory
873
if (!isGame && path_.GetPath().size() >= 4 && IsValidPBP(path_.GetPath() / fileInfo[i].name / "EBOOT.PBP", true))
874
isGame = true;
875
else if (!isGame && File::Exists(path_.GetPath() / fileInfo[i].name / "PSP_GAME/SYSDIR"))
876
isGame = true;
877
else if (!isGame && File::Exists(path_.GetPath() / fileInfo[i].name / "PARAM.SFO"))
878
isSaveData = true;
879
880
if (!isGame && !isSaveData) {
881
if (browseFlags_ & BrowseFlags::NAVIGATE) {
882
dirButtons.push_back(new DirButton(fileInfo[i].fullName, fileInfo[i].name, *gridStyle_, new UI::LinearLayoutParams(UI::FILL_PARENT, UI::FILL_PARENT)));
883
}
884
} else {
885
gameButtons.push_back(new GameButton(fileInfo[i].fullName, *gridStyle_, new UI::LinearLayoutParams(*gridStyle_ == true ? UI::WRAP_CONTENT : UI::FILL_PARENT, UI::WRAP_CONTENT)));
886
}
887
}
888
// Put RAR/ZIP files at the end to get them out of the way. They're only shown so that people
889
// can click them and get an explanation that they need to unpack them. This is necessary due
890
// to a flood of support email...
891
if (browseFlags_ & BrowseFlags::ARCHIVES) {
892
fileInfo.clear();
893
path_.GetListing(fileInfo, "zip:rar:r01:7z:");
894
if (!fileInfo.empty()) {
895
UI::LinearLayout *zl = new UI::LinearLayoutList(UI::ORIENT_VERTICAL, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT));
896
zl->SetSpacing(4.0f);
897
Add(zl);
898
for (size_t i = 0; i < fileInfo.size(); i++) {
899
if (!fileInfo[i].isDirectory) {
900
GameButton *b = zl->Add(new GameButton(fileInfo[i].fullName, false, new UI::LinearLayoutParams(UI::FILL_PARENT, UI::WRAP_CONTENT)));
901
b->OnClick.Handle(this, &GameBrowser::GameButtonClick);
902
b->SetHoldEnabled(false);
903
}
904
}
905
}
906
}
907
}
908
909
if (browseFlags_ & BrowseFlags::NAVIGATE) {
910
if (path_.CanNavigateUp()) {
911
gameList_->Add(new DirButton(Path(std::string("..")), *gridStyle_, new UI::LinearLayoutParams(UI::FILL_PARENT, UI::FILL_PARENT)))->
912
OnClick.Handle(this, &GameBrowser::NavigateClick);
913
}
914
915
// Add any pinned paths before other directories.
916
auto pinnedPaths = GetPinnedPaths();
917
for (auto it = pinnedPaths.begin(), end = pinnedPaths.end(); it != end; ++it) {
918
gameList_->Add(new DirButton(*it, GetBaseName((*it).ToString()), *gridStyle_, new UI::LinearLayoutParams(UI::FILL_PARENT, UI::FILL_PARENT)))->
919
OnClick.Handle(this, &GameBrowser::NavigateClick);
920
}
921
}
922
923
if (listingPending_) {
924
gameList_->Add(new UI::TextView(mm->T("Loading..."), ALIGN_CENTER, false, new UI::LinearLayoutParams(UI::FILL_PARENT, UI::FILL_PARENT)));
925
}
926
927
for (size_t i = 0; i < dirButtons.size(); i++) {
928
gameList_->Add(dirButtons[i])->OnClick.Handle(this, &GameBrowser::NavigateClick);
929
}
930
931
for (size_t i = 0; i < gameButtons.size(); i++) {
932
GameButton *b = gameList_->Add(gameButtons[i]);
933
b->OnClick.Handle(this, &GameBrowser::GameButtonClick);
934
b->OnHoldClick.Handle(this, &GameBrowser::GameButtonHoldClick);
935
b->OnHighlight.Handle(this, &GameBrowser::GameButtonHighlight);
936
937
if (!focusGamePath_.empty() && b->GamePath() == focusGamePath_) {
938
b->SetFocus();
939
}
940
}
941
942
// Show a button to toggle pinning at the very end.
943
if ((browseFlags_ & BrowseFlags::PIN) && !path_.GetPath().empty()) {
944
std::string caption = IsCurrentPathPinned() ? "-" : "+";
945
if (!*gridStyle_) {
946
caption = IsCurrentPathPinned() ? mm->T("UnpinPath", "Unpin") : mm->T("PinPath", "Pin");
947
}
948
gameList_->Add(new UI::Button(caption, new UI::LinearLayoutParams(UI::FILL_PARENT, UI::FILL_PARENT)))->
949
OnClick.Handle(this, &GameBrowser::PinToggleClick);
950
}
951
952
if (path_.GetPath().empty()) {
953
Add(new TextView(mm->T("UseBrowseOrLoad", "Use Browse to choose a folder, or Load to choose a file.")));
954
}
955
956
if (!lastText_.empty()) {
957
Add(new Spacer());
958
Add(new Choice(lastText_, new UI::LinearLayoutParams(UI::WRAP_CONTENT, UI::WRAP_CONTENT)))->OnClick.Handle(this, &GameBrowser::LastClick);
959
}
960
}
961
962
bool GameBrowser::IsCurrentPathPinned() {
963
const auto &paths = g_Config.vPinnedPaths;
964
if (paths.empty()) {
965
return false;
966
}
967
std::string resolved = File::ResolvePath(path_.GetPath().ToString());
968
return std::find(paths.begin(), paths.end(), resolved) != paths.end();
969
}
970
971
std::vector<Path> GameBrowser::GetPinnedPaths() const {
972
#ifndef _WIN32
973
static const std::string sepChars = "/";
974
#else
975
static const std::string sepChars = "/\\";
976
#endif
977
if (g_Config.vPinnedPaths.empty()) {
978
// Early-out.
979
return std::vector<Path>();
980
}
981
982
const std::string currentPath = File::ResolvePath(path_.GetPath().ToString());
983
const std::vector<std::string> paths = g_Config.vPinnedPaths;
984
std::vector<Path> results;
985
for (size_t i = 0; i < paths.size(); ++i) {
986
// We want to exclude the current path, and its direct children.
987
if (paths[i] == currentPath) {
988
continue;
989
}
990
if (startsWith(paths[i], currentPath)) {
991
std::string descendant = paths[i].substr(currentPath.size());
992
// If there's only one separator (or none), its a direct child.
993
if (descendant.find_last_of(sepChars) == descendant.find_first_of(sepChars)) {
994
continue;
995
}
996
}
997
998
results.push_back(Path(paths[i]));
999
}
1000
return results;
1001
}
1002
1003
std::string GameBrowser::GetBaseName(const std::string &path) const {
1004
#ifndef _WIN32
1005
static const std::string sepChars = "/";
1006
#else
1007
static const std::string sepChars = "/\\";
1008
#endif
1009
1010
auto trailing = path.find_last_not_of(sepChars);
1011
if (trailing != path.npos) {
1012
size_t start = path.find_last_of(sepChars, trailing);
1013
if (start != path.npos) {
1014
return path.substr(start + 1, trailing - start);
1015
}
1016
return path.substr(0, trailing);
1017
}
1018
1019
size_t start = path.find_last_of(sepChars);
1020
if (start != path.npos) {
1021
return path.substr(start + 1);
1022
}
1023
return path;
1024
}
1025
1026
UI::EventReturn GameBrowser::GameButtonClick(UI::EventParams &e) {
1027
GameButton *button = static_cast<GameButton *>(e.v);
1028
UI::EventParams e2{};
1029
e2.s = button->GamePath().ToString();
1030
// Insta-update - here we know we are already on the right thread.
1031
OnChoice.Trigger(e2);
1032
return UI::EVENT_DONE;
1033
}
1034
1035
UI::EventReturn GameBrowser::GameButtonHoldClick(UI::EventParams &e) {
1036
GameButton *button = static_cast<GameButton *>(e.v);
1037
UI::EventParams e2{};
1038
e2.s = button->GamePath().ToString();
1039
// Insta-update - here we know we are already on the right thread.
1040
OnHoldChoice.Trigger(e2);
1041
return UI::EVENT_DONE;
1042
}
1043
1044
UI::EventReturn GameBrowser::GameButtonHighlight(UI::EventParams &e) {
1045
// Insta-update - here we know we are already on the right thread.
1046
OnHighlight.Trigger(e);
1047
return UI::EVENT_DONE;
1048
}
1049
1050
UI::EventReturn GameBrowser::NavigateClick(UI::EventParams &e) {
1051
DirButton *button = static_cast<DirButton *>(e.v);
1052
Path text = button->GetPath();
1053
if (button->PathAbsolute()) {
1054
path_.SetPath(text);
1055
} else {
1056
path_.Navigate(text.ToString());
1057
}
1058
g_Config.currentDirectory = path_.GetPath();
1059
Refresh();
1060
return UI::EVENT_DONE;
1061
}
1062
1063
UI::EventReturn GameBrowser::GridSettingsClick(UI::EventParams &e) {
1064
auto sy = GetI18NCategory(I18NCat::SYSTEM);
1065
auto gridSettings = new GridSettingsPopupScreen(sy->T("Games list settings"));
1066
gridSettings->OnRecentChanged.Handle(this, &GameBrowser::OnRecentClear);
1067
if (e.v)
1068
gridSettings->SetPopupOrigin(e.v);
1069
1070
screenManager_->push(gridSettings);
1071
return UI::EVENT_DONE;
1072
}
1073
1074
UI::EventReturn GameBrowser::OnRecentClear(UI::EventParams &e) {
1075
screenManager_->RecreateAllViews();
1076
System_Notify(SystemNotification::UI);
1077
return UI::EVENT_DONE;
1078
}
1079
1080
UI::EventReturn GameBrowser::OnHomebrewStore(UI::EventParams &e) {
1081
screenManager_->push(new StoreScreen());
1082
return UI::EVENT_DONE;
1083
}
1084
1085
MainScreen::MainScreen() {
1086
g_BackgroundAudio.SetGame(Path());
1087
}
1088
1089
MainScreen::~MainScreen() {
1090
g_BackgroundAudio.SetGame(Path());
1091
}
1092
1093
void MainScreen::CreateViews() {
1094
// Information in the top left.
1095
// Back button to the bottom left.
1096
// Scrolling action menu to the right.
1097
using namespace UI;
1098
1099
const bool vertical = UseVerticalLayout();
1100
1101
auto mm = GetI18NCategory(I18NCat::MAINMENU);
1102
1103
tabHolder_ = new TabHolder(ORIENT_HORIZONTAL, 64, nullptr, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT, 1.0f));
1104
ViewGroup *leftColumn = tabHolder_;
1105
tabHolder_->SetTag("MainScreenGames");
1106
gameBrowsers_.clear();
1107
1108
tabHolder_->SetClip(true);
1109
1110
bool showRecent = g_Config.iMaxRecent > 0;
1111
bool hasStorageAccess = !System_GetPropertyBool(SYSPROP_SUPPORTS_PERMISSIONS) ||
1112
System_GetPermissionStatus(SYSTEM_PERMISSION_STORAGE) == PERMISSION_STATUS_GRANTED;
1113
bool storageIsTemporary = IsTempPath(GetSysDirectory(DIRECTORY_SAVEDATA)) && !confirmedTemporary_;
1114
if (showRecent && !hasStorageAccess) {
1115
showRecent = g_recentFiles.HasAny();
1116
}
1117
1118
if (showRecent) {
1119
ScrollView *scrollRecentGames = new ScrollView(ORIENT_VERTICAL, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT));
1120
scrollRecentGames->SetTag("MainScreenRecentGames");
1121
GameBrowser *tabRecentGames = new GameBrowser(GetRequesterToken(),
1122
Path("!RECENT"), BrowseFlags::NONE, &g_Config.bGridView1, screenManager(), "", "",
1123
new LinearLayoutParams(FILL_PARENT, FILL_PARENT));
1124
scrollRecentGames->Add(tabRecentGames);
1125
gameBrowsers_.push_back(tabRecentGames);
1126
1127
tabHolder_->AddTab(mm->T("Recent"), scrollRecentGames);
1128
tabRecentGames->OnChoice.Handle(this, &MainScreen::OnGameSelectedInstant);
1129
tabRecentGames->OnHoldChoice.Handle(this, &MainScreen::OnGameSelected);
1130
tabRecentGames->OnHighlight.Handle(this, &MainScreen::OnGameHighlight);
1131
}
1132
1133
Button *focusButton = nullptr;
1134
if (hasStorageAccess) {
1135
scrollAllGames_ = new ScrollView(ORIENT_VERTICAL, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT));
1136
scrollAllGames_->SetTag("MainScreenAllGames");
1137
ScrollView *scrollHomebrew = new ScrollView(ORIENT_VERTICAL, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT));
1138
scrollHomebrew->SetTag("MainScreenHomebrew");
1139
1140
#if PPSSPP_PLATFORM(IOS)
1141
std::string_view getGamesUri = "https://www.ppsspp.org/getgames_ios";
1142
std::string_view getHomebrewUri = "https://www.ppsspp.org/gethomebrew_ios";
1143
#else
1144
std::string_view getGamesUri = "https://www.ppsspp.org/getgames";
1145
std::string_view getHomebrewUri = "https://www.ppsspp.org/gethomebrew";
1146
#endif
1147
1148
GameBrowser *tabAllGames = new GameBrowser(GetRequesterToken(), Path(g_Config.currentDirectory), BrowseFlags::STANDARD, &g_Config.bGridView2, screenManager(),
1149
mm->T("How to get games"), getGamesUri,
1150
new LinearLayoutParams(FILL_PARENT, FILL_PARENT));
1151
GameBrowser *tabHomebrew = new GameBrowser(GetRequesterToken(), GetSysDirectory(DIRECTORY_GAME), BrowseFlags::HOMEBREW_STORE, &g_Config.bGridView3, screenManager(),
1152
mm->T("How to get homebrew & demos", "How to get homebrew && demos"), getHomebrewUri,
1153
new LinearLayoutParams(FILL_PARENT, FILL_PARENT));
1154
1155
scrollAllGames_->Add(tabAllGames);
1156
gameBrowsers_.push_back(tabAllGames);
1157
scrollHomebrew->Add(tabHomebrew);
1158
gameBrowsers_.push_back(tabHomebrew);
1159
1160
tabHolder_->AddTab(mm->T("Games"), scrollAllGames_);
1161
tabHolder_->AddTab(mm->T("Homebrew & Demos"), scrollHomebrew);
1162
scrollAllGames_->RememberPosition(&g_Config.fGameListScrollPosition);
1163
1164
tabAllGames->OnChoice.Handle(this, &MainScreen::OnGameSelectedInstant);
1165
tabHomebrew->OnChoice.Handle(this, &MainScreen::OnGameSelectedInstant);
1166
1167
tabAllGames->OnHoldChoice.Handle(this, &MainScreen::OnGameSelected);
1168
tabHomebrew->OnHoldChoice.Handle(this, &MainScreen::OnGameSelected);
1169
1170
tabAllGames->OnHighlight.Handle(this, &MainScreen::OnGameHighlight);
1171
tabHomebrew->OnHighlight.Handle(this, &MainScreen::OnGameHighlight);
1172
1173
if (g_Config.bRemoteTab && !g_Config.sLastRemoteISOServer.empty()) {
1174
auto ri = GetI18NCategory(I18NCat::REMOTEISO);
1175
1176
ScrollView *scrollRemote = new ScrollView(ORIENT_VERTICAL, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT));
1177
scrollRemote->SetTag("MainScreenRemote");
1178
1179
Path remotePath(FormatRemoteISOUrl(g_Config.sLastRemoteISOServer.c_str(), g_Config.iLastRemoteISOPort, RemoteSubdir().c_str()));
1180
1181
GameBrowser *tabRemote = new GameBrowser(GetRequesterToken(), remotePath, BrowseFlags::NAVIGATE, &g_Config.bGridView3, screenManager(),
1182
ri->T("Remote disc streaming"), "https://www.ppsspp.org/docs/reference/disc-streaming",
1183
new LinearLayoutParams(FILL_PARENT, FILL_PARENT));
1184
tabRemote->SetHomePath(remotePath);
1185
1186
scrollRemote->Add(tabRemote);
1187
gameBrowsers_.push_back(tabRemote);
1188
1189
tabHolder_->AddTab(ri->T("Remote disc streaming"), scrollRemote);
1190
1191
tabRemote->OnChoice.Handle(this, &MainScreen::OnGameSelectedInstant);
1192
tabRemote->OnHoldChoice.Handle(this, &MainScreen::OnGameSelected);
1193
tabRemote->OnHighlight.Handle(this, &MainScreen::OnGameHighlight);
1194
}
1195
1196
if (g_recentFiles.HasAny()) {
1197
tabHolder_->SetCurrentTab(std::clamp(g_Config.iDefaultTab, 0, g_Config.bRemoteTab ? 3 : 2), true);
1198
} else if (g_Config.iMaxRecent > 0) {
1199
tabHolder_->SetCurrentTab(1, true);
1200
}
1201
1202
if (backFromStore_ || showHomebrewTab) {
1203
tabHolder_->SetCurrentTab(2, true);
1204
backFromStore_ = false;
1205
showHomebrewTab = false;
1206
}
1207
1208
if (storageIsTemporary) {
1209
LinearLayout *buttonHolder = new LinearLayout(ORIENT_HORIZONTAL, new LinearLayoutParams(WRAP_CONTENT, WRAP_CONTENT));
1210
buttonHolder->Add(new Spacer(new LinearLayoutParams(1.0f)));
1211
focusButton = new Button(mm->T("SavesAreTemporaryIgnore", "Ignore warning"), new LinearLayoutParams(WRAP_CONTENT, WRAP_CONTENT));
1212
focusButton->SetPadding(32, 16);
1213
buttonHolder->Add(focusButton)->OnClick.Add([this](UI::EventParams &e) {
1214
confirmedTemporary_ = true;
1215
RecreateViews();
1216
return UI::EVENT_DONE;
1217
});
1218
buttonHolder->Add(new Spacer(new LinearLayoutParams(1.0f)));
1219
1220
leftColumn->Add(new Spacer(new LinearLayoutParams(0.1f)));
1221
leftColumn->Add(new TextView(mm->T("SavesAreTemporary", "PPSSPP saving in temporary storage"), ALIGN_HCENTER, false));
1222
leftColumn->Add(new TextView(mm->T("SavesAreTemporaryGuidance", "Extract PPSSPP somewhere to save permanently"), ALIGN_HCENTER, false));
1223
leftColumn->Add(new Spacer(10.0f));
1224
leftColumn->Add(buttonHolder);
1225
leftColumn->Add(new Spacer(new LinearLayoutParams(0.1f)));
1226
}
1227
} else {
1228
scrollAllGames_ = nullptr;
1229
if (!showRecent) {
1230
leftColumn = new LinearLayout(ORIENT_VERTICAL, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT, 1.0f));
1231
// Just so it's destroyed on recreate.
1232
leftColumn->Add(tabHolder_);
1233
tabHolder_->SetVisibility(V_GONE);
1234
}
1235
1236
LinearLayout *buttonHolder = new LinearLayout(ORIENT_HORIZONTAL, new LinearLayoutParams(WRAP_CONTENT, WRAP_CONTENT));
1237
buttonHolder->Add(new Spacer(new LinearLayoutParams(1.0f)));
1238
focusButton = new Button(mm->T("Give PPSSPP permission to access storage"), new LinearLayoutParams(WRAP_CONTENT, WRAP_CONTENT));
1239
focusButton->SetPadding(32, 16);
1240
buttonHolder->Add(focusButton)->OnClick.Handle(this, &MainScreen::OnAllowStorage);
1241
buttonHolder->Add(new Spacer(new LinearLayoutParams(1.0f)));
1242
1243
leftColumn->Add(new Spacer(new LinearLayoutParams(0.1f)));
1244
leftColumn->Add(buttonHolder);
1245
leftColumn->Add(new Spacer(10.0f));
1246
leftColumn->Add(new TextView(mm->T("PPSSPP can't load games or save right now"), ALIGN_HCENTER, false));
1247
leftColumn->Add(new Spacer(new LinearLayoutParams(0.1f)));
1248
}
1249
1250
ViewGroup *rightColumn = new ScrollView(ORIENT_VERTICAL);
1251
LinearLayout *rightColumnItems = new LinearLayout(ORIENT_VERTICAL, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT));
1252
rightColumnItems->SetSpacing(0.0f);
1253
rightColumn->Add(rightColumnItems);
1254
1255
std::string versionString = PPSSPP_GIT_VERSION;
1256
// Strip the 'v' from the displayed version, and shorten the commit hash.
1257
if (versionString.size() > 2) {
1258
if (versionString[0] == 'v' && isdigit(versionString[1])) {
1259
versionString = versionString.substr(1);
1260
}
1261
if (CountChar(versionString, '-') == 2) {
1262
// Shorten the commit hash.
1263
size_t cutPos = versionString.find_last_of('-') + 8;
1264
versionString = versionString.substr(0, std::min(cutPos, versionString.size()));
1265
}
1266
}
1267
1268
rightColumnItems->SetSpacing(0.0f);
1269
AnchorLayout *logos = new AnchorLayout(new AnchorLayoutParams(FILL_PARENT, 60.0f, false));
1270
if (System_GetPropertyBool(SYSPROP_APP_GOLD)) {
1271
logos->Add(new ImageView(ImageID("I_ICONGOLD"), "", IS_DEFAULT, new AnchorLayoutParams(64, 64, 0, 0, NONE, NONE, false)));
1272
} else {
1273
logos->Add(new ImageView(ImageID("I_ICON"), "", IS_DEFAULT, new AnchorLayoutParams(64, 64, 0, 0, NONE, NONE, false)));
1274
}
1275
logos->Add(new ImageView(ImageID("I_LOGO"), "PPSSPP", IS_DEFAULT, new AnchorLayoutParams(180, 64, 64, -5.0f, NONE, NONE, false)));
1276
1277
#if !defined(MOBILE_DEVICE)
1278
auto gr = GetI18NCategory(I18NCat::GRAPHICS);
1279
ImageID icon(g_Config.UseFullScreen() ? "I_RESTORE" : "I_FULLSCREEN");
1280
fullscreenButton_ = logos->Add(new Button(gr->T("FullScreen", "Full Screen"), icon, new AnchorLayoutParams(48, 48, NONE, 0, 0, NONE, false)));
1281
fullscreenButton_->SetIgnoreText(true);
1282
fullscreenButton_->OnClick.Handle(this, &MainScreen::OnFullScreenToggle);
1283
#endif
1284
1285
rightColumnItems->Add(logos);
1286
ClickableTextView *ver = rightColumnItems->Add(new ClickableTextView(versionString, new LinearLayoutParams(Margins(70, -10, 0, 4))));
1287
ver->SetSmall(true);
1288
ver->SetClip(false);
1289
1290
// Only allow copying the version if it looks like a git version string. 1.19 for example is not really necessary to be able to copy/paste.
1291
if (strchr(PPSSPP_GIT_VERSION, '-')) {
1292
ver->OnClick.Add([](UI::EventParams &e) {
1293
auto di = GetI18NCategory(I18NCat::DIALOG);
1294
System_CopyStringToClipboard(PPSSPP_GIT_VERSION);
1295
g_OSD.Show(OSDType::MESSAGE_INFO, ApplySafeSubstitutions(di->T("Copied to clipboard: %1"), PPSSPP_GIT_VERSION));
1296
return UI::EVENT_DONE;
1297
});
1298
}
1299
1300
LinearLayout *rightColumnChoices = rightColumnItems;
1301
if (vertical) {
1302
ScrollView *rightColumnScroll = new ScrollView(ORIENT_HORIZONTAL);
1303
rightColumnChoices = new LinearLayout(ORIENT_HORIZONTAL);
1304
rightColumnScroll->Add(rightColumnChoices);
1305
rightColumnItems->Add(rightColumnScroll);
1306
}
1307
1308
if (System_GetPropertyBool(SYSPROP_HAS_FILE_BROWSER)) {
1309
rightColumnChoices->Add(new Choice(mm->T("Load", "Load...")))->OnClick.Handle(this, &MainScreen::OnLoadFile);
1310
}
1311
rightColumnChoices->Add(new Choice(mm->T("Game Settings", "Settings")))->OnClick.Handle(this, &MainScreen::OnGameSettings);
1312
rightColumnChoices->Add(new Choice(mm->T("Credits")))->OnClick.Handle(this, &MainScreen::OnCredits);
1313
1314
if (!vertical) {
1315
rightColumnChoices->Add(new Choice(mm->T("www.ppsspp.org")))->OnClick.Handle(this, &MainScreen::OnPPSSPPOrg);
1316
if (!System_GetPropertyBool(SYSPROP_APP_GOLD) && (System_GetPropertyInt(SYSPROP_DEVICE_TYPE) != DEVICE_TYPE_VR)) {
1317
Choice *gold = rightColumnChoices->Add(new Choice(mm->T("Buy PPSSPP Gold")));
1318
ScreenManager *sm = screenManager();
1319
gold->OnClick.Add([sm](UI::EventParams &) {
1320
LaunchBuyGold(sm);
1321
return UI::EVENT_DONE;
1322
});
1323
gold->SetIcon(ImageID("I_ICONGOLD"), 0.5f);
1324
gold->SetShine(true);
1325
}
1326
}
1327
1328
rightColumnChoices->Add(new Spacer(25.0));
1329
#if !PPSSPP_PLATFORM(IOS_APP_STORE)
1330
// Officially, iOS apps should not have exit buttons. Remove it to maximize app store review chances.
1331
rightColumnChoices->Add(new Choice(mm->T("Exit")))->OnClick.Handle(this, &MainScreen::OnExit);
1332
#endif
1333
1334
if (vertical) {
1335
root_ = new LinearLayout(ORIENT_VERTICAL);
1336
rightColumn->ReplaceLayoutParams(new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT));
1337
leftColumn->ReplaceLayoutParams(new LinearLayoutParams(1.0f));
1338
root_->Add(rightColumn);
1339
root_->Add(leftColumn);
1340
} else {
1341
Margins actionMenuMargins(0, 10, 10, 0);
1342
root_ = new LinearLayout(ORIENT_HORIZONTAL);
1343
rightColumn->ReplaceLayoutParams(new LinearLayoutParams(300, FILL_PARENT, actionMenuMargins));
1344
root_->Add(leftColumn);
1345
root_->Add(rightColumn);
1346
}
1347
1348
if (focusButton) {
1349
root_->SetDefaultFocusView(focusButton);
1350
} else if (tabHolder_->GetVisibility() != V_GONE) {
1351
root_->SetDefaultFocusView(tabHolder_);
1352
}
1353
1354
root_->SetTag("mainroot");
1355
}
1356
1357
bool MainScreen::key(const KeyInput &touch) {
1358
if (touch.flags & KEY_DOWN) {
1359
if (touch.keyCode == NKCODE_CTRL_LEFT || touch.keyCode == NKCODE_CTRL_RIGHT)
1360
searchKeyModifier_ = true;
1361
if (touch.keyCode == NKCODE_F && searchKeyModifier_ && System_GetPropertyBool(SYSPROP_HAS_TEXT_INPUT_DIALOG)) {
1362
auto se = GetI18NCategory(I18NCat::SEARCH);
1363
System_InputBoxGetString(GetRequesterToken(), se->T("Search term"), searchFilter_, false, [&](const std::string &value, int) {
1364
searchFilter_ = StripSpaces(value);
1365
searchChanged_ = true;
1366
});
1367
}
1368
} else if (touch.flags & KEY_UP) {
1369
if (touch.keyCode == NKCODE_CTRL_LEFT || touch.keyCode == NKCODE_CTRL_RIGHT)
1370
searchKeyModifier_ = false;
1371
}
1372
1373
return UIScreenWithBackground::key(touch);
1374
}
1375
1376
UI::EventReturn MainScreen::OnAllowStorage(UI::EventParams &e) {
1377
System_AskForPermission(SYSTEM_PERMISSION_STORAGE);
1378
return UI::EVENT_DONE;
1379
}
1380
1381
void MainScreen::sendMessage(UIMessage message, const char *value) {
1382
// Always call the base class method first to handle the most common messages.
1383
UIScreenWithBackground::sendMessage(message, value);
1384
1385
if (message == UIMessage::REQUEST_GAME_BOOT) {
1386
LaunchFile(screenManager(), this, Path(std::string(value)));
1387
} else if (message == UIMessage::PERMISSION_GRANTED && !strcmp(value, "storage")) {
1388
RecreateViews();
1389
} else if (message == UIMessage::RECENT_FILES_CHANGED) {
1390
RecreateViews();
1391
}
1392
}
1393
1394
void MainScreen::update() {
1395
UIScreen::update();
1396
UpdateUIState(UISTATE_MENU);
1397
1398
if (searchChanged_) {
1399
for (auto browser : gameBrowsers_)
1400
browser->ApplySearchFilter(searchFilter_);
1401
searchChanged_ = false;
1402
}
1403
}
1404
1405
UI::EventReturn MainScreen::OnLoadFile(UI::EventParams &e) {
1406
if (System_GetPropertyBool(SYSPROP_HAS_FILE_BROWSER)) {
1407
auto mm = GetI18NCategory(I18NCat::MAINMENU);
1408
System_BrowseForFile(GetRequesterToken(), mm->T("Load"), BrowseFileType::BOOTABLE, [](const std::string &value, int) {
1409
System_PostUIMessage(UIMessage::REQUEST_GAME_BOOT, value);
1410
});
1411
}
1412
return UI::EVENT_DONE;
1413
}
1414
1415
UI::EventReturn MainScreen::OnFullScreenToggle(UI::EventParams &e) {
1416
if (g_Config.iForceFullScreen != -1)
1417
g_Config.bFullScreen = g_Config.UseFullScreen();
1418
if (fullscreenButton_) {
1419
fullscreenButton_->SetImageID(ImageID(!g_Config.UseFullScreen() ? "I_RESTORE" : "I_FULLSCREEN"));
1420
}
1421
#if !defined(MOBILE_DEVICE)
1422
g_Config.bFullScreen = !g_Config.bFullScreen;
1423
System_ToggleFullscreenState("");
1424
#endif
1425
return UI::EVENT_DONE;
1426
}
1427
1428
void MainScreen::DrawBackground(UIContext &dc) {
1429
if (highlightedGamePath_.empty() && prevHighlightedGamePath_.empty()) {
1430
return;
1431
}
1432
1433
if (DrawBackgroundFor(dc, prevHighlightedGamePath_, 1.0f - prevHighlightProgress_)) {
1434
if (prevHighlightProgress_ < 1.0f) {
1435
prevHighlightProgress_ += 1.0f / 20.0f;
1436
}
1437
}
1438
if (!highlightedGamePath_.empty()) {
1439
if (DrawBackgroundFor(dc, highlightedGamePath_, highlightProgress_)) {
1440
if (highlightProgress_ < 1.0f) {
1441
highlightProgress_ += 1.0f / 20.0f;
1442
}
1443
}
1444
}
1445
}
1446
1447
bool MainScreen::DrawBackgroundFor(UIContext &dc, const Path &gamePath, float progress) {
1448
dc.Flush();
1449
1450
std::shared_ptr<GameInfo> ginfo;
1451
if (!gamePath.empty()) {
1452
ginfo = g_gameInfoCache->GetInfo(dc.GetDrawContext(), gamePath, GameInfoFlags::PIC1);
1453
// Loading texture data may bind a texture.
1454
dc.RebindTexture();
1455
1456
// Let's not bother if there's no picture.
1457
if (!ginfo->Ready(GameInfoFlags::PIC1) || !ginfo->pic1.texture) {
1458
return false;
1459
}
1460
} else {
1461
return false;
1462
}
1463
1464
auto pic = ginfo->GetPIC1();
1465
Draw::Texture *texture = pic ? pic->texture : nullptr;
1466
1467
uint32_t color = whiteAlpha(ease(progress)) & 0xFFc0c0c0;
1468
if (texture) {
1469
dc.GetDrawContext()->BindTexture(0, texture);
1470
dc.Draw()->DrawTexRect(dc.GetBounds(), 0, 0, 1, 1, color);
1471
dc.Flush();
1472
dc.RebindTexture();
1473
}
1474
return true;
1475
}
1476
1477
UI::EventReturn MainScreen::OnGameSelected(UI::EventParams &e) {
1478
Path path(e.s);
1479
std::shared_ptr<GameInfo> ginfo = g_gameInfoCache->GetInfo(nullptr, path, GameInfoFlags::FILE_TYPE);
1480
if (ginfo->fileType == IdentifiedFileType::PSP_SAVEDATA_DIRECTORY) {
1481
return UI::EVENT_DONE;
1482
}
1483
if (g_GameManager.GetState() == GameManagerState::INSTALLING)
1484
return UI::EVENT_DONE;
1485
1486
// Restore focus if it was highlighted (e.g. by gamepad.)
1487
restoreFocusGamePath_ = highlightedGamePath_;
1488
g_BackgroundAudio.SetGame(path);
1489
lockBackgroundAudio_ = true;
1490
screenManager()->push(new GameScreen(path, false));
1491
return UI::EVENT_DONE;
1492
}
1493
1494
UI::EventReturn MainScreen::OnGameHighlight(UI::EventParams &e) {
1495
using namespace UI;
1496
1497
Path path(e.s);
1498
1499
// Don't change when re-highlighting what's already highlighted.
1500
if (path != highlightedGamePath_ || e.a == FF_LOSTFOCUS) {
1501
if (!highlightedGamePath_.empty()) {
1502
if (prevHighlightedGamePath_.empty() || prevHighlightProgress_ >= 0.75f) {
1503
prevHighlightedGamePath_ = highlightedGamePath_;
1504
prevHighlightProgress_ = 1.0 - highlightProgress_;
1505
}
1506
highlightedGamePath_.clear();
1507
}
1508
if (e.a == FF_GOTFOCUS) {
1509
highlightedGamePath_ = path;
1510
highlightProgress_ = 0.0f;
1511
}
1512
}
1513
1514
if ((!highlightedGamePath_.empty() || e.a == FF_LOSTFOCUS) && !lockBackgroundAudio_) {
1515
g_BackgroundAudio.SetGame(highlightedGamePath_);
1516
}
1517
1518
lockBackgroundAudio_ = false;
1519
return UI::EVENT_DONE;
1520
}
1521
1522
UI::EventReturn MainScreen::OnGameSelectedInstant(UI::EventParams &e) {
1523
ScreenManager *screen = screenManager();
1524
LaunchFile(screen, nullptr, Path(e.s));
1525
return UI::EVENT_DONE;
1526
}
1527
1528
UI::EventReturn MainScreen::OnGameSettings(UI::EventParams &e) {
1529
screenManager()->push(new GameSettingsScreen(Path(), ""));
1530
return UI::EVENT_DONE;
1531
}
1532
1533
UI::EventReturn MainScreen::OnCredits(UI::EventParams &e) {
1534
screenManager()->push(new CreditsScreen());
1535
return UI::EVENT_DONE;
1536
}
1537
1538
void LaunchBuyGold(ScreenManager *screenManager) {
1539
if (System_GetPropertyBool(SYSPROP_USE_IAP)) {
1540
screenManager->push(new IAPScreen());
1541
} else {
1542
#if PPSSPP_PLATFORM(IOS_APP_STORE)
1543
System_LaunchUrl(LaunchUrlType::BROWSER_URL, "https://apps.apple.com/us/app/ppsspp-gold-psp-emulator/id6502287918");
1544
#elif PPSSPP_PLATFORM(ANDROID)
1545
System_LaunchUrl(LaunchUrlType::BROWSER_URL, "market://details?id=org.ppsspp.ppssppgold");
1546
#else
1547
System_LaunchUrl(LaunchUrlType::BROWSER_URL, "https://www.ppsspp.org/buygold");
1548
#endif
1549
}
1550
}
1551
1552
UI::EventReturn MainScreen::OnPPSSPPOrg(UI::EventParams &e) {
1553
System_LaunchUrl(LaunchUrlType::BROWSER_URL, "https://www.ppsspp.org");
1554
return UI::EVENT_DONE;
1555
}
1556
1557
UI::EventReturn MainScreen::OnForums(UI::EventParams &e) {
1558
System_LaunchUrl(LaunchUrlType::BROWSER_URL, "https://forums.ppsspp.org");
1559
return UI::EVENT_DONE;
1560
}
1561
1562
UI::EventReturn MainScreen::OnExit(UI::EventParams &e) {
1563
// Let's make sure the config was saved, since it may not have been.
1564
if (!g_Config.Save("MainScreen::OnExit")) {
1565
System_Toast("Failed to save settings!\nCheck permissions, or try to restart the device.");
1566
}
1567
1568
// Request the framework to exit cleanly.
1569
System_ExitApp();
1570
1571
UpdateUIState(UISTATE_EXIT);
1572
return UI::EVENT_DONE;
1573
}
1574
1575
void MainScreen::dialogFinished(const Screen *dialog, DialogResult result) {
1576
std::string tag = dialog->tag();
1577
if (tag == "Store") {
1578
backFromStore_ = true;
1579
RecreateViews();
1580
} else if (tag == "Game") {
1581
if (!restoreFocusGamePath_.empty() && UI::IsFocusMovementEnabled()) {
1582
// Prevent the background from fading, since we just were displaying it.
1583
highlightedGamePath_ = restoreFocusGamePath_;
1584
highlightProgress_ = 1.0f;
1585
1586
// Refocus the game button itself.
1587
int tab = tabHolder_->GetCurrentTab();
1588
if (tab >= 0 && tab < (int)gameBrowsers_.size()) {
1589
gameBrowsers_[tab]->FocusGame(restoreFocusGamePath_);
1590
}
1591
1592
// Don't get confused next time.
1593
restoreFocusGamePath_.clear();
1594
} else {
1595
// Not refocusing, so we need to stop the audio.
1596
g_BackgroundAudio.SetGame(Path());
1597
}
1598
} else if (tag == "InstallZip") {
1599
INFO_LOG(Log::System, "InstallZip finished, refreshing");
1600
if (gameBrowsers_.size() >= 2) {
1601
gameBrowsers_[1]->RequestRefresh();
1602
}
1603
} else if (tag == "IAP") {
1604
// Gold status may have changed.
1605
RecreateViews();
1606
}
1607
}
1608
1609
void UmdReplaceScreen::CreateViews() {
1610
using namespace UI;
1611
Margins actionMenuMargins(0, 100, 15, 0);
1612
auto mm = GetI18NCategory(I18NCat::MAINMENU);
1613
auto di = GetI18NCategory(I18NCat::DIALOG);
1614
1615
TabHolder *leftColumn = new TabHolder(ORIENT_HORIZONTAL, 64, nullptr, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT, 1.0));
1616
leftColumn->SetTag("UmdReplace");
1617
leftColumn->SetClip(true);
1618
1619
ViewGroup *rightColumn = new ScrollView(ORIENT_VERTICAL, new LinearLayoutParams(270, FILL_PARENT, actionMenuMargins));
1620
LinearLayout *rightColumnItems = new LinearLayout(ORIENT_VERTICAL, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT));
1621
rightColumnItems->SetSpacing(0.0f);
1622
rightColumn->Add(rightColumnItems);
1623
1624
if (g_Config.iMaxRecent > 0) {
1625
ScrollView *scrollRecentGames = new ScrollView(ORIENT_VERTICAL, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT));
1626
scrollRecentGames->SetTag("UmdReplaceRecentGames");
1627
GameBrowser *tabRecentGames = new GameBrowser(GetRequesterToken(),
1628
Path("!RECENT"), BrowseFlags::NONE, &g_Config.bGridView1, screenManager(), "", "",
1629
new LinearLayoutParams(FILL_PARENT, FILL_PARENT));
1630
scrollRecentGames->Add(tabRecentGames);
1631
leftColumn->AddTab(mm->T("Recent"), scrollRecentGames);
1632
tabRecentGames->OnChoice.Handle(this, &UmdReplaceScreen::OnGameSelected);
1633
tabRecentGames->OnHoldChoice.Handle(this, &UmdReplaceScreen::OnGameSelected);
1634
}
1635
ScrollView *scrollAllGames = new ScrollView(ORIENT_VERTICAL, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT));
1636
scrollAllGames->SetTag("UmdReplaceAllGames");
1637
1638
GameBrowser *tabAllGames = new GameBrowser(GetRequesterToken(), Path(g_Config.currentDirectory), BrowseFlags::STANDARD, &g_Config.bGridView2, screenManager(),
1639
mm->T("How to get games"), "https://www.ppsspp.org/getgames.html",
1640
new LinearLayoutParams(FILL_PARENT, FILL_PARENT));
1641
1642
scrollAllGames->Add(tabAllGames);
1643
1644
leftColumn->AddTab(mm->T("Games"), scrollAllGames);
1645
1646
tabAllGames->OnChoice.Handle(this, &UmdReplaceScreen::OnGameSelected);
1647
1648
tabAllGames->OnHoldChoice.Handle(this, &UmdReplaceScreen::OnGameSelected);
1649
1650
if (System_GetPropertyBool(SYSPROP_HAS_FILE_BROWSER)) {
1651
rightColumnItems->Add(new Choice(mm->T("Load", "Load...")))->OnClick.Add([&](UI::EventParams &e) {
1652
auto mm = GetI18NCategory(I18NCat::MAINMENU);
1653
System_BrowseForFile(GetRequesterToken(), mm->T("Load"), BrowseFileType::BOOTABLE, [this](const std::string &value, int) {
1654
__UmdReplace(Path(value));
1655
TriggerFinish(DR_OK);
1656
});
1657
return EVENT_DONE;
1658
});
1659
}
1660
1661
rightColumnItems->Add(new Choice(di->T("Cancel")))->OnClick.Handle<UIScreen>(this, &UIScreen::OnCancel);
1662
rightColumnItems->Add(new Spacer());
1663
rightColumnItems->Add(new Choice(mm->T("Game Settings")))->OnClick.Handle(this, &UmdReplaceScreen::OnGameSettings);
1664
1665
if (g_recentFiles.HasAny()) {
1666
leftColumn->SetCurrentTab(0, true);
1667
} else if (g_Config.iMaxRecent > 0) {
1668
leftColumn->SetCurrentTab(1, true);
1669
}
1670
1671
root_ = new LinearLayout(ORIENT_HORIZONTAL);
1672
root_->Add(leftColumn);
1673
root_->Add(rightColumn);
1674
}
1675
1676
void UmdReplaceScreen::update() {
1677
UpdateUIState(UISTATE_PAUSEMENU);
1678
UIScreen::update();
1679
}
1680
1681
UI::EventReturn UmdReplaceScreen::OnGameSelected(UI::EventParams &e) {
1682
__UmdReplace(Path(e.s));
1683
TriggerFinish(DR_OK);
1684
return UI::EVENT_DONE;
1685
}
1686
1687
UI::EventReturn UmdReplaceScreen::OnGameSettings(UI::EventParams &e) {
1688
screenManager()->push(new GameSettingsScreen(Path()));
1689
return UI::EVENT_DONE;
1690
}
1691
1692
void GridSettingsPopupScreen::CreatePopupContents(UI::ViewGroup *parent) {
1693
using namespace UI;
1694
1695
auto di = GetI18NCategory(I18NCat::DIALOG);
1696
auto sy = GetI18NCategory(I18NCat::SYSTEM);
1697
auto mm = GetI18NCategory(I18NCat::MAINMENU);
1698
1699
ScrollView *scroll = new ScrollView(ORIENT_VERTICAL, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT, 1.0f));
1700
LinearLayout *items = new LinearLayoutList(ORIENT_VERTICAL);
1701
1702
items->Add(new CheckBox(&g_Config.bGridView1, sy->T("Display Recent on a grid")));
1703
items->Add(new CheckBox(&g_Config.bGridView2, sy->T("Display Games on a grid")));
1704
items->Add(new CheckBox(&g_Config.bGridView3, sy->T("Display Homebrew on a grid")));
1705
static const char *defaultTabs[] = { "Recent", "Games", "Homebrew & Demos" };
1706
PopupMultiChoice *beziersChoice = items->Add(new PopupMultiChoice(&g_Config.iDefaultTab, sy->T("Default tab"), defaultTabs, 0, ARRAY_SIZE(defaultTabs), I18NCat::MAINMENU, screenManager()));
1707
1708
items->Add(new ItemHeader(sy->T("Grid icon size")));
1709
items->Add(new Choice(sy->T("Increase size")))->OnClick.Handle(this, &GridSettingsPopupScreen::GridPlusClick);
1710
items->Add(new Choice(sy->T("Decrease size")))->OnClick.Handle(this, &GridSettingsPopupScreen::GridMinusClick);
1711
1712
items->Add(new ItemHeader(sy->T("Display Extra Info")));
1713
items->Add(new CheckBox(&g_Config.bShowIDOnGameIcon, sy->T("Show ID")));
1714
items->Add(new CheckBox(&g_Config.bShowRegionOnGameIcon, sy->T("Show region flag")));
1715
1716
if (g_Config.iMaxRecent > 0) {
1717
items->Add(new ItemHeader(sy->T("Clear Recent")));
1718
items->Add(new Choice(sy->T("Clear Recent Games List")))->OnClick.Handle(this, &GridSettingsPopupScreen::OnRecentClearClick);
1719
}
1720
1721
scroll->Add(items);
1722
parent->Add(scroll);
1723
}
1724
1725
UI::EventReturn GridSettingsPopupScreen::GridPlusClick(UI::EventParams &e) {
1726
g_Config.fGameGridScale = std::min(g_Config.fGameGridScale*1.25f, MAX_GAME_GRID_SCALE);
1727
return UI::EVENT_DONE;
1728
}
1729
1730
UI::EventReturn GridSettingsPopupScreen::GridMinusClick(UI::EventParams &e) {
1731
g_Config.fGameGridScale = std::max(g_Config.fGameGridScale/1.25f, MIN_GAME_GRID_SCALE);
1732
return UI::EVENT_DONE;
1733
}
1734
1735
UI::EventReturn GridSettingsPopupScreen::OnRecentClearClick(UI::EventParams &e) {
1736
g_recentFiles.Clear();
1737
OnRecentChanged.Trigger(e);
1738
TriggerFinish(DR_OK);
1739
return UI::EVENT_DONE;
1740
}
1741
1742