CoCalc provides the best real-time collaborative environment for Jupyter Notebooks, LaTeX documents, and SageMath, scalable from individual users to large groups and classes!
CoCalc provides the best real-time collaborative environment for Jupyter Notebooks, LaTeX documents, and SageMath, scalable from individual users to large groups and classes!
Path: blob/master/UI/MainScreen.cpp
Views: 1401
// Copyright (c) 2013- PPSSPP Project.12// This program is free software: you can redistribute it and/or modify3// it under the terms of the GNU General Public License as published by4// the Free Software Foundation, version 2.0 or later versions.56// This program is distributed in the hope that it will be useful,7// but WITHOUT ANY WARRANTY; without even the implied warranty of8// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the9// GNU General Public License 2.0 for more details.1011// A copy of the GPL 2.0 should have been included with the program.12// If not, see http://www.gnu.org/licenses/1314// Official git repository and contact information can be found at15// https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/.1617#include <algorithm>18#include <cmath>19#include <sstream>2021#include "ppsspp_config.h"2223#include "Common/System/Display.h"24#include "Common/System/System.h"25#include "Common/System/Request.h"26#include "Common/System/NativeApp.h"27#include "Common/Render/TextureAtlas.h"28#include "Common/Render/DrawBuffer.h"29#include "Common/UI/Root.h"30#include "Common/UI/Context.h"31#include "Common/UI/View.h"32#include "Common/UI/ViewGroup.h"3334#include "Common/Data/Color/RGBAUtil.h"35#include "Common/Data/Encoding/Utf8.h"36#include "Common/File/PathBrowser.h"37#include "Common/Math/curves.h"38#include "Common/Net/URL.h"39#include "Common/File/FileUtil.h"40#include "Common/TimeUtil.h"41#include "Common/StringUtils.h"42#include "Core/System.h"43#include "Core/Reporting.h"44#include "Core/HLE/sceCtrl.h"45#include "Core/ELF/PBPReader.h"46#include "Core/ELF/ParamSFO.h"47#include "Core/Util/GameManager.h"4849#include "UI/BackgroundAudio.h"50#include "UI/EmuScreen.h"51#include "UI/MainScreen.h"52#include "UI/GameScreen.h"53#include "UI/GameInfoCache.h"54#include "UI/GameSettingsScreen.h"55#include "UI/MiscScreens.h"56#include "UI/ControlMappingScreen.h"57#include "UI/RemoteISOScreen.h"58#include "UI/DisplayLayoutScreen.h"59#include "UI/SavedataScreen.h"60#include "UI/Store.h"61#include "UI/InstallZipScreen.h"62#include "Core/Config.h"63#include "Core/Loaders.h"64#include "GPU/GPUInterface.h"65#include "Common/Data/Text/I18n.h"6667#if PPSSPP_PLATFORM(IOS) || PPSSPP_PLATFORM(MAC)68#include "UI/DarwinFileSystemServices.h" // For the browser69#endif7071#include "Core/HLE/sceUmd.h"7273bool MainScreen::showHomebrewTab = false;7475bool LaunchFile(ScreenManager *screenManager, const Path &path) {76// Depending on the file type, we don't want to launch EmuScreen at all.77auto loader = ConstructFileLoader(path);78if (!loader) {79return false;80}8182std::string errorString;83IdentifiedFileType type = Identify_File(loader, &errorString);84delete loader;8586switch (type) {87case IdentifiedFileType::ARCHIVE_ZIP:88screenManager->push(new InstallZipScreen(path));89break;90default:91// Let the EmuScreen take care of it.92screenManager->switchScreen(new EmuScreen(path));93break;94}95return true;96}9798static bool IsTempPath(const Path &str) {99std::string item = str.ToString();100101#ifdef _WIN32102// Normalize slashes.103item = ReplaceAll(item, "/", "\\");104#endif105106std::vector<std::string> tempPaths = System_GetPropertyStringVec(SYSPROP_TEMP_DIRS);107for (auto temp : tempPaths) {108#ifdef _WIN32109temp = ReplaceAll(temp, "/", "\\");110if (!temp.empty() && temp[temp.size() - 1] != '\\')111temp += "\\";112#else113if (!temp.empty() && temp[temp.size() - 1] != '/')114temp += "/";115#endif116if (startsWith(item, temp))117return true;118}119120return false;121}122123class GameButton : public UI::Clickable {124public:125GameButton(const Path &gamePath, bool gridStyle, UI::LayoutParams *layoutParams = 0)126: UI::Clickable(layoutParams), gridStyle_(gridStyle), gamePath_(gamePath) {}127128void Draw(UIContext &dc) override;129std::string DescribeText() const override;130void GetContentDimensions(const UIContext &dc, float &w, float &h) const override {131if (gridStyle_) {132w = 144*g_Config.fGameGridScale;133h = 80*g_Config.fGameGridScale;134} else {135w = 500;136h = 50;137}138}139140const Path &GamePath() const { return gamePath_; }141142void SetHoldEnabled(bool hold) {143holdEnabled_ = hold;144}145bool Touch(const TouchInput &input) override {146bool retval = UI::Clickable::Touch(input);147hovering_ = bounds_.Contains(input.x, input.y);148if (hovering_ && (input.flags & TOUCH_DOWN)) {149holdStart_ = time_now_d();150}151if (input.flags & TOUCH_UP) {152holdStart_ = 0;153}154return retval;155}156157bool Key(const KeyInput &key) override {158std::vector<int> pspKeys;159bool showInfo = false;160161if (HasFocus() && UI::IsInfoKey(key)) {162// If the button mapped to triangle, then show the info.163if (key.flags & KEY_UP) {164showInfo = true;165}166} else if (hovering_ && key.deviceId == DEVICE_ID_MOUSE && key.keyCode == NKCODE_EXT_MOUSEBUTTON_2) {167// If it's the right mouse button, and it's not otherwise mapped, show the info also.168if (key.flags & KEY_DOWN) {169showInfoPressed_ = true;170}171if ((key.flags & KEY_UP) && showInfoPressed_) {172showInfo = true;173showInfoPressed_ = false;174}175}176177if (showInfo) {178TriggerOnHoldClick();179return true;180}181182return Clickable::Key(key);183}184185void Update() override {186// Hold button for 1.5 seconds to launch the game options187if (holdEnabled_ && holdStart_ != 0.0 && holdStart_ < time_now_d() - 1.5) {188TriggerOnHoldClick();189}190}191192void FocusChanged(int focusFlags) override {193UI::Clickable::FocusChanged(focusFlags);194TriggerOnHighlight(focusFlags);195}196197UI::Event OnHoldClick;198UI::Event OnHighlight;199200private:201void TriggerOnHoldClick() {202holdStart_ = 0.0;203UI::EventParams e{};204e.v = this;205e.s = gamePath_.ToString();206down_ = false;207OnHoldClick.Trigger(e);208}209void TriggerOnHighlight(int focusFlags) {210UI::EventParams e{};211e.v = this;212e.s = gamePath_.ToString();213e.a = focusFlags;214OnHighlight.Trigger(e);215}216217bool gridStyle_;218Path gamePath_;219std::string title_;220221double holdStart_ = 0.0;222bool holdEnabled_ = true;223bool showInfoPressed_ = false;224bool hovering_ = false;225};226227void GameButton::Draw(UIContext &dc) {228std::shared_ptr<GameInfo> ginfo = g_gameInfoCache->GetInfo(dc.GetDrawContext(), gamePath_, GameInfoFlags::PARAM_SFO | GameInfoFlags::ICON);229Draw::Texture *texture = 0;230u32 color = 0, shadowColor = 0;231using namespace UI;232233if (ginfo->Ready(GameInfoFlags::ICON) && ginfo->icon.texture) {234texture = ginfo->icon.texture;235}236237int x = bounds_.x;238int y = bounds_.y;239int w = gridStyle_ ? bounds_.w : 144;240int h = bounds_.h;241242UI::Style style = dc.theme->itemStyle;243if (down_)244style = dc.theme->itemDownStyle;245246if (!gridStyle_ || !texture) {247h = 50;248if (HasFocus())249style = down_ ? dc.theme->itemDownStyle : dc.theme->itemFocusedStyle;250251Drawable bg = style.background;252253dc.Draw()->Flush();254dc.RebindTexture();255dc.FillRect(bg, bounds_);256dc.Draw()->Flush();257}258259if (texture) {260color = whiteAlpha(ease((time_now_d() - ginfo->icon.timeLoaded) * 2));261shadowColor = blackAlpha(ease((time_now_d() - ginfo->icon.timeLoaded) * 2));262float tw = texture->Width();263float th = texture->Height();264265// Adjust position so we don't stretch the image vertically or horizontally.266// Make sure it's not wider than 144 (like Doom Legacy homebrew), ugly in the grid mode.267float nw = std::min(h * tw / th, (float)w);268x += (w - nw) / 2.0f;269w = nw;270}271272int txOffset = down_ ? 4 : 0;273if (!gridStyle_) txOffset = 0;274275Bounds overlayBounds = bounds_;276u32 overlayColor = 0;277if (holdEnabled_ && holdStart_ != 0.0) {278double time_held = time_now_d() - holdStart_;279overlayColor = whiteAlpha(time_held / 2.5f);280}281282// Render button283int dropsize = 10;284if (texture) {285if (!gridStyle_) {286x += 4;287}288if (txOffset) {289dropsize = 3;290y += txOffset * 2;291overlayBounds.y += txOffset * 2;292}293if (HasFocus()) {294dc.Draw()->Flush();295dc.RebindTexture();296float pulse = sin(time_now_d() * 7.0) * 0.25 + 0.8;297dc.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);298dc.Draw()->Flush();299} else {300dc.Draw()->Flush();301dc.RebindTexture();302dc.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);303dc.Draw()->Flush();304}305306dc.Draw()->Flush();307dc.GetDrawContext()->BindTexture(0, texture);308if (holdStart_ != 0.0) {309double time_held = time_now_d() - holdStart_;310int holdFrameCount = (int)(time_held * 60.0f);311if (holdFrameCount > 60) {312// Blink before launching by holding313if (((holdFrameCount >> 3) & 1) == 0)314color = darkenColor(color);315}316}317dc.Draw()->DrawTexRect(x, y, x+w, y+h, 0, 0, 1, 1, color);318dc.Draw()->Flush();319}320321char discNumInfo[8];322if (ginfo->disc_total > 1)323snprintf(discNumInfo, sizeof(discNumInfo), "-DISC%d", ginfo->disc_number);324else325discNumInfo[0] = '\0';326327dc.Draw()->Flush();328dc.RebindTexture();329dc.SetFontStyle(dc.theme->uiFont);330if (gridStyle_ && ginfo->fileType == IdentifiedFileType::PPSSPP_GE_DUMP) {331// Super simple drawing for ge dumps.332dc.PushScissor(bounds_);333const std::string currentTitle = ginfo->GetTitle();334dc.SetFontScale(0.6f, 0.6f);335dc.DrawText(title_, bounds_.x + 4.0f, bounds_.centerY(), style.fgColor, ALIGN_VCENTER | ALIGN_LEFT);336dc.SetFontScale(1.0f, 1.0f);337title_ = currentTitle;338dc.Draw()->Flush();339dc.PopScissor();340} else if (!gridStyle_) {341float tw, th;342dc.Draw()->Flush();343dc.PushScissor(bounds_);344const std::string currentTitle = ginfo->GetTitle();345if (!currentTitle.empty()) {346title_ = ReplaceAll(currentTitle, "\n", " ");347}348349dc.MeasureText(dc.GetFontStyle(), 1.0f, 1.0f, title_, &tw, &th, 0);350351int availableWidth = bounds_.w - 150;352if (g_Config.bShowIDOnGameIcon) {353float vw, vh;354dc.MeasureText(dc.GetFontStyle(), 0.7f, 0.7f, ginfo->id_version, &vw, &vh, 0);355availableWidth -= vw + 20;356dc.SetFontScale(0.7f, 0.7f);357dc.DrawText(ginfo->id_version, bounds_.x + availableWidth + 160, bounds_.centerY(), style.fgColor, ALIGN_VCENTER);358dc.SetFontScale(1.0f, 1.0f);359}360float sineWidth = std::max(0.0f, (tw - availableWidth)) / 2.0f;361362float tx = 150;363if (availableWidth < tw) {364tx -= (1.0f + sin(time_now_d() * 1.5f)) * sineWidth;365Bounds tb = bounds_;366tb.x = bounds_.x + 150;367tb.w = availableWidth;368dc.PushScissor(tb);369}370dc.DrawText(title_, bounds_.x + tx, bounds_.centerY(), style.fgColor, ALIGN_VCENTER);371if (availableWidth < tw) {372dc.PopScissor();373}374dc.Draw()->Flush();375dc.PopScissor();376} else if (!texture) {377dc.Draw()->Flush();378dc.PushScissor(bounds_);379dc.DrawText(title_, bounds_.x + 4, bounds_.centerY(), style.fgColor, ALIGN_VCENTER);380dc.Draw()->Flush();381dc.PopScissor();382} else {383dc.Draw()->Flush();384}385if (ginfo->hasConfig && !ginfo->id.empty()) {386const AtlasImage *gearImage = dc.Draw()->GetAtlas()->getImage(ImageID("I_GEAR"));387if (gearImage) {388if (gridStyle_) {389dc.Draw()->DrawImage(ImageID("I_GEAR"), x, y + h - gearImage->h*g_Config.fGameGridScale, g_Config.fGameGridScale);390} else {391dc.Draw()->DrawImage(ImageID("I_GEAR"), x - gearImage->w, y, 1.0f);392}393}394}395if (g_Config.bShowRegionOnGameIcon && ginfo->region >= 0 && ginfo->region < GAMEREGION_MAX && ginfo->region != GAMEREGION_OTHER) {396const ImageID regionIcons[GAMEREGION_MAX] = {397ImageID("I_FLAG_JP"),398ImageID("I_FLAG_US"),399ImageID("I_FLAG_EU"),400ImageID("I_FLAG_HK"),401ImageID("I_FLAG_AS"),402ImageID("I_FLAG_KO"),403ImageID::invalid(),404};405const AtlasImage *image = dc.Draw()->GetAtlas()->getImage(regionIcons[ginfo->region]);406if (image) {407if (gridStyle_) {408dc.Draw()->DrawImage(regionIcons[ginfo->region], x + w - (image->w + 5)*g_Config.fGameGridScale,409y + h - (image->h + 5)*g_Config.fGameGridScale, g_Config.fGameGridScale);410} else {411dc.Draw()->DrawImage(regionIcons[ginfo->region], x - 2 - image->w - 3, y + h - image->h - 5, 1.0f);412}413}414}415if (gridStyle_ && g_Config.bShowIDOnGameIcon) {416dc.SetFontScale(0.5f*g_Config.fGameGridScale, 0.5f*g_Config.fGameGridScale);417dc.DrawText(ginfo->id_version, x+5, y+1, 0xFF000000, ALIGN_TOPLEFT);418dc.DrawText(ginfo->id_version, x+4, y, dc.theme->infoStyle.fgColor, ALIGN_TOPLEFT);419dc.SetFontScale(1.0f, 1.0f);420}421if (overlayColor) {422dc.FillRect(Drawable(overlayColor), overlayBounds);423}424dc.RebindTexture();425}426427std::string GameButton::DescribeText() const {428std::shared_ptr<GameInfo> ginfo = g_gameInfoCache->GetInfo(nullptr, gamePath_, GameInfoFlags::PARAM_SFO);429if (!ginfo->Ready(GameInfoFlags::PARAM_SFO))430return "...";431auto u = GetI18NCategory(I18NCat::UI_ELEMENTS);432return ApplySafeSubstitutions(u->T("%1 button"), ginfo->GetTitle());433}434435class DirButton : public UI::Button {436public:437DirButton(const Path &path, bool gridStyle, UI::LayoutParams *layoutParams)438: UI::Button(path.ToString(), layoutParams), path_(path), gridStyle_(gridStyle), absolute_(false) {}439DirButton(const Path &path, const std::string &text, bool gridStyle, UI::LayoutParams *layoutParams = 0)440: UI::Button(text, layoutParams), path_(path), gridStyle_(gridStyle), absolute_(true) {}441442void Draw(UIContext &dc) override;443444const Path &GetPath() const {445return path_;446}447448bool PathAbsolute() const {449return absolute_;450}451452private:453Path path_;454bool gridStyle_;455bool absolute_;456};457458void DirButton::Draw(UIContext &dc) {459using namespace UI;460Style style = dc.theme->itemStyle;461462if (HasFocus()) style = dc.theme->itemFocusedStyle;463if (down_) style = dc.theme->itemDownStyle;464if (!IsEnabled()) style = dc.theme->itemDisabledStyle;465466dc.FillRect(style.background, bounds_);467468std::string_view text(GetText());469470ImageID image = ImageID("I_FOLDER");471if (text == "..") {472image = ImageID("I_UP_DIRECTORY");473}474475float tw, th;476dc.MeasureText(dc.GetFontStyle(), gridStyle_ ? g_Config.fGameGridScale : 1.0, gridStyle_ ? g_Config.fGameGridScale : 1.0, text, &tw, &th, 0);477478bool compact = bounds_.w < 180 * (gridStyle_ ? g_Config.fGameGridScale : 1.0);479480if (gridStyle_) {481dc.SetFontScale(g_Config.fGameGridScale, g_Config.fGameGridScale);482}483if (compact) {484// No icon, except "up"485dc.PushScissor(bounds_);486if (image == ImageID("I_FOLDER")) {487dc.DrawText(text, bounds_.x + 5, bounds_.centerY(), style.fgColor, ALIGN_VCENTER);488} else {489dc.Draw()->DrawImage(image, bounds_.centerX(), bounds_.centerY(), gridStyle_ ? g_Config.fGameGridScale : 1.0, style.fgColor, ALIGN_CENTER);490}491dc.PopScissor();492} else {493bool scissor = false;494if (tw + 150 > bounds_.w) {495dc.PushScissor(bounds_);496scissor = true;497}498dc.Draw()->DrawImage(image, bounds_.x + 72, bounds_.centerY(), 0.88f*(gridStyle_ ? g_Config.fGameGridScale : 1.0), style.fgColor, ALIGN_CENTER);499dc.DrawText(text, bounds_.x + 150, bounds_.centerY(), style.fgColor, ALIGN_VCENTER);500501if (scissor) {502dc.PopScissor();503}504}505if (gridStyle_) {506dc.SetFontScale(1.0, 1.0);507}508}509510GameBrowser::GameBrowser(int token, const Path &path, BrowseFlags browseFlags, bool *gridStyle, ScreenManager *screenManager, std::string_view lastText, std::string_view lastLink, UI::LayoutParams *layoutParams)511: LinearLayout(UI::ORIENT_VERTICAL, layoutParams), gridStyle_(gridStyle), browseFlags_(browseFlags), lastText_(lastText), lastLink_(lastLink), screenManager_(screenManager), token_(token) {512using namespace UI;513path_.SetUserAgent(StringFromFormat("PPSSPP/%s", PPSSPP_GIT_VERSION));514Path memstickRoot = GetSysDirectory(DIRECTORY_MEMSTICK_ROOT);515if (memstickRoot == GetSysDirectory(DIRECTORY_PSP)) {516path_.SetRootAlias("ms:/PSP/", memstickRoot);517} else {518path_.SetRootAlias("ms:/", memstickRoot);519}520if (System_GetPropertyBool(SYSPROP_LIMITED_FILE_BROWSING) &&521(path.Type() == PathType::NATIVE || path.Type() == PathType::CONTENT_URI)) {522// Note: We don't restrict if the path is HTTPS, otherwise remote disc streaming breaks!523path_.RestrictToRoot(GetSysDirectory(DIRECTORY_MEMSTICK_ROOT));524}525path_.SetPath(path);526Refresh();527}528529void GameBrowser::FocusGame(const Path &gamePath) {530focusGamePath_ = gamePath;531Refresh();532focusGamePath_.clear();533}534535void GameBrowser::SetPath(const Path &path) {536path_.SetPath(path);537g_Config.currentDirectory = path_.GetPath();538Refresh();539}540541void GameBrowser::ApplySearchFilter(const std::string &filter) {542searchFilter_ = filter;543std::transform(searchFilter_.begin(), searchFilter_.end(), searchFilter_.begin(), tolower);544545// We don't refresh because game info loads asynchronously anyway.546ApplySearchFilter();547}548549void GameBrowser::ApplySearchFilter() {550if (searchFilter_.empty() && searchStates_.empty()) {551// We haven't hidden anything, and we're not searching, so do nothing.552searchPending_ = false;553return;554}555556searchPending_ = false;557// By default, everything is matching.558searchStates_.resize(gameList_->GetNumSubviews(), SearchState::MATCH);559560if (searchFilter_.empty()) {561// Just quickly mark anything we hid as visible again.562for (int i = 0; i < gameList_->GetNumSubviews(); ++i) {563UI::View *v = gameList_->GetViewByIndex(i);564if (searchStates_[i] != SearchState::MATCH)565v->SetVisibility(UI::V_VISIBLE);566}567568searchStates_.clear();569return;570}571572for (int i = 0; i < gameList_->GetNumSubviews(); ++i) {573UI::View *v = gameList_->GetViewByIndex(i);574std::string label = v->DescribeText();575// TODO: Maybe we should just save the gameButtons list, though nice to search dirs too?576// This is a bit of a hack to recognize a pending game title.577if (label == "...") {578searchPending_ = true;579// Hide anything pending while, we'll pop-in search results as they match.580// Note: we leave it at MATCH if gone before, so we don't show it again.581if (v->GetVisibility() == UI::V_VISIBLE) {582if (searchStates_[i] == SearchState::MATCH)583v->SetVisibility(UI::V_GONE);584searchStates_[i] = SearchState::PENDING;585}586continue;587}588589std::transform(label.begin(), label.end(), label.begin(), tolower);590bool match = v->CanBeFocused() && label.find(searchFilter_) != label.npos;591if (match && searchStates_[i] != SearchState::MATCH) {592// It was previously visible and force hidden, so show it again.593v->SetVisibility(UI::V_VISIBLE);594searchStates_[i] = SearchState::MATCH;595} else if (!match && searchStates_[i] == SearchState::MATCH && v->GetVisibility() == UI::V_VISIBLE) {596v->SetVisibility(UI::V_GONE);597searchStates_[i] = SearchState::MISMATCH;598}599}600}601602UI::EventReturn GameBrowser::LayoutChange(UI::EventParams &e) {603*gridStyle_ = e.a == 0 ? true : false;604Refresh();605return UI::EVENT_DONE;606}607608UI::EventReturn GameBrowser::LastClick(UI::EventParams &e) {609System_LaunchUrl(LaunchUrlType::BROWSER_URL, lastLink_.c_str());610return UI::EVENT_DONE;611}612613UI::EventReturn GameBrowser::BrowseClick(UI::EventParams &e) {614auto mm = GetI18NCategory(I18NCat::MAINMENU);615System_BrowseForFolder(token_, mm->T("Choose folder"), path_.GetPath(), [this](const std::string &filename, int) {616this->SetPath(Path(filename));617});618return UI::EVENT_DONE;619}620621UI::EventReturn GameBrowser::StorageClick(UI::EventParams &e) {622std::vector<std::string> storageDirs = System_GetPropertyStringVec(SYSPROP_ADDITIONAL_STORAGE_DIRS);623if (storageDirs.empty()) {624// Shouldn't happen - this button shouldn't be clickable.625return UI::EVENT_DONE;626}627if (storageDirs.size() == 1) {628SetPath(Path(storageDirs[0]));629} else {630// TODO: We should popup a dialog letting the user choose one.631SetPath(Path(storageDirs[0]));632}633return UI::EVENT_DONE;634}635636UI::EventReturn GameBrowser::OnHomeClick(UI::EventParams &e) {637if (path_.GetPath().Type() == PathType::CONTENT_URI) {638Path rootPath = path_.GetPath().GetRootVolume();639if (rootPath != path_.GetPath()) {640SetPath(rootPath);641return UI::EVENT_DONE;642}643if (System_GetPropertyBool(SYSPROP_ANDROID_SCOPED_STORAGE)) {644// There'll be no sensible home, ignore.645return UI::EVENT_DONE;646}647}648649SetPath(HomePath());650return UI::EVENT_DONE;651}652653// TODO: This doesn't make that much sense for Android, especially after scoped storage..654// Maybe we should have no home directory in this case. Or it should just navigate to the root655// of the current folder tree.656Path GameBrowser::HomePath() {657if (!homePath_.empty()) {658return homePath_;659}660#if PPSSPP_PLATFORM(ANDROID) || PPSSPP_PLATFORM(SWITCH) || defined(USING_WIN_UI) || PPSSPP_PLATFORM(UWP) || PPSSPP_PLATFORM(IOS)661return g_Config.memStickDirectory;662#else663return Path(getenv("HOME"));664#endif665}666667UI::EventReturn GameBrowser::PinToggleClick(UI::EventParams &e) {668auto &pinnedPaths = g_Config.vPinnedPaths;669const std::string path = File::ResolvePath(path_.GetPath().ToString());670if (IsCurrentPathPinned()) {671pinnedPaths.erase(std::remove(pinnedPaths.begin(), pinnedPaths.end(), path), pinnedPaths.end());672} else {673pinnedPaths.push_back(path);674}675Refresh();676return UI::EVENT_DONE;677}678679bool GameBrowser::DisplayTopBar() {680return path_.GetPath().ToString() != "!RECENT";681}682683bool GameBrowser::HasSpecialFiles(std::vector<Path> &filenames) {684if (path_.GetPath().ToString() == "!RECENT") {685filenames.clear();686for (auto &str : g_Config.RecentIsos()) {687filenames.push_back(Path(str));688}689return true;690}691return false;692}693694void GameBrowser::Update() {695LinearLayout::Update();696if (refreshPending_) {697path_.Refresh();698}699if ((listingPending_ && path_.IsListingReady()) || refreshPending_) {700Refresh();701refreshPending_ = false;702}703if (searchPending_) {704ApplySearchFilter();705}706}707708void GameBrowser::Draw(UIContext &dc) {709using namespace UI;710711if (lastScale_ != g_Config.fGameGridScale || lastLayoutWasGrid_ != *gridStyle_) {712Refresh();713}714715if (hasDropShadow_) {716// Darken things behind.717dc.FillRect(UI::Drawable(0x60000000), dc.GetBounds().Expand(dropShadowExpand_));718float dropsize = 30.0f;719dc.Draw()->DrawImage4Grid(dc.theme->dropShadow4Grid,720bounds_.x - dropsize, bounds_.y,721bounds_.x2() + dropsize, bounds_.y2()+dropsize*1.5f, 0xDF000000, 3.0f);722}723724if (clip_) {725dc.PushScissor(bounds_);726}727728dc.FillRect(bg_, bounds_);729for (View *view : views_) {730if (view->GetVisibility() == V_VISIBLE) {731// Check if bounds are in current scissor rectangle.732if (dc.GetScissorBounds().Intersects(dc.TransformBounds(view->GetBounds())))733view->Draw(dc);734}735}736if (clip_) {737dc.PopScissor();738}739}740741static bool IsValidPBP(const Path &path, bool allowHomebrew) {742if (!File::Exists(path))743return false;744745std::unique_ptr<FileLoader> loader(ConstructFileLoader(path));746PBPReader pbp(loader.get());747std::vector<u8> sfoData;748if (!pbp.GetSubFile(PBP_PARAM_SFO, &sfoData))749return false;750751ParamSFOData sfo;752sfo.ReadSFO(sfoData);753if (!allowHomebrew && sfo.GetValueString("DISC_ID").empty())754return false;755756if (sfo.GetValueString("CATEGORY") == "ME")757return false;758759return true;760}761762void GameBrowser::Refresh() {763using namespace UI;764765lastScale_ = g_Config.fGameGridScale;766lastLayoutWasGrid_ = *gridStyle_;767768// Kill all the contents769Clear();770searchStates_.clear();771772Add(new Spacer(1.0f));773auto mm = GetI18NCategory(I18NCat::MAINMENU);774775// No topbar on recent screen776if (DisplayTopBar()) {777LinearLayout *topBar = new LinearLayout(ORIENT_HORIZONTAL, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT));778if (browseFlags_ & BrowseFlags::NAVIGATE) {779topBar->Add(new Spacer(2.0f));780topBar->Add(new TextView(path_.GetFriendlyPath(), ALIGN_VCENTER | FLAG_WRAP_TEXT, true, new LinearLayoutParams(FILL_PARENT, 64.0f, 1.0f)));781topBar->Add(new Choice(ImageID("I_HOME"), new LayoutParams(WRAP_CONTENT, 64.0f)))->OnClick.Handle(this, &GameBrowser::OnHomeClick);782if (System_GetPropertyBool(SYSPROP_HAS_ADDITIONAL_STORAGE)) {783topBar->Add(new Choice(ImageID("I_SDCARD"), new LayoutParams(WRAP_CONTENT, 64.0f)))->OnClick.Handle(this, &GameBrowser::StorageClick);784}785#if PPSSPP_PLATFORM(IOS_APP_STORE)786// Don't show a browse button, not meaningful to browse outside the documents folder it seems,787// as we can't list things like document folders of another app, as far as I can tell.788// However, we do show a Load.. button for picking individual files, that seems to work.789#elif PPSSPP_PLATFORM(IOS) || PPSSPP_PLATFORM(MAC)790// on Darwin, we don't show the 'Browse' text alongside the image791// we show just the image, because we don't need to emphasize the button on Darwin792topBar->Add(new Choice(ImageID("I_FOLDER_OPEN"), new LayoutParams(WRAP_CONTENT, 64.0f)))->OnClick.Handle(this, &GameBrowser::BrowseClick);793#else794if ((browseFlags_ & BrowseFlags::BROWSE) && System_GetPropertyBool(SYSPROP_HAS_FOLDER_BROWSER)) {795topBar->Add(new Choice(mm->T("Browse"), ImageID("I_FOLDER_OPEN"), new LayoutParams(WRAP_CONTENT, 64.0f)))->OnClick.Handle(this, &GameBrowser::BrowseClick);796}797if (System_GetPropertyInt(SYSPROP_DEVICE_TYPE) == DEVICE_TYPE_TV) {798topBar->Add(new Choice(mm->T("Enter Path"), new LayoutParams(WRAP_CONTENT, 64.0f)))->OnClick.Add([=](UI::EventParams &) {799auto mm = GetI18NCategory(I18NCat::MAINMENU);800System_InputBoxGetString(token_, mm->T("Enter Path"), path_.GetPath().ToString(), false, [=](const char *responseString, int responseValue) {801this->SetPath(Path(responseString));802});803return UI::EVENT_DONE;804});805}806#endif807} else {808topBar->Add(new Spacer(new LinearLayoutParams(FILL_PARENT, 64.0f, 1.0f)));809}810811if (browseFlags_ & BrowseFlags::HOMEBREW_STORE) {812topBar->Add(new Choice(mm->T("PPSSPP Homebrew Store"), new UI::LinearLayoutParams(WRAP_CONTENT, 64.0f)))->OnClick.Handle(this, &GameBrowser::OnHomebrewStore);813}814815ChoiceStrip *layoutChoice = topBar->Add(new ChoiceStrip(ORIENT_HORIZONTAL));816layoutChoice->AddChoice(ImageID("I_GRID"));817layoutChoice->AddChoice(ImageID("I_LINES"));818layoutChoice->SetSelection(*gridStyle_ ? 0 : 1, false);819layoutChoice->OnChoice.Handle(this, &GameBrowser::LayoutChange);820topBar->Add(new Choice(ImageID("I_ROTATE_LEFT"), new LayoutParams(64.0f, 64.0f)))->OnClick.Add([=](UI::EventParams &e) {821path_.Refresh();822Refresh();823return UI::EVENT_DONE;824});825topBar->Add(new Choice(ImageID("I_GEAR"), new LayoutParams(64.0f, 64.0f)))->OnClick.Handle(this, &GameBrowser::GridSettingsClick);826Add(topBar);827828if (*gridStyle_) {829gameList_ = new UI::GridLayoutList(UI::GridLayoutSettings(150*g_Config.fGameGridScale, 85*g_Config.fGameGridScale), new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT));830Add(gameList_);831} else {832UI::LinearLayout *gl = new UI::LinearLayoutList(UI::ORIENT_VERTICAL, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT));833gl->SetSpacing(4.0f);834gameList_ = gl;835Add(gameList_);836}837} else {838if (*gridStyle_) {839gameList_ = new UI::GridLayoutList(UI::GridLayoutSettings(150*g_Config.fGameGridScale, 85*g_Config.fGameGridScale), new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT));840} else {841UI::LinearLayout *gl = new UI::LinearLayout(UI::ORIENT_VERTICAL, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT));842gl->SetSpacing(4.0f);843gameList_ = gl;844}845// Until we can come up with a better space to put it (next to the tabs?) let's get rid of the icon config846// button on the Recent tab, it's ugly. You can use the button from the other tabs.847848// LinearLayout *gridOptionColumn = new LinearLayout(ORIENT_VERTICAL, new LinearLayoutParams(64.0, 64.0f));849// gridOptionColumn->Add(new Spacer(12.0));850// gridOptionColumn->Add(new Choice(ImageID("I_GEAR"), new LayoutParams(64.0f, 64.0f)))->OnClick.Handle(this, &GameBrowser::GridSettingsClick);851// LinearLayout *grid = new LinearLayout(ORIENT_HORIZONTAL, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT));852// gameList_->ReplaceLayoutParams(new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT, 0.75));853// grid->Add(gameList_);854// grid->Add(gridOptionColumn);855// Add(grid);856Add(gameList_);857}858859// Find games in the current directory and create new ones.860std::vector<DirButton *> dirButtons;861std::vector<GameButton *> gameButtons;862863listingPending_ = !path_.IsListingReady();864865// TODO: If listing failed, show a special error message.866867std::vector<Path> filenames;868if (HasSpecialFiles(filenames)) {869for (size_t i = 0; i < filenames.size(); i++) {870gameButtons.push_back(new GameButton(filenames[i], *gridStyle_, new UI::LinearLayoutParams(*gridStyle_ == true ? UI::WRAP_CONTENT : UI::FILL_PARENT, UI::WRAP_CONTENT)));871}872} else if (!listingPending_) {873std::vector<File::FileInfo> fileInfo;874path_.GetListing(fileInfo, "iso:cso:chd:pbp:elf:prx:ppdmp:");875for (size_t i = 0; i < fileInfo.size(); i++) {876bool isGame = !fileInfo[i].isDirectory;877bool isSaveData = false;878// Check if eboot directory879if (!isGame && path_.GetPath().size() >= 4 && IsValidPBP(path_.GetPath() / fileInfo[i].name / "EBOOT.PBP", true))880isGame = true;881else if (!isGame && File::Exists(path_.GetPath() / fileInfo[i].name / "PSP_GAME/SYSDIR"))882isGame = true;883else if (!isGame && File::Exists(path_.GetPath() / fileInfo[i].name / "PARAM.SFO"))884isSaveData = true;885886if (!isGame && !isSaveData) {887if (browseFlags_ & BrowseFlags::NAVIGATE) {888dirButtons.push_back(new DirButton(fileInfo[i].fullName, fileInfo[i].name, *gridStyle_, new UI::LinearLayoutParams(UI::FILL_PARENT, UI::FILL_PARENT)));889}890} else {891gameButtons.push_back(new GameButton(fileInfo[i].fullName, *gridStyle_, new UI::LinearLayoutParams(*gridStyle_ == true ? UI::WRAP_CONTENT : UI::FILL_PARENT, UI::WRAP_CONTENT)));892}893}894// Put RAR/ZIP files at the end to get them out of the way. They're only shown so that people895// can click them and get an explanation that they need to unpack them. This is necessary due896// to a flood of support email...897if (browseFlags_ & BrowseFlags::ARCHIVES) {898fileInfo.clear();899path_.GetListing(fileInfo, "zip:rar:r01:7z:");900if (!fileInfo.empty()) {901UI::LinearLayout *zl = new UI::LinearLayoutList(UI::ORIENT_VERTICAL, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT));902zl->SetSpacing(4.0f);903Add(zl);904for (size_t i = 0; i < fileInfo.size(); i++) {905if (!fileInfo[i].isDirectory) {906GameButton *b = zl->Add(new GameButton(fileInfo[i].fullName, false, new UI::LinearLayoutParams(UI::FILL_PARENT, UI::WRAP_CONTENT)));907b->OnClick.Handle(this, &GameBrowser::GameButtonClick);908b->SetHoldEnabled(false);909}910}911}912}913}914915if (browseFlags_ & BrowseFlags::NAVIGATE) {916if (path_.CanNavigateUp()) {917gameList_->Add(new DirButton(Path(std::string("..")), *gridStyle_, new UI::LinearLayoutParams(UI::FILL_PARENT, UI::FILL_PARENT)))->918OnClick.Handle(this, &GameBrowser::NavigateClick);919}920921// Add any pinned paths before other directories.922auto pinnedPaths = GetPinnedPaths();923for (auto it = pinnedPaths.begin(), end = pinnedPaths.end(); it != end; ++it) {924gameList_->Add(new DirButton(*it, GetBaseName((*it).ToString()), *gridStyle_, new UI::LinearLayoutParams(UI::FILL_PARENT, UI::FILL_PARENT)))->925OnClick.Handle(this, &GameBrowser::NavigateClick);926}927}928929if (listingPending_) {930gameList_->Add(new UI::TextView(mm->T("Loading..."), ALIGN_CENTER, false, new UI::LinearLayoutParams(UI::FILL_PARENT, UI::FILL_PARENT)));931}932933for (size_t i = 0; i < dirButtons.size(); i++) {934gameList_->Add(dirButtons[i])->OnClick.Handle(this, &GameBrowser::NavigateClick);935}936937for (size_t i = 0; i < gameButtons.size(); i++) {938GameButton *b = gameList_->Add(gameButtons[i]);939b->OnClick.Handle(this, &GameBrowser::GameButtonClick);940b->OnHoldClick.Handle(this, &GameBrowser::GameButtonHoldClick);941b->OnHighlight.Handle(this, &GameBrowser::GameButtonHighlight);942943if (!focusGamePath_.empty() && b->GamePath() == focusGamePath_) {944b->SetFocus();945}946}947948// Show a button to toggle pinning at the very end.949if ((browseFlags_ & BrowseFlags::PIN) && !path_.GetPath().empty()) {950std::string caption = IsCurrentPathPinned() ? "-" : "+";951if (!*gridStyle_) {952caption = IsCurrentPathPinned() ? mm->T("UnpinPath", "Unpin") : mm->T("PinPath", "Pin");953}954gameList_->Add(new UI::Button(caption, new UI::LinearLayoutParams(UI::FILL_PARENT, UI::FILL_PARENT)))->955OnClick.Handle(this, &GameBrowser::PinToggleClick);956}957958if (path_.GetPath().empty()) {959Add(new TextView(mm->T("UseBrowseOrLoad", "Use Browse to choose a folder, or Load to choose a file.")));960}961962if (!lastText_.empty()) {963Add(new Spacer());964Add(new Choice(lastText_, new UI::LinearLayoutParams(UI::WRAP_CONTENT, UI::WRAP_CONTENT)))->OnClick.Handle(this, &GameBrowser::LastClick);965}966}967968bool GameBrowser::IsCurrentPathPinned() {969const auto paths = g_Config.vPinnedPaths;970return std::find(paths.begin(), paths.end(), File::ResolvePath(path_.GetPath().ToString())) != paths.end();971}972973std::vector<Path> GameBrowser::GetPinnedPaths() const {974#ifndef _WIN32975static const std::string sepChars = "/";976#else977static const std::string sepChars = "/\\";978#endif979980const std::string currentPath = File::ResolvePath(path_.GetPath().ToString());981const std::vector<std::string> paths = g_Config.vPinnedPaths;982std::vector<Path> results;983for (size_t i = 0; i < paths.size(); ++i) {984// We want to exclude the current path, and its direct children.985if (paths[i] == currentPath) {986continue;987}988if (startsWith(paths[i], currentPath)) {989std::string descendant = paths[i].substr(currentPath.size());990// If there's only one separator (or none), its a direct child.991if (descendant.find_last_of(sepChars) == descendant.find_first_of(sepChars)) {992continue;993}994}995996results.push_back(Path(paths[i]));997}998return results;999}10001001std::string GameBrowser::GetBaseName(const std::string &path) const {1002#ifndef _WIN321003static const std::string sepChars = "/";1004#else1005static const std::string sepChars = "/\\";1006#endif10071008auto trailing = path.find_last_not_of(sepChars);1009if (trailing != path.npos) {1010size_t start = path.find_last_of(sepChars, trailing);1011if (start != path.npos) {1012return path.substr(start + 1, trailing - start);1013}1014return path.substr(0, trailing);1015}10161017size_t start = path.find_last_of(sepChars);1018if (start != path.npos) {1019return path.substr(start + 1);1020}1021return path;1022}10231024UI::EventReturn GameBrowser::GameButtonClick(UI::EventParams &e) {1025GameButton *button = static_cast<GameButton *>(e.v);1026UI::EventParams e2{};1027e2.s = button->GamePath().ToString();1028// Insta-update - here we know we are already on the right thread.1029OnChoice.Trigger(e2);1030return UI::EVENT_DONE;1031}10321033UI::EventReturn GameBrowser::GameButtonHoldClick(UI::EventParams &e) {1034GameButton *button = static_cast<GameButton *>(e.v);1035UI::EventParams e2{};1036e2.s = button->GamePath().ToString();1037// Insta-update - here we know we are already on the right thread.1038OnHoldChoice.Trigger(e2);1039return UI::EVENT_DONE;1040}10411042UI::EventReturn GameBrowser::GameButtonHighlight(UI::EventParams &e) {1043// Insta-update - here we know we are already on the right thread.1044OnHighlight.Trigger(e);1045return UI::EVENT_DONE;1046}10471048UI::EventReturn GameBrowser::NavigateClick(UI::EventParams &e) {1049DirButton *button = static_cast<DirButton *>(e.v);1050Path text = button->GetPath();1051if (button->PathAbsolute()) {1052path_.SetPath(text);1053} else {1054path_.Navigate(text.ToString());1055}1056g_Config.currentDirectory = path_.GetPath();1057Refresh();1058return UI::EVENT_DONE;1059}10601061UI::EventReturn GameBrowser::GridSettingsClick(UI::EventParams &e) {1062auto sy = GetI18NCategory(I18NCat::SYSTEM);1063auto gridSettings = new GridSettingsScreen(sy->T("Games list settings"));1064gridSettings->OnRecentChanged.Handle(this, &GameBrowser::OnRecentClear);1065if (e.v)1066gridSettings->SetPopupOrigin(e.v);10671068screenManager_->push(gridSettings);1069return UI::EVENT_DONE;1070}10711072UI::EventReturn GameBrowser::OnRecentClear(UI::EventParams &e) {1073screenManager_->RecreateAllViews();1074System_Notify(SystemNotification::UI);1075return UI::EVENT_DONE;1076}10771078UI::EventReturn GameBrowser::OnHomebrewStore(UI::EventParams &e) {1079screenManager_->push(new StoreScreen());1080return UI::EVENT_DONE;1081}10821083MainScreen::MainScreen() {1084g_BackgroundAudio.SetGame(Path());1085}10861087MainScreen::~MainScreen() {1088g_BackgroundAudio.SetGame(Path());1089}10901091void MainScreen::CreateViews() {1092// Information in the top left.1093// Back button to the bottom left.1094// Scrolling action menu to the right.1095using namespace UI;10961097bool vertical = UseVerticalLayout();10981099auto mm = GetI18NCategory(I18NCat::MAINMENU);11001101tabHolder_ = new TabHolder(ORIENT_HORIZONTAL, 64, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT, 1.0f));1102ViewGroup *leftColumn = tabHolder_;1103tabHolder_->SetTag("MainScreenGames");1104gameBrowsers_.clear();11051106tabHolder_->SetClip(true);11071108bool showRecent = g_Config.iMaxRecent > 0;1109bool hasStorageAccess = !System_GetPropertyBool(SYSPROP_SUPPORTS_PERMISSIONS) ||1110System_GetPermissionStatus(SYSTEM_PERMISSION_STORAGE) == PERMISSION_STATUS_GRANTED;1111bool storageIsTemporary = IsTempPath(GetSysDirectory(DIRECTORY_SAVEDATA)) && !confirmedTemporary_;1112if (showRecent && !hasStorageAccess) {1113showRecent = g_Config.HasRecentIsos();1114}11151116if (showRecent) {1117ScrollView *scrollRecentGames = new ScrollView(ORIENT_VERTICAL, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT));1118scrollRecentGames->SetTag("MainScreenRecentGames");1119GameBrowser *tabRecentGames = new GameBrowser(GetRequesterToken(),1120Path("!RECENT"), BrowseFlags::NONE, &g_Config.bGridView1, screenManager(), "", "",1121new LinearLayoutParams(FILL_PARENT, FILL_PARENT));1122scrollRecentGames->Add(tabRecentGames);1123gameBrowsers_.push_back(tabRecentGames);11241125tabHolder_->AddTab(mm->T("Recent"), scrollRecentGames);1126tabRecentGames->OnChoice.Handle(this, &MainScreen::OnGameSelectedInstant);1127tabRecentGames->OnHoldChoice.Handle(this, &MainScreen::OnGameSelected);1128tabRecentGames->OnHighlight.Handle(this, &MainScreen::OnGameHighlight);1129}11301131Button *focusButton = nullptr;1132if (hasStorageAccess) {1133scrollAllGames_ = new ScrollView(ORIENT_VERTICAL, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT));1134scrollAllGames_->SetTag("MainScreenAllGames");1135ScrollView *scrollHomebrew = new ScrollView(ORIENT_VERTICAL, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT));1136scrollHomebrew->SetTag("MainScreenHomebrew");11371138#if PPSSPP_PLATFORM(IOS)1139std::string_view getGamesUri = "https://www.ppsspp.org/getgames_ios";1140std::string_view getHomebrewUri = "https://www.ppsspp.org/gethomebrew_ios";1141#else1142std::string_view getGamesUri = "https://www.ppsspp.org/getgames";1143std::string_view getHomebrewUri = "https://www.ppsspp.org/gethomebrew";1144#endif11451146GameBrowser *tabAllGames = new GameBrowser(GetRequesterToken(), Path(g_Config.currentDirectory), BrowseFlags::STANDARD, &g_Config.bGridView2, screenManager(),1147mm->T("How to get games"), getGamesUri,1148new LinearLayoutParams(FILL_PARENT, FILL_PARENT));1149GameBrowser *tabHomebrew = new GameBrowser(GetRequesterToken(), GetSysDirectory(DIRECTORY_GAME), BrowseFlags::HOMEBREW_STORE, &g_Config.bGridView3, screenManager(),1150mm->T("How to get homebrew & demos", "How to get homebrew && demos"), getHomebrewUri,1151new LinearLayoutParams(FILL_PARENT, FILL_PARENT));11521153scrollAllGames_->Add(tabAllGames);1154gameBrowsers_.push_back(tabAllGames);1155scrollHomebrew->Add(tabHomebrew);1156gameBrowsers_.push_back(tabHomebrew);11571158tabHolder_->AddTab(mm->T("Games"), scrollAllGames_);1159tabHolder_->AddTab(mm->T("Homebrew & Demos"), scrollHomebrew);1160scrollAllGames_->RememberPosition(&g_Config.fGameListScrollPosition);11611162tabAllGames->OnChoice.Handle(this, &MainScreen::OnGameSelectedInstant);1163tabHomebrew->OnChoice.Handle(this, &MainScreen::OnGameSelectedInstant);11641165tabAllGames->OnHoldChoice.Handle(this, &MainScreen::OnGameSelected);1166tabHomebrew->OnHoldChoice.Handle(this, &MainScreen::OnGameSelected);11671168tabAllGames->OnHighlight.Handle(this, &MainScreen::OnGameHighlight);1169tabHomebrew->OnHighlight.Handle(this, &MainScreen::OnGameHighlight);11701171if (g_Config.bRemoteTab && !g_Config.sLastRemoteISOServer.empty()) {1172auto ri = GetI18NCategory(I18NCat::REMOTEISO);11731174ScrollView *scrollRemote = new ScrollView(ORIENT_VERTICAL, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT));1175scrollRemote->SetTag("MainScreenRemote");11761177Path remotePath(FormatRemoteISOUrl(g_Config.sLastRemoteISOServer.c_str(), g_Config.iLastRemoteISOPort, RemoteSubdir().c_str()));11781179GameBrowser *tabRemote = new GameBrowser(GetRequesterToken(), remotePath, BrowseFlags::NAVIGATE, &g_Config.bGridView3, screenManager(),1180ri->T("Remote disc streaming"), "https://www.ppsspp.org/docs/reference/disc-streaming",1181new LinearLayoutParams(FILL_PARENT, FILL_PARENT));1182tabRemote->SetHomePath(remotePath);11831184scrollRemote->Add(tabRemote);1185gameBrowsers_.push_back(tabRemote);11861187tabHolder_->AddTab(ri->T("Remote disc streaming"), scrollRemote);11881189tabRemote->OnChoice.Handle(this, &MainScreen::OnGameSelectedInstant);1190tabRemote->OnHoldChoice.Handle(this, &MainScreen::OnGameSelected);1191tabRemote->OnHighlight.Handle(this, &MainScreen::OnGameHighlight);1192}11931194if (g_Config.HasRecentIsos()) {1195tabHolder_->SetCurrentTab(0, true);1196} else if (g_Config.iMaxRecent > 0) {1197tabHolder_->SetCurrentTab(1, true);1198}11991200if (backFromStore_ || showHomebrewTab) {1201tabHolder_->SetCurrentTab(2, true);1202backFromStore_ = false;1203showHomebrewTab = false;1204}12051206if (storageIsTemporary) {1207LinearLayout *buttonHolder = new LinearLayout(ORIENT_HORIZONTAL, new LinearLayoutParams(WRAP_CONTENT, WRAP_CONTENT));1208buttonHolder->Add(new Spacer(new LinearLayoutParams(1.0f)));1209focusButton = new Button(mm->T("SavesAreTemporaryIgnore", "Ignore warning"), new LinearLayoutParams(WRAP_CONTENT, WRAP_CONTENT));1210focusButton->SetPadding(32, 16);1211buttonHolder->Add(focusButton)->OnClick.Add([this](UI::EventParams &e) {1212confirmedTemporary_ = true;1213RecreateViews();1214return UI::EVENT_DONE;1215});1216buttonHolder->Add(new Spacer(new LinearLayoutParams(1.0f)));12171218leftColumn->Add(new Spacer(new LinearLayoutParams(0.1f)));1219leftColumn->Add(new TextView(mm->T("SavesAreTemporary", "PPSSPP saving in temporary storage"), ALIGN_HCENTER, false));1220leftColumn->Add(new TextView(mm->T("SavesAreTemporaryGuidance", "Extract PPSSPP somewhere to save permanently"), ALIGN_HCENTER, false));1221leftColumn->Add(new Spacer(10.0f));1222leftColumn->Add(buttonHolder);1223leftColumn->Add(new Spacer(new LinearLayoutParams(0.1f)));1224}1225} else {1226scrollAllGames_ = nullptr;1227if (!showRecent) {1228leftColumn = new LinearLayout(ORIENT_VERTICAL, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT, 1.0f));1229// Just so it's destroyed on recreate.1230leftColumn->Add(tabHolder_);1231tabHolder_->SetVisibility(V_GONE);1232}12331234LinearLayout *buttonHolder = new LinearLayout(ORIENT_HORIZONTAL, new LinearLayoutParams(WRAP_CONTENT, WRAP_CONTENT));1235buttonHolder->Add(new Spacer(new LinearLayoutParams(1.0f)));1236focusButton = new Button(mm->T("Give PPSSPP permission to access storage"), new LinearLayoutParams(WRAP_CONTENT, WRAP_CONTENT));1237focusButton->SetPadding(32, 16);1238buttonHolder->Add(focusButton)->OnClick.Handle(this, &MainScreen::OnAllowStorage);1239buttonHolder->Add(new Spacer(new LinearLayoutParams(1.0f)));12401241leftColumn->Add(new Spacer(new LinearLayoutParams(0.1f)));1242leftColumn->Add(buttonHolder);1243leftColumn->Add(new Spacer(10.0f));1244leftColumn->Add(new TextView(mm->T("PPSSPP can't load games or save right now"), ALIGN_HCENTER, false));1245leftColumn->Add(new Spacer(new LinearLayoutParams(0.1f)));1246}12471248ViewGroup *rightColumn = new ScrollView(ORIENT_VERTICAL);1249LinearLayout *rightColumnItems = new LinearLayout(ORIENT_VERTICAL, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT));1250rightColumnItems->SetSpacing(0.0f);1251rightColumn->Add(rightColumnItems);12521253char versionString[256];1254snprintf(versionString, sizeof(versionString), "%s", PPSSPP_GIT_VERSION);1255rightColumnItems->SetSpacing(0.0f);1256AnchorLayout *logos = new AnchorLayout(new AnchorLayoutParams(FILL_PARENT, 60.0f, false));1257if (System_GetPropertyBool(SYSPROP_APP_GOLD)) {1258logos->Add(new ImageView(ImageID("I_ICONGOLD"), "", IS_DEFAULT, new AnchorLayoutParams(64, 64, 0, 0, NONE, NONE, false)));1259} else {1260logos->Add(new ImageView(ImageID("I_ICON"), "", IS_DEFAULT, new AnchorLayoutParams(64, 64, 0, 0, NONE, NONE, false)));1261}1262logos->Add(new ImageView(ImageID("I_LOGO"), "PPSSPP", IS_DEFAULT, new AnchorLayoutParams(180, 64, 64, -5.0f, NONE, NONE, false)));12631264#if !defined(MOBILE_DEVICE)1265auto gr = GetI18NCategory(I18NCat::GRAPHICS);1266ImageID icon(g_Config.UseFullScreen() ? "I_RESTORE" : "I_FULLSCREEN");1267fullscreenButton_ = logos->Add(new Button(gr->T("FullScreen", "Full Screen"), icon, new AnchorLayoutParams(48, 48, NONE, 0, 0, NONE, false)));1268fullscreenButton_->SetIgnoreText(true);1269fullscreenButton_->OnClick.Handle(this, &MainScreen::OnFullScreenToggle);1270#endif12711272rightColumnItems->Add(logos);1273TextView *ver = rightColumnItems->Add(new TextView(versionString, new LinearLayoutParams(Margins(70, -10, 0, 4))));1274ver->SetSmall(true);1275ver->SetClip(false);12761277LinearLayout *rightColumnChoices = rightColumnItems;1278if (vertical) {1279ScrollView *rightColumnScroll = new ScrollView(ORIENT_HORIZONTAL);1280rightColumnChoices = new LinearLayout(ORIENT_HORIZONTAL);1281rightColumnScroll->Add(rightColumnChoices);1282rightColumnItems->Add(rightColumnScroll);1283}12841285if (System_GetPropertyBool(SYSPROP_HAS_FILE_BROWSER)) {1286rightColumnChoices->Add(new Choice(mm->T("Load", "Load...")))->OnClick.Handle(this, &MainScreen::OnLoadFile);1287}1288rightColumnChoices->Add(new Choice(mm->T("Game Settings", "Settings")))->OnClick.Handle(this, &MainScreen::OnGameSettings);1289rightColumnChoices->Add(new Choice(mm->T("Credits")))->OnClick.Handle(this, &MainScreen::OnCredits);12901291if (!vertical) {1292rightColumnChoices->Add(new Choice(mm->T("www.ppsspp.org")))->OnClick.Handle(this, &MainScreen::OnPPSSPPOrg);1293if (!System_GetPropertyBool(SYSPROP_APP_GOLD) && (System_GetPropertyInt(SYSPROP_DEVICE_TYPE) != DEVICE_TYPE_VR)) {1294Choice *gold = rightColumnChoices->Add(new Choice(mm->T("Buy PPSSPP Gold")));1295gold->OnClick.Handle(this, &MainScreen::OnSupport);1296gold->SetIcon(ImageID("I_ICONGOLD"), 0.5f);1297}1298}12991300rightColumnChoices->Add(new Spacer(25.0));1301#if !PPSSPP_PLATFORM(IOS_APP_STORE)1302// Officially, iOS apps should not have exit buttons. Remove it to maximize app store review chances.1303rightColumnChoices->Add(new Choice(mm->T("Exit")))->OnClick.Handle(this, &MainScreen::OnExit);1304#endif13051306if (vertical) {1307root_ = new LinearLayout(ORIENT_VERTICAL);1308rightColumn->ReplaceLayoutParams(new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT));1309leftColumn->ReplaceLayoutParams(new LinearLayoutParams(1.0f));1310root_->Add(rightColumn);1311root_->Add(leftColumn);1312} else {1313Margins actionMenuMargins(0, 10, 10, 0);1314root_ = new LinearLayout(ORIENT_HORIZONTAL);1315rightColumn->ReplaceLayoutParams(new LinearLayoutParams(300, FILL_PARENT, actionMenuMargins));1316root_->Add(leftColumn);1317root_->Add(rightColumn);1318}13191320if (focusButton) {1321root_->SetDefaultFocusView(focusButton);1322} else if (tabHolder_->GetVisibility() != V_GONE) {1323root_->SetDefaultFocusView(tabHolder_);1324}13251326root_->SetTag("mainroot");13271328upgradeBar_ = 0;1329if (!g_Config.upgradeMessage.empty()) {1330auto u = GetI18NCategory(I18NCat::UPGRADE);1331upgradeBar_ = new LinearLayout(ORIENT_HORIZONTAL, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT));13321333UI::Margins textMargins(10, 5);1334UI::Margins buttonMargins(0, 0);1335UI::Drawable solid(0xFFbd9939);1336upgradeBar_->SetBG(solid);1337upgradeBar_->Add(new TextView(std::string(u->T("New version of PPSSPP available")) + std::string(": ") + g_Config.upgradeVersion, new LinearLayoutParams(1.0f, textMargins)));1338#if PPSSPP_PLATFORM(ANDROID) || PPSSPP_PLATFORM(WINDOWS)1339upgradeBar_->Add(new Button(u->T("Download"), new LinearLayoutParams(buttonMargins)))->OnClick.Handle(this, &MainScreen::OnDownloadUpgrade);1340#else1341upgradeBar_->Add(new Button(u->T("Details"), new LinearLayoutParams(buttonMargins)))->OnClick.Handle(this, &MainScreen::OnDownloadUpgrade);1342#endif1343upgradeBar_->Add(new Button(u->T("Dismiss"), new LinearLayoutParams(buttonMargins)))->OnClick.Handle(this, &MainScreen::OnDismissUpgrade);13441345// Slip in under root_1346LinearLayout *newRoot = new LinearLayout(ORIENT_VERTICAL);1347newRoot->Add(root_);1348newRoot->Add(upgradeBar_);1349root_->ReplaceLayoutParams(new LinearLayoutParams(1.0));1350root_ = newRoot;1351}1352}13531354bool MainScreen::key(const KeyInput &touch) {1355if (touch.flags & KEY_DOWN) {1356if (touch.keyCode == NKCODE_CTRL_LEFT || touch.keyCode == NKCODE_CTRL_RIGHT)1357searchKeyModifier_ = true;1358if (touch.keyCode == NKCODE_F && searchKeyModifier_ && System_GetPropertyBool(SYSPROP_HAS_TEXT_INPUT_DIALOG)) {1359auto se = GetI18NCategory(I18NCat::SEARCH);1360System_InputBoxGetString(GetRequesterToken(), se->T("Search term"), searchFilter_, false, [&](const std::string &value, int) {1361searchFilter_ = StripSpaces(value);1362searchChanged_ = true;1363});1364}1365} else if (touch.flags & KEY_UP) {1366if (touch.keyCode == NKCODE_CTRL_LEFT || touch.keyCode == NKCODE_CTRL_RIGHT)1367searchKeyModifier_ = false;1368}13691370return UIScreenWithBackground::key(touch);1371}13721373UI::EventReturn MainScreen::OnAllowStorage(UI::EventParams &e) {1374System_AskForPermission(SYSTEM_PERMISSION_STORAGE);1375return UI::EVENT_DONE;1376}13771378UI::EventReturn MainScreen::OnDownloadUpgrade(UI::EventParams &e) {1379#if PPSSPP_PLATFORM(ANDROID)1380// Go to app store1381if (System_GetPropertyBool(SYSPROP_APP_GOLD)) {1382System_LaunchUrl(LaunchUrlType::BROWSER_URL, "market://details?id=org.ppsspp.ppssppgold");1383} else {1384System_LaunchUrl(LaunchUrlType::BROWSER_URL, "market://details?id=org.ppsspp.ppsspp");1385}1386#elif PPSSPP_PLATFORM(WINDOWS)1387System_LaunchUrl(LaunchUrlType::BROWSER_URL, "https://www.ppsspp.org/download");1388#else1389// Go directly to ppsspp.org and let the user sort it out1390// (for details and in case downloads doesn't have their platform.)1391System_LaunchUrl(LaunchUrlType::BROWSER_URL, "https://www.ppsspp.org/");1392#endif1393return UI::EVENT_DONE;1394}13951396UI::EventReturn MainScreen::OnDismissUpgrade(UI::EventParams &e) {1397g_Config.DismissUpgrade();1398upgradeBar_->SetVisibility(UI::V_GONE);1399return UI::EVENT_DONE;1400}14011402void MainScreen::sendMessage(UIMessage message, const char *value) {1403// Always call the base class method first to handle the most common messages.1404UIScreenWithBackground::sendMessage(message, value);14051406if (message == UIMessage::REQUEST_GAME_BOOT) {1407if (screenManager()->topScreen() == this) {1408LaunchFile(screenManager(), Path(std::string(value)));1409}1410} else if (message == UIMessage::PERMISSION_GRANTED && !strcmp(value, "storage")) {1411RecreateViews();1412}1413}14141415void MainScreen::update() {1416UIScreen::update();1417UpdateUIState(UISTATE_MENU);14181419if (searchChanged_) {1420for (auto browser : gameBrowsers_)1421browser->ApplySearchFilter(searchFilter_);1422searchChanged_ = false;1423}1424}14251426UI::EventReturn MainScreen::OnLoadFile(UI::EventParams &e) {1427if (System_GetPropertyBool(SYSPROP_HAS_FILE_BROWSER)) {1428auto mm = GetI18NCategory(I18NCat::MAINMENU);1429System_BrowseForFile(GetRequesterToken(), mm->T("Load"), BrowseFileType::BOOTABLE, [](const std::string &value, int) {1430System_PostUIMessage(UIMessage::REQUEST_GAME_BOOT, value);1431});1432}1433return UI::EVENT_DONE;1434}14351436UI::EventReturn MainScreen::OnFullScreenToggle(UI::EventParams &e) {1437if (g_Config.iForceFullScreen != -1)1438g_Config.bFullScreen = g_Config.UseFullScreen();1439if (fullscreenButton_) {1440fullscreenButton_->SetImageID(ImageID(!g_Config.UseFullScreen() ? "I_RESTORE" : "I_FULLSCREEN"));1441}1442#if !defined(MOBILE_DEVICE)1443g_Config.bFullScreen = !g_Config.bFullScreen;1444System_ToggleFullscreenState("");1445#endif1446return UI::EVENT_DONE;1447}14481449void MainScreen::DrawBackground(UIContext &dc) {1450if (highlightedGamePath_.empty() && prevHighlightedGamePath_.empty()) {1451return;1452}14531454if (DrawBackgroundFor(dc, prevHighlightedGamePath_, 1.0f - prevHighlightProgress_)) {1455if (prevHighlightProgress_ < 1.0f) {1456prevHighlightProgress_ += 1.0f / 20.0f;1457}1458}1459if (!highlightedGamePath_.empty()) {1460if (DrawBackgroundFor(dc, highlightedGamePath_, highlightProgress_)) {1461if (highlightProgress_ < 1.0f) {1462highlightProgress_ += 1.0f / 20.0f;1463}1464}1465}1466}14671468bool MainScreen::DrawBackgroundFor(UIContext &dc, const Path &gamePath, float progress) {1469dc.Flush();14701471std::shared_ptr<GameInfo> ginfo;1472if (!gamePath.empty()) {1473ginfo = g_gameInfoCache->GetInfo(dc.GetDrawContext(), gamePath, GameInfoFlags::BG);1474// Loading texture data may bind a texture.1475dc.RebindTexture();14761477// Let's not bother if there's no picture.1478if (!ginfo->Ready(GameInfoFlags::BG) || (!ginfo->pic1.texture && !ginfo->pic0.texture)) {1479return false;1480}1481} else {1482return false;1483}14841485auto pic = ginfo->GetBGPic();1486Draw::Texture *texture = pic ? pic->texture : nullptr;14871488uint32_t color = whiteAlpha(ease(progress)) & 0xFFc0c0c0;1489if (texture) {1490dc.GetDrawContext()->BindTexture(0, texture);1491dc.Draw()->DrawTexRect(dc.GetBounds(), 0, 0, 1, 1, color);1492dc.Flush();1493dc.RebindTexture();1494}1495return true;1496}14971498UI::EventReturn MainScreen::OnGameSelected(UI::EventParams &e) {1499g_Config.Save("MainScreen::OnGameSelected");1500Path path(e.s);1501std::shared_ptr<GameInfo> ginfo = g_gameInfoCache->GetInfo(nullptr, path, GameInfoFlags::FILE_TYPE);1502if (ginfo->fileType == IdentifiedFileType::PSP_SAVEDATA_DIRECTORY) {1503return UI::EVENT_DONE;1504}1505if (g_GameManager.GetState() == GameManagerState::INSTALLING)1506return UI::EVENT_DONE;15071508// Restore focus if it was highlighted (e.g. by gamepad.)1509restoreFocusGamePath_ = highlightedGamePath_;1510g_BackgroundAudio.SetGame(path);1511lockBackgroundAudio_ = true;1512screenManager()->push(new GameScreen(path, false));1513return UI::EVENT_DONE;1514}15151516UI::EventReturn MainScreen::OnGameHighlight(UI::EventParams &e) {1517using namespace UI;15181519Path path(e.s);15201521// Don't change when re-highlighting what's already highlighted.1522if (path != highlightedGamePath_ || e.a == FF_LOSTFOCUS) {1523if (!highlightedGamePath_.empty()) {1524if (prevHighlightedGamePath_.empty() || prevHighlightProgress_ >= 0.75f) {1525prevHighlightedGamePath_ = highlightedGamePath_;1526prevHighlightProgress_ = 1.0 - highlightProgress_;1527}1528highlightedGamePath_.clear();1529}1530if (e.a == FF_GOTFOCUS) {1531highlightedGamePath_ = path;1532highlightProgress_ = 0.0f;1533}1534}15351536if ((!highlightedGamePath_.empty() || e.a == FF_LOSTFOCUS) && !lockBackgroundAudio_) {1537g_BackgroundAudio.SetGame(highlightedGamePath_);1538}15391540lockBackgroundAudio_ = false;1541return UI::EVENT_DONE;1542}15431544UI::EventReturn MainScreen::OnGameSelectedInstant(UI::EventParams &e) {1545// TODO: This is really not necessary here in all cases.1546g_Config.Save("MainScreen::OnGameSelectedInstant");1547ScreenManager *screen = screenManager();1548LaunchFile(screen, Path(e.s));1549return UI::EVENT_DONE;1550}15511552UI::EventReturn MainScreen::OnGameSettings(UI::EventParams &e) {1553screenManager()->push(new GameSettingsScreen(Path(), ""));1554return UI::EVENT_DONE;1555}15561557UI::EventReturn MainScreen::OnCredits(UI::EventParams &e) {1558screenManager()->push(new CreditsScreen());1559return UI::EVENT_DONE;1560}15611562UI::EventReturn MainScreen::OnSupport(UI::EventParams &e) {1563#if PPSSPP_PLATFORM(IOS_APP_STORE)1564System_LaunchUrl(LaunchUrlType::BROWSER_URL, "https://apps.apple.com/us/app/ppsspp-gold-psp-emulator/id6502287918");1565#elif PPSSPP_PLATFORM(ANDROID)1566System_LaunchUrl(LaunchUrlType::BROWSER_URL, "market://details?id=org.ppsspp.ppssppgold");1567#else1568System_LaunchUrl(LaunchUrlType::BROWSER_URL, "https://www.ppsspp.org/buygold");1569#endif1570return UI::EVENT_DONE;1571}15721573UI::EventReturn MainScreen::OnPPSSPPOrg(UI::EventParams &e) {1574System_LaunchUrl(LaunchUrlType::BROWSER_URL, "https://www.ppsspp.org");1575return UI::EVENT_DONE;1576}15771578UI::EventReturn MainScreen::OnForums(UI::EventParams &e) {1579System_LaunchUrl(LaunchUrlType::BROWSER_URL, "https://forums.ppsspp.org");1580return UI::EVENT_DONE;1581}15821583UI::EventReturn MainScreen::OnExit(UI::EventParams &e) {1584// Let's make sure the config was saved, since it may not have been.1585if (!g_Config.Save("MainScreen::OnExit")) {1586System_Toast("Failed to save settings!\nCheck permissions, or try to restart the device.");1587}15881589// Request the framework to exit cleanly.1590System_ExitApp();15911592UpdateUIState(UISTATE_EXIT);1593return UI::EVENT_DONE;1594}15951596void MainScreen::dialogFinished(const Screen *dialog, DialogResult result) {1597std::string tag = dialog->tag();1598if (tag == "Store") {1599backFromStore_ = true;1600RecreateViews();1601}1602if (tag == "Game") {1603if (!restoreFocusGamePath_.empty() && UI::IsFocusMovementEnabled()) {1604// Prevent the background from fading, since we just were displaying it.1605highlightedGamePath_ = restoreFocusGamePath_;1606highlightProgress_ = 1.0f;16071608// Refocus the game button itself.1609int tab = tabHolder_->GetCurrentTab();1610if (tab >= 0 && tab < (int)gameBrowsers_.size()) {1611gameBrowsers_[tab]->FocusGame(restoreFocusGamePath_);1612}16131614// Don't get confused next time.1615restoreFocusGamePath_.clear();1616} else {1617// Not refocusing, so we need to stop the audio.1618g_BackgroundAudio.SetGame(Path());1619}1620}1621if (tag == "InstallZip") {1622INFO_LOG(Log::System, "InstallZip finished, refreshing");1623if (gameBrowsers_.size() >= 2) {1624gameBrowsers_[1]->RequestRefresh();1625}1626}1627}16281629void UmdReplaceScreen::CreateViews() {1630using namespace UI;1631Margins actionMenuMargins(0, 100, 15, 0);1632auto mm = GetI18NCategory(I18NCat::MAINMENU);1633auto di = GetI18NCategory(I18NCat::DIALOG);16341635TabHolder *leftColumn = new TabHolder(ORIENT_HORIZONTAL, 64, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT, 1.0));1636leftColumn->SetTag("UmdReplace");1637leftColumn->SetClip(true);16381639ViewGroup *rightColumn = new ScrollView(ORIENT_VERTICAL, new LinearLayoutParams(270, FILL_PARENT, actionMenuMargins));1640LinearLayout *rightColumnItems = new LinearLayout(ORIENT_VERTICAL, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT));1641rightColumnItems->SetSpacing(0.0f);1642rightColumn->Add(rightColumnItems);16431644if (g_Config.iMaxRecent > 0) {1645ScrollView *scrollRecentGames = new ScrollView(ORIENT_VERTICAL, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT));1646scrollRecentGames->SetTag("UmdReplaceRecentGames");1647GameBrowser *tabRecentGames = new GameBrowser(GetRequesterToken(),1648Path("!RECENT"), BrowseFlags::NONE, &g_Config.bGridView1, screenManager(), "", "",1649new LinearLayoutParams(FILL_PARENT, FILL_PARENT));1650scrollRecentGames->Add(tabRecentGames);1651leftColumn->AddTab(mm->T("Recent"), scrollRecentGames);1652tabRecentGames->OnChoice.Handle(this, &UmdReplaceScreen::OnGameSelected);1653tabRecentGames->OnHoldChoice.Handle(this, &UmdReplaceScreen::OnGameSelected);1654}1655ScrollView *scrollAllGames = new ScrollView(ORIENT_VERTICAL, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT));1656scrollAllGames->SetTag("UmdReplaceAllGames");16571658GameBrowser *tabAllGames = new GameBrowser(GetRequesterToken(), Path(g_Config.currentDirectory), BrowseFlags::STANDARD, &g_Config.bGridView2, screenManager(),1659mm->T("How to get games"), "https://www.ppsspp.org/getgames.html",1660new LinearLayoutParams(FILL_PARENT, FILL_PARENT));16611662scrollAllGames->Add(tabAllGames);16631664leftColumn->AddTab(mm->T("Games"), scrollAllGames);16651666tabAllGames->OnChoice.Handle(this, &UmdReplaceScreen::OnGameSelected);16671668tabAllGames->OnHoldChoice.Handle(this, &UmdReplaceScreen::OnGameSelected);16691670if (System_GetPropertyBool(SYSPROP_HAS_FILE_BROWSER)) {1671rightColumnItems->Add(new Choice(mm->T("Load", "Load...")))->OnClick.Add([&](UI::EventParams &e) {1672auto mm = GetI18NCategory(I18NCat::MAINMENU);1673System_BrowseForFile(GetRequesterToken(), mm->T("Load"), BrowseFileType::BOOTABLE, [&](const std::string &value, int) {1674__UmdReplace(Path(value));1675TriggerFinish(DR_OK);1676});1677return EVENT_DONE;1678});1679}16801681rightColumnItems->Add(new Choice(di->T("Cancel")))->OnClick.Handle<UIScreen>(this, &UIScreen::OnCancel);1682rightColumnItems->Add(new Spacer());1683rightColumnItems->Add(new Choice(mm->T("Game Settings")))->OnClick.Handle(this, &UmdReplaceScreen::OnGameSettings);16841685if (g_Config.HasRecentIsos()) {1686leftColumn->SetCurrentTab(0, true);1687} else if (g_Config.iMaxRecent > 0) {1688leftColumn->SetCurrentTab(1, true);1689}16901691root_ = new LinearLayout(ORIENT_HORIZONTAL);1692root_->Add(leftColumn);1693root_->Add(rightColumn);1694}16951696void UmdReplaceScreen::update() {1697UpdateUIState(UISTATE_PAUSEMENU);1698UIScreen::update();1699}17001701UI::EventReturn UmdReplaceScreen::OnGameSelected(UI::EventParams &e) {1702__UmdReplace(Path(e.s));1703TriggerFinish(DR_OK);1704return UI::EVENT_DONE;1705}17061707UI::EventReturn UmdReplaceScreen::OnGameSettings(UI::EventParams &e) {1708screenManager()->push(new GameSettingsScreen(Path()));1709return UI::EVENT_DONE;1710}17111712void GridSettingsScreen::CreatePopupContents(UI::ViewGroup *parent) {1713using namespace UI;17141715auto di = GetI18NCategory(I18NCat::DIALOG);1716auto sy = GetI18NCategory(I18NCat::SYSTEM);17171718ScrollView *scroll = new ScrollView(ORIENT_VERTICAL, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT, 1.0f));1719LinearLayout *items = new LinearLayoutList(ORIENT_VERTICAL);17201721items->Add(new CheckBox(&g_Config.bGridView1, sy->T("Display Recent on a grid")));1722items->Add(new CheckBox(&g_Config.bGridView2, sy->T("Display Games on a grid")));1723items->Add(new CheckBox(&g_Config.bGridView3, sy->T("Display Homebrew on a grid")));17241725items->Add(new ItemHeader(sy->T("Grid icon size")));1726items->Add(new Choice(sy->T("Increase size")))->OnClick.Handle(this, &GridSettingsScreen::GridPlusClick);1727items->Add(new Choice(sy->T("Decrease size")))->OnClick.Handle(this, &GridSettingsScreen::GridMinusClick);17281729items->Add(new ItemHeader(sy->T("Display Extra Info")));1730items->Add(new CheckBox(&g_Config.bShowIDOnGameIcon, sy->T("Show ID")));1731items->Add(new CheckBox(&g_Config.bShowRegionOnGameIcon, sy->T("Show region flag")));17321733if (g_Config.iMaxRecent > 0) {1734items->Add(new ItemHeader(sy->T("Clear Recent")));1735items->Add(new Choice(sy->T("Clear Recent Games List")))->OnClick.Handle(this, &GridSettingsScreen::OnRecentClearClick);1736}17371738scroll->Add(items);1739parent->Add(scroll);1740}17411742UI::EventReturn GridSettingsScreen::GridPlusClick(UI::EventParams &e) {1743g_Config.fGameGridScale = std::min(g_Config.fGameGridScale*1.25f, MAX_GAME_GRID_SCALE);1744return UI::EVENT_DONE;1745}17461747UI::EventReturn GridSettingsScreen::GridMinusClick(UI::EventParams &e) {1748g_Config.fGameGridScale = std::max(g_Config.fGameGridScale/1.25f, MIN_GAME_GRID_SCALE);1749return UI::EVENT_DONE;1750}17511752UI::EventReturn GridSettingsScreen::OnRecentClearClick(UI::EventParams &e) {1753g_Config.ClearRecentIsos();1754OnRecentChanged.Trigger(e);1755return UI::EVENT_DONE;1756}175717581759