CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutSign UpSign In
hrydgard

CoCalc provides the best real-time collaborative environment for Jupyter Notebooks, LaTeX documents, and SageMath, scalable from individual users to large groups and classes!

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