mirror of
https://github.com/hrydgard/ppsspp.git
synced 2026-09-22 20:36:22 +02:00
It used to return the path unchanged when the path didn't end in oldExtension, so a caller that guessed wrong silently went on using the original file - and "the screenshot next to this savestate" quietly becomes "this savestate". Every caller had to know to check the extension first, and most didn't. Now it's [[nodiscard]] bool with an out-param, in the style of ComputePathTo next door, so the mismatch has to be handled. Changing the signature rather than the behaviour means no call site can keep the old assumption by accident. All four callers wanted "skip it" or "fall back", which they now say out loud. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
715 lines
24 KiB
C++
715 lines
24 KiB
C++
// Copyright (c) 2013- PPSSPP Project.
|
|
|
|
// This program is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU General Public License as published by
|
|
// the Free Software Foundation, version 2.0 or later versions.
|
|
|
|
// This program is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU General Public License 2.0 for more details.
|
|
|
|
// A copy of the GPL 2.0 should have been included with the program.
|
|
// If not, see http://www.gnu.org/licenses/
|
|
|
|
// Official git repository and contact information can be found at
|
|
// https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/.
|
|
|
|
#include <algorithm>
|
|
#include <functional>
|
|
|
|
#include "Common/Data/Color/RGBAUtil.h"
|
|
#include "Common/Render/DrawBuffer.h"
|
|
#include "Common/Data/Encoding/Utf8.h"
|
|
#include "Common/Data/Text/I18n.h"
|
|
#include "Common/Math/curves.h"
|
|
#include "Common/Data/Text/Parsers.h"
|
|
#include "Common/System/NativeApp.h"
|
|
#include "Common/System/Request.h"
|
|
#include "Common/UI/Context.h"
|
|
#include "Common/UI/View.h"
|
|
#include "Common/UI/PopupScreens.h"
|
|
#include "Common/UI/ViewGroup.h"
|
|
#include "Common/UI/AsyncImageFileView.h"
|
|
#include "Common/UI/ScreenManager.h"
|
|
#include "UI/SavedataScreen.h"
|
|
#include "UI/MainScreen.h"
|
|
#include "UI/GameInfoCache.h"
|
|
#include "UI/PauseScreen.h"
|
|
#include "UI/MiscScreens.h"
|
|
|
|
#include "Common/File/FileUtil.h"
|
|
#include "Common/TimeUtil.h"
|
|
#include "Common/StringUtils.h"
|
|
#include "Core/Config.h"
|
|
#include "Core/Loaders.h"
|
|
#include "Core/SaveState.h"
|
|
#include "Core/System.h"
|
|
#include "Core/HLE/sceUtility.h"
|
|
#include "UI/MiscViews.h"
|
|
|
|
class SavedataButton;
|
|
|
|
SavedataView::SavedataView(UIContext &dc, const Path &savePath, IdentifiedFileType type, std::string_view title, std::string_view savedataTitle, std::string_view savedataDetail, std::string_view fileSize, std::string_view mtime, bool showIcon, UI::LayoutParams *layoutParams)
|
|
: LinearLayout(ORIENT_VERTICAL, layoutParams)
|
|
{
|
|
using namespace UI;
|
|
|
|
const Style &textStyle = dc.GetTheme().popupStyle;
|
|
LinearLayout *toprow = new LinearLayout(ORIENT_HORIZONTAL, new LayoutParams(FILL_PARENT, WRAP_CONTENT));
|
|
Add(toprow);
|
|
toprow->SetSpacing(0.0);
|
|
|
|
savedataTitle_ = nullptr;
|
|
fileSize_ = nullptr;
|
|
mTime_ = nullptr;
|
|
detail_ = nullptr;
|
|
if (type == IdentifiedFileType::PSP_SAVEDATA_DIRECTORY) {
|
|
if (showIcon) {
|
|
toprow->Add(new GameImageView(savePath, GameInfoFlags::ICON, 2.0f, new LinearLayoutParams(Margins(5, 5))));
|
|
}
|
|
// Contents to the right of the image:
|
|
LinearLayout *topright = new LinearLayout(ORIENT_VERTICAL, new LinearLayoutParams(WRAP_CONTENT, WRAP_CONTENT, 1.0f));
|
|
topright->SetSpacing(1.0f);
|
|
savedataTitle_ = topright->Add(new TextView(savedataTitle, ALIGN_LEFT | FLAG_WRAP_TEXT, false));
|
|
savedataTitle_->SetTextColor(textStyle.fgColor);
|
|
fileSize_ = topright->Add(new TextView(fileSize, 0, true));
|
|
fileSize_->SetTextColor(textStyle.fgColor);
|
|
mTime_ = topright->Add(new TextView(mtime, 0, true));
|
|
mTime_->SetTextColor(textStyle.fgColor);
|
|
toprow->Add(topright);
|
|
|
|
// Content below the image.
|
|
Add(new Spacer(3.0));
|
|
detail_ = Add(new TextView(SanitizeString(savedataDetail, StringRestriction::ConvertToUnixEndings), ALIGN_LEFT | FLAG_WRAP_TEXT, true, new LinearLayoutParams(Margins(10, 0))));
|
|
detail_->SetTextColor(textStyle.fgColor);
|
|
Add(new Spacer(3.0));
|
|
} else {
|
|
_dbg_assert_(type == IdentifiedFileType::PPSSPP_SAVESTATE);
|
|
Path image_path;
|
|
if (savePath.WithReplacedExtension(".ppst", ".jpg", &image_path) && File::Exists(image_path)) {
|
|
toprow->Add(new AsyncImageFileView(image_path, IS_KEEP_ASPECT, new LinearLayoutParams(480, 272, Margins(10, 0))));
|
|
} else {
|
|
auto sa = GetI18NCategory(I18NCat::SAVEDATA);
|
|
toprow->Add(new TextView(sa->T("No screenshot"), new LinearLayoutParams(Margins(10, 5))))->SetTextColor(textStyle.fgColor);
|
|
}
|
|
mTime_ = Add(new TextView(mtime, 0, true, new LinearLayoutParams(Margins(10, 5))));
|
|
mTime_->SetTextColor(textStyle.fgColor);
|
|
}
|
|
}
|
|
|
|
// TODO: This runs every frame, which is a bit silly.
|
|
void SavedataView::UpdateGame(GameInfo *ginfo) {
|
|
if (!ginfo->Ready(GameInfoFlags::PARAM_SFO | GameInfoFlags::SIZE)) {
|
|
return;
|
|
}
|
|
if (savedataTitle_) {
|
|
savedataTitle_->SetText(SanitizeString(ginfo->GetParamSFO().GetValueString("SAVEDATA_TITLE"), StringRestriction::NoLineBreaksOrSpecials));
|
|
}
|
|
if (detail_) {
|
|
detail_->SetText(SanitizeString(ginfo->GetParamSFO().GetValueString("SAVEDATA_DETAIL"), StringRestriction::ConvertToUnixEndings));
|
|
}
|
|
if (fileSize_) {
|
|
fileSize_->SetText(NiceSizeFormat(ginfo->gameSizeOnDisk));
|
|
}
|
|
if (mTime_) {
|
|
mTime_->SetText(ginfo->GetMTime());
|
|
}
|
|
}
|
|
|
|
SavedataView::SavedataView(UIContext &dc, GameInfo *ginfo, IdentifiedFileType type, bool showIcon, UI::LayoutParams *layoutParams)
|
|
: SavedataView(dc,
|
|
ginfo->GetFilePath(),
|
|
type,
|
|
"",
|
|
"",
|
|
"",
|
|
"",
|
|
"",
|
|
showIcon,
|
|
layoutParams) {
|
|
if (ginfo) {
|
|
UpdateGame(ginfo);
|
|
}
|
|
}
|
|
|
|
SavedataPopupScreen::SavedataPopupScreen(Path gamePath, Path savePath, std::string_view title) : PopupScreen(StripSpaces(title)), savePath_(savePath), gamePath_(gamePath) {}
|
|
|
|
void SavedataPopupScreen::update() {
|
|
PopupScreen::update();
|
|
std::shared_ptr<GameInfo> ginfo = g_gameInfoCache->GetInfo(screenManager()->getDrawContext(), savePath_, GameInfoFlags::PARAM_SFO | GameInfoFlags::ICON | GameInfoFlags::SIZE);
|
|
if (!ginfo->Ready(GameInfoFlags::PARAM_SFO)) {
|
|
// Hm, this is no good. But hopefully the previous screen loaded it.
|
|
return;
|
|
}
|
|
if (savedataView_) {
|
|
savedataView_->UpdateGame(ginfo.get());
|
|
}
|
|
}
|
|
|
|
void SavedataPopupScreen::CreatePopupContents(UI::ViewGroup *parent) {
|
|
using namespace UI;
|
|
UIContext &dc = *screenManager()->getUIContext();
|
|
|
|
std::shared_ptr<GameInfo> ginfo = g_gameInfoCache->GetInfo(screenManager()->getDrawContext(), savePath_, GameInfoFlags::PARAM_SFO | GameInfoFlags::ICON | GameInfoFlags::SIZE);
|
|
if (!ginfo->Ready(GameInfoFlags::PARAM_SFO)) {
|
|
// This is OK, handled in Update. Though most likely, the previous screen loaded it.
|
|
}
|
|
|
|
ScrollView *contentScroll = new ScrollView(ORIENT_VERTICAL, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT, 1.0f, UI::Margins(0, 3)));
|
|
parent->Add(contentScroll);
|
|
|
|
// TODO: If the game info wasn't already loaded, we'll get a bogus fileType here.
|
|
savedataView_ = contentScroll->Add(new SavedataView(dc, ginfo.get(), ginfo->fileType, true));
|
|
|
|
auto di = GetI18NCategory(I18NCat::DIALOG);
|
|
|
|
LinearLayout *buttonRow = new LinearLayout(ORIENT_HORIZONTAL, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT));
|
|
buttonRow->SetSpacing(0);
|
|
Margins buttonMargins(5, 5, 5, 13); // not sure why we need more at the bottom to make it look right
|
|
|
|
buttonRow->Add(new Choice(di->T("Back"), ImageID("I_NAVIGATE_BACK"), new LinearLayoutParams(1.0f, buttonMargins)))->OnClick.Handle<UIScreen>(this, &UIScreen::OnBack);
|
|
buttonRow->Add(new Choice(di->T("Delete"), ImageID("I_TRASHCAN"), new LinearLayoutParams(1.0f, buttonMargins)))->OnClick.Add([this](UI::EventParams &e) {
|
|
auto di = GetI18NCategory(I18NCat::DIALOG);
|
|
std::shared_ptr<GameInfo> ginfo = g_gameInfoCache->GetInfo(nullptr, savePath_, GameInfoFlags::PARAM_SFO);
|
|
|
|
const bool trashAvailable = System_GetPropertyBool(SYSPROP_HAS_TRASH_BIN);
|
|
|
|
std::string_view confirmMessage = di->T("Are you sure you want to delete the file?");
|
|
screenManager()->push(new MessagePopupScreen(di->T("Delete"), confirmMessage, trashAvailable ? di->T("Move to trash") : di->T("Delete"), di->T("Cancel"), [=](bool result) {
|
|
if (result) {
|
|
ginfo->Delete();
|
|
TriggerFinish(DR_NO);
|
|
}
|
|
}));
|
|
});
|
|
if (System_GetPropertyBool(SYSPROP_CAN_SHOW_FILE)) {
|
|
buttonRow->Add(new Choice(di->T("Show in folder"), ImageID("I_FOLDER"), new LinearLayoutParams(1.0f, buttonMargins)))->OnClick.Add([this](UI::EventParams &e) {
|
|
System_ShowFileInFolder(savePath_);
|
|
});
|
|
}
|
|
parent->Add(buttonRow);
|
|
}
|
|
|
|
class SortedLinearLayout : public UI::LinearLayoutList {
|
|
public:
|
|
typedef std::function<void(View *)> PrepFunc;
|
|
typedef std::function<bool(const View *, const View *)> CompareFunc;
|
|
|
|
SortedLinearLayout(Orientation orientation, UI::LayoutParams *layoutParams = nullptr)
|
|
: UI::LinearLayoutList(orientation, layoutParams) {
|
|
}
|
|
|
|
void SetCompare(const PrepFunc &prepFunc, const CompareFunc &lessFunc) {
|
|
prepIndex_ = 0;
|
|
prepFunc_ = prepFunc;
|
|
lessFunc_ = lessFunc;
|
|
}
|
|
|
|
void Update() override;
|
|
|
|
private:
|
|
size_t prepIndex_ = 0;
|
|
PrepFunc prepFunc_;
|
|
CompareFunc lessFunc_;
|
|
};
|
|
|
|
void SortedLinearLayout::Update() {
|
|
if (prepFunc_) {
|
|
// Try to avoid dropping more than a frame, prefer items shift.
|
|
constexpr double ALLOWED_TIME = 0.95 / 60.0;
|
|
double start_time = time_now_d();
|
|
for (; prepIndex_ < views_.size(); ++prepIndex_) {
|
|
prepFunc_(views_[prepIndex_]);
|
|
if (time_now_d() > start_time + ALLOWED_TIME) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (lessFunc_) {
|
|
// We may sort several times while calculating.
|
|
std::stable_sort(views_.begin(), views_.end(), lessFunc_);
|
|
}
|
|
// We're done if we got through all items.
|
|
if (prepIndex_ >= views_.size()) {
|
|
prepFunc_ = PrepFunc();
|
|
lessFunc_ = CompareFunc();
|
|
}
|
|
|
|
UI::LinearLayout::Update();
|
|
}
|
|
|
|
void SavedataButton::UpdateTotalSize() {
|
|
if (hasTotalSize_)
|
|
return;
|
|
|
|
File::FileInfo info;
|
|
if (File::GetFileInfo(savePath_, &info)) {
|
|
totalSize_ = info.size;
|
|
if (info.isDirectory)
|
|
totalSize_ = File::ComputeRecursiveDirectorySize(savePath_);
|
|
}
|
|
|
|
hasTotalSize_ = true;
|
|
}
|
|
|
|
void SavedataButton::UpdateDateSeconds() {
|
|
if (hasDateSeconds_)
|
|
return;
|
|
|
|
File::FileInfo info;
|
|
if (File::GetFileInfo(savePath_, &info)) {
|
|
dateSeconds_ = info.mtime;
|
|
if (info.isDirectory && File::GetFileInfo(savePath_ / "PARAM.SFO", &info)) {
|
|
dateSeconds_ = info.mtime;
|
|
}
|
|
}
|
|
|
|
hasDateSeconds_ = true;
|
|
}
|
|
|
|
bool SavedataButton::UpdateText() {
|
|
std::shared_ptr<GameInfo> ginfo = g_gameInfoCache->GetInfo(nullptr, savePath_, GameInfoFlags::PARAM_SFO);
|
|
if (ginfo->Ready(GameInfoFlags::PARAM_SFO)) {
|
|
UpdateText(ginfo);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void SavedataButton::UpdateText(const std::shared_ptr<GameInfo> &ginfo) {
|
|
const std::string currentTitle = ginfo->GetTitle();
|
|
if (!currentTitle.empty()) {
|
|
title_ = SanitizeString(currentTitle, StringRestriction::NoLineBreaksOrSpecials);
|
|
}
|
|
if (subtitle_.empty() && ginfo->gameSizeOnDisk > 0) {
|
|
std::string date = ginfo->GetMTime();
|
|
std::string savedata_title = ginfo->GetParamSFO().GetValueString("SAVEDATA_TITLE");
|
|
subtitle_ = SanitizeString(savedata_title, StringRestriction::NoLineBreaksOrSpecials) + " (" + NiceSizeFormat(ginfo->gameSizeOnDisk) + ", " + date + ")";
|
|
}
|
|
}
|
|
|
|
void SavedataButton::Draw(UIContext &dc) {
|
|
std::shared_ptr<GameInfo> ginfo = g_gameInfoCache->GetInfo(dc.GetDrawContext(), savePath_, GameInfoFlags::ICON | GameInfoFlags::PARAM_SFO | GameInfoFlags::SIZE);
|
|
Draw::Texture *texture = 0;
|
|
u32 color = 0, shadowColor = 0;
|
|
using namespace UI;
|
|
|
|
if (ginfo->Ready(GameInfoFlags::ICON) && ginfo->icon.texture) {
|
|
texture = ginfo->icon.texture;
|
|
}
|
|
|
|
int x = bounds_.x;
|
|
int y = bounds_.y;
|
|
int w = 144;
|
|
int h = bounds_.h;
|
|
|
|
UI::Style style = dc.GetTheme().itemStyle;
|
|
if (down_)
|
|
style = dc.GetTheme().itemDownStyle;
|
|
|
|
h = bounds_.h;
|
|
if (HasFocus())
|
|
style = down_ ? dc.GetTheme().itemDownStyle : dc.GetTheme().itemFocusedStyle;
|
|
|
|
Drawable bg = style.background;
|
|
|
|
dc.Draw()->Flush();
|
|
dc.RebindTexture();
|
|
dc.FillRect(bg, bounds_);
|
|
dc.Draw()->Flush();
|
|
|
|
if (texture) {
|
|
color = whiteAlpha(ease((time_now_d() - ginfo->icon.timeLoaded) * 2));
|
|
shadowColor = blackAlpha(ease((time_now_d() - ginfo->icon.timeLoaded) * 2));
|
|
float tw = texture->Width();
|
|
float th = texture->Height();
|
|
|
|
// Adjust position so we don't stretch the image vertically or horizontally.
|
|
// TODO: Add a param to specify fit? The below assumes it's never too wide.
|
|
float nw = h * tw / th;
|
|
x += (w - nw) / 2.0f;
|
|
w = nw;
|
|
}
|
|
|
|
// int txOffset = down_ ? 4 : 0;
|
|
int txOffset = 0;
|
|
|
|
Bounds overlayBounds = bounds_;
|
|
|
|
// Render button
|
|
int dropsize = 10;
|
|
if (texture) {
|
|
if (txOffset) {
|
|
dropsize = 3;
|
|
y += txOffset * 2;
|
|
overlayBounds.y += txOffset * 2;
|
|
}
|
|
if (HasFocus()) {
|
|
dc.Draw()->Flush();
|
|
dc.RebindTexture();
|
|
float pulse = sin(time_now_d() * 7.0) * 0.25 + 0.8;
|
|
dc.Draw()->DrawImage4Grid(dc.GetTheme().dropShadow4Grid, x - dropsize*1.5f, y - dropsize*1.5f, x + w + dropsize*1.5f, y + h + dropsize*1.5f, alphaMul(color, pulse), 1.0f);
|
|
dc.Draw()->Flush();
|
|
} else {
|
|
dc.Draw()->Flush();
|
|
dc.RebindTexture();
|
|
dc.Draw()->DrawImage4Grid(dc.GetTheme().dropShadow4Grid, x - dropsize, y - dropsize*0.5f, x + w + dropsize, y + h + dropsize*1.5, alphaMul(shadowColor, 0.5f), 1.0f);
|
|
dc.Draw()->Flush();
|
|
}
|
|
}
|
|
|
|
if (texture) {
|
|
dc.Draw()->Flush();
|
|
dc.GetDrawContext()->BindTexture(0, texture);
|
|
dc.Draw()->DrawTexRect(x, y, x + w, y + h, 0, 0, 1, 1, color);
|
|
dc.Draw()->Flush();
|
|
}
|
|
|
|
dc.Draw()->Flush();
|
|
dc.RebindTexture();
|
|
dc.SetFontStyle(dc.GetTheme().uiFont);
|
|
|
|
float tw, th;
|
|
dc.Draw()->Flush();
|
|
dc.PushScissor(bounds_);
|
|
|
|
UpdateText(ginfo);
|
|
dc.MeasureText(dc.GetFontStyle(), 1.0f, 1.0f, title_, &tw, &th, 0);
|
|
|
|
int availableWidth = bounds_.w - 150;
|
|
float sineWidth = std::max(0.0f, (tw - availableWidth)) / 2.0f;
|
|
|
|
float tx = 150.0f;
|
|
if (availableWidth < tw) {
|
|
float overageRatio = 1.5f * availableWidth * 1.0f / tw;
|
|
tx -= (1.0f + sin(time_now_d() * overageRatio)) * sineWidth;
|
|
Bounds tb = bounds_;
|
|
tb.x = bounds_.x + 150.0f;
|
|
tb.w = std::max(1.0f, bounds_.w - 150.0f);
|
|
dc.PushScissor(tb);
|
|
}
|
|
dc.DrawText(title_, bounds_.x + tx, bounds_.y + 4, style.fgColor, ALIGN_TOPLEFT);
|
|
dc.SetFontScale(0.6f, 0.6f);
|
|
dc.DrawText(subtitle_, bounds_.x + tx, bounds_.y2() - 7, style.fgColor, ALIGN_BOTTOM);
|
|
dc.SetFontScale(1.0f, 1.0f);
|
|
|
|
if (availableWidth < tw) {
|
|
dc.PopScissor();
|
|
}
|
|
dc.Draw()->Flush();
|
|
dc.PopScissor();
|
|
|
|
dc.RebindTexture();
|
|
}
|
|
|
|
std::string SavedataButton::DescribeText() const {
|
|
auto u = GetI18NCategory(I18NCat::UI_ELEMENTS);
|
|
return ApplySafeSubstitutions(u->T("%1 button"), title_) + "\n" + subtitle_;
|
|
}
|
|
|
|
SavedataBrowser::SavedataBrowser(const Path &path, UI::LayoutParams *layoutParams)
|
|
: LinearLayout(ORIENT_VERTICAL, layoutParams), path_(path) {
|
|
Refresh();
|
|
}
|
|
|
|
void SavedataBrowser::Update() {
|
|
LinearLayout::Update();
|
|
if (searchPending_) {
|
|
searchPending_ = false;
|
|
|
|
int n = gameList_->GetNumSubviews();
|
|
bool matches = searchFilter_.empty();
|
|
for (int i = 0; i < n; ++i) {
|
|
View *view = gameList_->GetViewByIndex(i);
|
|
// Note: might be resetting to empty string. Can do that right away.
|
|
if (searchFilter_.empty()) {
|
|
view->SetVisibility(UI::V_VISIBLE);
|
|
continue;
|
|
}
|
|
|
|
SavedataButton *savedataButton = static_cast<SavedataButton *>(view);
|
|
if (!savedataButton->UpdateText()) {
|
|
// We'll need to wait until the text is loaded.
|
|
searchPending_ = true;
|
|
view->SetVisibility(UI::V_GONE);
|
|
continue;
|
|
}
|
|
|
|
std::string label = view->DescribeText();
|
|
std::transform(label.begin(), label.end(), label.begin(), tolower);
|
|
bool match = label.find(searchFilter_) != label.npos;
|
|
matches = matches || match;
|
|
view->SetVisibility(match ? UI::V_VISIBLE : UI::V_GONE);
|
|
}
|
|
|
|
if (searchingView_) {
|
|
bool show = !searchFilter_.empty() && (matches || searchPending_);
|
|
searchingView_->SetVisibility(show ? UI::V_VISIBLE : UI::V_GONE);
|
|
}
|
|
if (noMatchView_)
|
|
noMatchView_->SetVisibility(matches || searchPending_ ? UI::V_GONE : UI::V_VISIBLE);
|
|
}
|
|
}
|
|
|
|
void SavedataBrowser::SetSearchFilter(const std::string &filter) {
|
|
auto sa = GetI18NCategory(I18NCat::SAVEDATA);
|
|
|
|
searchFilter_.resize(filter.size());
|
|
std::transform(filter.begin(), filter.end(), searchFilter_.begin(), tolower);
|
|
|
|
if (gameList_)
|
|
searchPending_ = true;
|
|
if (noMatchView_)
|
|
noMatchView_->SetText(ApplySafeSubstitutions(sa->T("Nothing matching '%1' was found."), filter));
|
|
if (searchingView_)
|
|
searchingView_->SetText(ApplySafeSubstitutions(sa->T("Showing matches for '%1'."), filter));
|
|
}
|
|
|
|
void SavedataBrowser::SetSortOption(SavedataSortOption opt) {
|
|
sortOption_ = opt;
|
|
if (gameList_) {
|
|
SortedLinearLayout *gl = static_cast<SortedLinearLayout *>(gameList_);
|
|
if (sortOption_ == SavedataSortOption::FILENAME) {
|
|
gl->SetCompare(&PrepFilename, &ByFilename);
|
|
} else if (sortOption_ == SavedataSortOption::SIZE) {
|
|
gl->SetCompare(&PrepSize, &BySize);
|
|
} else if (sortOption_ == SavedataSortOption::DATE) {
|
|
gl->SetCompare(&PrepDate, &ByDate);
|
|
}
|
|
}
|
|
}
|
|
|
|
void SavedataBrowser::PrepFilename(UI::View *v) {
|
|
// Nothing needed.
|
|
}
|
|
|
|
bool SavedataBrowser::ByFilename(const UI::View *v1, const UI::View *v2) {
|
|
const SavedataButton *b1 = static_cast<const SavedataButton *>(v1);
|
|
const SavedataButton *b2 = static_cast<const SavedataButton *>(v2);
|
|
|
|
return b1->GamePath() < b2->GamePath();
|
|
}
|
|
|
|
void SavedataBrowser::PrepSize(UI::View *v) {
|
|
SavedataButton *b = static_cast<SavedataButton *>(v);
|
|
b->UpdateTotalSize();
|
|
}
|
|
|
|
bool SavedataBrowser::BySize(const UI::View *v1, const UI::View *v2) {
|
|
const SavedataButton *b1 = static_cast<const SavedataButton *>(v1);
|
|
const SavedataButton *b2 = static_cast<const SavedataButton *>(v2);
|
|
const uint64_t size1 = b1->GetTotalSize();
|
|
const uint64_t size2 = b2->GetTotalSize();
|
|
|
|
if (size1 > size2)
|
|
return true;
|
|
else if (size1 < size2)
|
|
return false;
|
|
return b1->GamePath() < b2->GamePath();
|
|
}
|
|
|
|
void SavedataBrowser::PrepDate(UI::View *v) {
|
|
SavedataButton *b = static_cast<SavedataButton *>(v);
|
|
b->UpdateDateSeconds();
|
|
}
|
|
|
|
bool SavedataBrowser::ByDate(const UI::View *v1, const UI::View *v2) {
|
|
const SavedataButton *b1 = static_cast<const SavedataButton *>(v1);
|
|
const SavedataButton *b2 = static_cast<const SavedataButton *>(v2);
|
|
const int64_t time1 = b1->GetDateSeconds();
|
|
const int64_t time2 = b2->GetDateSeconds();
|
|
|
|
if (time1 > time2)
|
|
return true;
|
|
if (time1 < time2)
|
|
return false;
|
|
return b1->GamePath() < b2->GamePath();
|
|
}
|
|
|
|
void SavedataBrowser::Refresh() {
|
|
using namespace UI;
|
|
|
|
// Kill all the contents
|
|
Clear();
|
|
|
|
Add(new Spacer(1.0f));
|
|
auto mm = GetI18NCategory(I18NCat::MAINMENU);
|
|
auto sa = GetI18NCategory(I18NCat::SAVEDATA);
|
|
|
|
// Find games in the current directory and create new ones.
|
|
std::vector<SavedataButton *> savedataButtons;
|
|
|
|
std::vector<File::FileInfo> fileInfo;
|
|
GetFilesInDir(path_, &fileInfo, "ppst:");
|
|
|
|
for (size_t i = 0; i < fileInfo.size(); i++) {
|
|
bool isState = !fileInfo[i].isDirectory;
|
|
bool isSaveData = false;
|
|
|
|
if (!isState && File::Exists(path_ / fileInfo[i].name / "PARAM.SFO"))
|
|
isSaveData = true;
|
|
|
|
if (isSaveData || isState) {
|
|
savedataButtons.push_back(new SavedataButton(fileInfo[i].fullName, new UI::LinearLayoutParams(UI::FILL_PARENT, UI::WRAP_CONTENT)));
|
|
}
|
|
}
|
|
|
|
ViewGroup *group = new LinearLayout(ORIENT_VERTICAL, new UI::LinearLayoutParams(UI::Margins(12, 0)));
|
|
Add(group);
|
|
|
|
if (savedataButtons.empty()) {
|
|
group->Add(new TextView(sa->T("None yet. Things will appear here after you save.")));
|
|
gameList_ = nullptr;
|
|
noMatchView_ = nullptr;
|
|
searchingView_ = nullptr;
|
|
} else {
|
|
noMatchView_ = group->Add(new TextView(sa->T("Nothing matching '%1' was found")));
|
|
noMatchView_->SetVisibility(UI::V_GONE);
|
|
searchingView_ = group->Add(new TextView(sa->T("Showing matches for '%1'")));
|
|
searchingView_->SetVisibility(UI::V_GONE);
|
|
|
|
SortedLinearLayout *gl = new SortedLinearLayout(ORIENT_VERTICAL, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT));
|
|
gl->SetSpacing(4.0f);
|
|
gameList_ = gl;
|
|
Add(gameList_);
|
|
|
|
for (size_t i = 0; i < savedataButtons.size(); i++) {
|
|
SavedataButton *b = gameList_->Add(savedataButtons[i]);
|
|
b->OnClick.Handle(this, &SavedataBrowser::SavedataButtonClick);
|
|
}
|
|
}
|
|
|
|
// Reapply.
|
|
SetSortOption(sortOption_);
|
|
if (!searchFilter_.empty())
|
|
SetSearchFilter(searchFilter_);
|
|
}
|
|
|
|
void SavedataBrowser::SavedataButtonClick(UI::EventParams &e) {
|
|
SavedataButton *button = static_cast<SavedataButton *>(e.v);
|
|
UI::EventParams e2{};
|
|
e2.v = e.v;
|
|
e2.s = button->GamePath().ToString();
|
|
// Insta-update - here we know we are already on the right thread.
|
|
OnChoice.Trigger(e2);
|
|
}
|
|
|
|
SavedataScreen::~SavedataScreen() {
|
|
if (g_gameInfoCache) {
|
|
g_gameInfoCache->PurgeType(IdentifiedFileType::PPSSPP_SAVESTATE);
|
|
g_gameInfoCache->PurgeType(IdentifiedFileType::PSP_SAVEDATA_DIRECTORY);
|
|
}
|
|
}
|
|
|
|
void SavedataScreen::CreateSavedataTab(UI::ViewGroup *savedata) {
|
|
auto sa = GetI18NCategory(I18NCat::SAVEDATA);
|
|
using namespace UI;
|
|
Path savedata_dir = GetSysDirectory(DIRECTORY_SAVEDATA);
|
|
|
|
ChoiceStrip *sortStrip = new ChoiceStrip(ORIENT_HORIZONTAL, new LinearLayoutParams(0.0f, UI::Gravity::G_CENTER));
|
|
sortStrip->AddChoice(sa->T("Filename"));
|
|
sortStrip->AddChoice(sa->T("Size"));
|
|
sortStrip->AddChoice(sa->T("Date"));
|
|
sortStrip->SetSelection((int)sortOption_, false);
|
|
sortStrip->OnChoice.Add([this](UI::EventParams &e) {
|
|
sortOption_ = SavedataSortOption(e.a);
|
|
dataBrowser_->SetSortOption(sortOption_);
|
|
});
|
|
savedata->Add(sortStrip);
|
|
|
|
dataBrowser_ = savedata->Add(new SavedataBrowser(savedata_dir, new LayoutParams(FILL_PARENT, FILL_PARENT)));
|
|
dataBrowser_->SetSortOption(sortOption_);
|
|
if (!searchFilter_.empty())
|
|
dataBrowser_->SetSearchFilter(searchFilter_);
|
|
dataBrowser_->OnChoice.Handle(this, &SavedataScreen::OnSavedataButtonClick);
|
|
}
|
|
|
|
void SavedataScreen::CreateSavestateTab(UI::ViewGroup *savestate) {
|
|
auto sa = GetI18NCategory(I18NCat::SAVEDATA);
|
|
using namespace UI;
|
|
Path savestate_dir = GetSysDirectory(DIRECTORY_SAVESTATE);
|
|
|
|
ChoiceStrip *sortStrip = new ChoiceStrip(ORIENT_HORIZONTAL, new LinearLayoutParams(0.0f, UI::Gravity::G_CENTER));
|
|
sortStrip->AddChoice(sa->T("Filename"));
|
|
sortStrip->AddChoice(sa->T("Size"));
|
|
sortStrip->AddChoice(sa->T("Date"));
|
|
sortStrip->SetSelection((int)sortOption_, false);
|
|
sortStrip->OnChoice.Add([this](UI::EventParams &e) {
|
|
sortOption_ = SavedataSortOption(e.a);
|
|
stateBrowser_->SetSortOption(sortOption_);
|
|
});
|
|
savestate->Add(sortStrip);
|
|
|
|
stateBrowser_ = savestate->Add(new SavedataBrowser(savestate_dir));
|
|
stateBrowser_->SetSortOption(sortOption_);
|
|
if (!searchFilter_.empty())
|
|
stateBrowser_->SetSearchFilter(searchFilter_);
|
|
stateBrowser_->OnChoice.Handle(this, &SavedataScreen::OnSavedataButtonClick);
|
|
}
|
|
|
|
void SavedataScreen::CreateTabs() {
|
|
using namespace UI;
|
|
auto sa = GetI18NCategory(I18NCat::SAVEDATA);
|
|
|
|
AddTab("SavedataBrowser", sa->T("Save data"), [this](UI::LinearLayout *parent) {
|
|
CreateSavedataTab(parent);
|
|
});
|
|
|
|
AddTab("SavedataStatesBrowser", sa->T("Save states"), [this](UI::LinearLayout *parent) {
|
|
CreateSavestateTab(parent);
|
|
});
|
|
}
|
|
|
|
void SavedataScreen::CreateExtraButtons(UI::ViewGroup *verticalLayout, int margins) {
|
|
using namespace UI;
|
|
if (System_GetPropertyBool(SYSPROP_HAS_TEXT_INPUT_DIALOG)) {
|
|
auto di = GetI18NCategory(I18NCat::DIALOG);
|
|
verticalLayout->Add(new Choice(di->T("Search"), ImageID("I_SEARCH"), new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT, 0.0f, Margins(0, 0, margins, margins))))
|
|
->OnClick.Handle<SavedataScreen>(this, &SavedataScreen::OnSearch);
|
|
}
|
|
}
|
|
|
|
void SavedataScreen::OnSearch(UI::EventParams &e) {
|
|
if (System_GetPropertyBool(SYSPROP_HAS_TEXT_INPUT_DIALOG)) {
|
|
auto di = GetI18NCategory(I18NCat::DIALOG);
|
|
System_InputBoxGetString(GetRequesterToken(), di->T("Filter"), searchFilter_, false, [](std::string_view value, int ivalue) {
|
|
System_PostUIMessage(UIMessage::SAVEDATA_SEARCH, value);
|
|
});
|
|
}
|
|
}
|
|
|
|
void SavedataScreen::OnSavedataButtonClick(UI::EventParams &e) {
|
|
// the game path: e.s;
|
|
std::shared_ptr<GameInfo> ginfo = g_gameInfoCache->GetInfo(screenManager()->getDrawContext(), Path(e.s), GameInfoFlags::PARAM_SFO);
|
|
if (!ginfo->Ready(GameInfoFlags::PARAM_SFO)) {
|
|
return;
|
|
}
|
|
|
|
// Sanitize the title.
|
|
std::string title = SanitizeString(ginfo->GetTitle(), StringRestriction::NoLineBreaksOrSpecials, 0, 200);
|
|
SavedataPopupScreen *popupScreen = new SavedataPopupScreen(gamePath_, Path(e.s), title);
|
|
if (e.v) {
|
|
popupScreen->SetPopupOrigin(e.v);
|
|
}
|
|
screenManager()->push(popupScreen);
|
|
}
|
|
|
|
void SavedataScreen::dialogFinished(const Screen *dialog, DialogResult result) {
|
|
if (result == DR_NO) {
|
|
RecreateViews();
|
|
}
|
|
}
|
|
|
|
void SavedataScreen::sendMessage(UIMessage message, const char *value) {
|
|
UIBaseDialogScreen::sendMessage(message, value);
|
|
|
|
if (message == UIMessage::SAVEDATA_SEARCH) {
|
|
EnsureTabs();
|
|
|
|
searchFilter_ = value;
|
|
dataBrowser_->SetSearchFilter(searchFilter_);
|
|
stateBrowser_->SetSearchFilter(searchFilter_);
|
|
}
|
|
}
|