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