mirror of
https://github.com/hrydgard/ppsspp.git
synced 2026-10-31 19:47:06 +01:00
Each tab is now a SettingsPage: a list of items (headers, settings, buttons, hints, links to other pages) that a function builds and views are created from. Settings are added with page->Setting(&g_Config.x), which takes the label, the kind of view, the range or choices, units, labels for special values and the default from the config. The pages keep what depends on the UI (which settings to show and when, enable rules, and behavior that needs the screen), and reach the screen through the page's SettingsPageHost. A page can link to other pages, which open on top of it in the same tab, under a bar showing where you are, like Tools > Developer tools > Graphics, where the earlier pages can be clicked to go back to them. The bar has back, search and help buttons, and the back key also goes back a page (or out of a search) before leaving the screen. Each page remembers its scroll position. In portrait there's no tab strip: a page links to the tabs, and the bar shows just the current page's title. Rotating keeps showing the same page. Moving between pages only focuses an item when navigating with a keyboard or controller. The developer tools are now pages linked from the Tools tab, instead of a screen of their own. The in-game developer menu and --start-screen=developertools open the settings on that page. The toggles of live state (the remote debugger and the MIPS tracer) show and change it through functions, and pretending to be in-game for the notification tests is a flag on the OSD. The memory map and memstick tests, only reachable from commented-out code, are removed. Search lists the matching settings from all tabs and the pages they link to, grouped by where they are. Each result goes to the setting on its page, scrolled to and highlighted (outlined, when picked with a mouse or touch), or for a page, opens it. This replaces the search tab and the old filter that hid the views that didn't match, and with them ShowSearchControls. Changing tabs leaves the search. The pages, the bar and search are in SettingsPagesDialogScreen, between the tabbed dialog screen and the settings screen, so the other tabbed screens keep plain tabs. Other visible changes: - Reset buttons that went to the wrong value now go to the real default: the touch control auto-hide delay (was 20 seconds instead of 8), the mouse wheel delay (1 ms, below its own range, instead of 80) and the UI size (0 instead of -1 on desktop). The alternative speeds get a reset button. - Units are shown on more sliders (frames, MHz, seconds, Hz), and popups no longer spell out what 0 means in the units, since the zero label does. Units of up to three characters (%, x, ms, MHz) aren't translated. - The Android HW scaler hides the scales the display is too small for, instead of cutting the list short. - Choices are translated in the setting's own section. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
582 lines
19 KiB
C++
582 lines
19 KiB
C++
#include "Common/UI/View.h"
|
|
#include "Common/UI/Context.h"
|
|
#include "Common/Render/DrawBuffer.h"
|
|
#include "Common/System/Request.h"
|
|
#include "Common/System/Display.h"
|
|
#include "Common/TimeUtil.h"
|
|
#include "Common/Data/Text/I18n.h"
|
|
#include "Common/Math/curves.h"
|
|
#include "Common/StringUtils.h"
|
|
#include "Common/Data/Encoding/Utf8.h"
|
|
#include "Common/UI/Root.h"
|
|
#include "UI/MiscViews.h"
|
|
#include "UI/GameInfoCache.h"
|
|
#include "Common/UI/PopupScreens.h"
|
|
#include "Core/Config.h"
|
|
#include "GPU/Common/PresentationCommon.h"
|
|
#include "Core/Util/VideoPlayer.h"
|
|
|
|
TextWithImage::TextWithImage(ImageID imageID, std::string_view text, UI::LinearLayoutParams *layoutParams) : UI::LinearLayout(ORIENT_HORIZONTAL, layoutParams) {
|
|
using namespace UI;
|
|
SetSpacing(8.0f);
|
|
if (!layoutParams) {
|
|
layoutParams_->width = FILL_PARENT;
|
|
layoutParams_->height = ITEM_HEIGHT;
|
|
}
|
|
if (imageID.isValid()) {
|
|
Add(new ImageView(imageID, "", new LinearLayoutParams(0.0f, UI::Gravity::G_VCENTER)));
|
|
}
|
|
Add(new TextView(text, new LinearLayoutParams(1.0f, UI::Gravity::G_VCENTER)));
|
|
}
|
|
|
|
CopyableText::CopyableText(ImageID imageID, std::string_view text, UI::LinearLayoutParams *layoutParams) : UI::LinearLayout(ORIENT_HORIZONTAL, layoutParams) {
|
|
using namespace UI;
|
|
SetSpacing(8.0f);
|
|
if (!layoutParams) {
|
|
layoutParams_->width = FILL_PARENT;
|
|
layoutParams_->height = ITEM_HEIGHT;
|
|
}
|
|
if (imageID.isValid()) {
|
|
Add(new ImageView(imageID, "", new LinearLayoutParams(0.0f, UI::Gravity::G_VCENTER)));
|
|
}
|
|
Add(new TextView(text, new LinearLayoutParams(1.0f, UI::Gravity::G_VCENTER)))->SetBig(true);
|
|
|
|
std::string textStr(text); // We need to store the text in the lambda context.
|
|
Add(new Choice(ImageID("I_FILE_COPY"), new LinearLayoutParams()))->OnClick.Add([textStr](UI::EventParams &) {
|
|
System_CopyStringToClipboard(textStr);
|
|
});
|
|
}
|
|
|
|
TopBar::TopBar(const UIContext &ctx, TopBarFlags flags, std::string_view title, UI::LayoutParams *layoutParams) : UI::LinearLayout(ORIENT_HORIZONTAL, layoutParams) {
|
|
using namespace UI;
|
|
SetSpacing(10.0f);
|
|
if (!layoutParams) {
|
|
layoutParams_->width = UI::FILL_PARENT;
|
|
layoutParams_->height = ITEM_HEIGHT;
|
|
}
|
|
|
|
if (!(flags & TopBarFlags::NoBackButton)) {
|
|
auto dlg = GetI18NCategory(I18NCat::DIALOG);
|
|
backButton_ = Add(new Choice(ImageID("I_NAVIGATE_BACK"), new LinearLayoutParams(ITEM_HEIGHT, ITEM_HEIGHT)));
|
|
backButton_->OnClick.Add([](UI::EventParams &e) {
|
|
e.bubbleResult = DR_BACK;
|
|
});
|
|
backButton_->SetRoundCorners();
|
|
} else {
|
|
Add(new Spacer(4.0f));
|
|
}
|
|
|
|
if (!title.empty()) {
|
|
TextView *titleView = Add(new TextView(title, ALIGN_VCENTER, false, new LinearLayoutParams(1.0f, Gravity::G_VCENTER)));
|
|
titleView->SetTextColor(ctx.GetTheme().itemStyle.fgColor);
|
|
titleView->SetBig(true);
|
|
// If using HCENTER, to balance the centering, add a spacer on the right.
|
|
// Add(new Spacer(50.0f));
|
|
}
|
|
|
|
if (flags & TopBarFlags::ContextMenuButton) {
|
|
contextMenuButton_ = Add(new Choice(ImageID("I_THREE_DOTS"), new LinearLayoutParams(ITEM_HEIGHT, ITEM_HEIGHT)));
|
|
contextMenuButton_->OnClick.Add([this](UI::EventParams &e) {
|
|
this->OnContextMenuClick.Trigger(e);
|
|
});
|
|
contextMenuButton_->SetRoundCorners();
|
|
}
|
|
}
|
|
|
|
void ShinyIcon::Draw(UIContext &dc) {
|
|
UI::DrawIconShine(dc, bounds_, 1.0f, animated_);
|
|
UI::ImageView::Draw(dc);
|
|
}
|
|
|
|
class SimpleGameIconView : public UI::InertView {
|
|
public:
|
|
SimpleGameIconView(const Path &gamePath, UI::LayoutParams *layoutParams = 0)
|
|
: InertView(layoutParams), gamePath_(gamePath) {
|
|
}
|
|
|
|
void GetContentDimensions(const UIContext &dc, float &w, float &h) const override {
|
|
h = UI::ITEM_HEIGHT;
|
|
float aspect = 1.0f;
|
|
if (textureHeight_ > 0) {
|
|
aspect = static_cast<float>(textureWidth_) / static_cast<float>(textureHeight_);
|
|
}
|
|
w = h * aspect;
|
|
}
|
|
|
|
void Draw(UIContext &dc) override {
|
|
std::shared_ptr<GameInfo> info = g_gameInfoCache->GetInfo(dc.GetDrawContext(), gamePath_, GameInfoFlags::ICON);
|
|
if (!info->Ready(GameInfoFlags::ICON) || !info->icon.texture) {
|
|
return;
|
|
}
|
|
|
|
Draw::Texture *texture = info->icon.texture;
|
|
|
|
textureWidth_ = texture->Width();
|
|
textureHeight_ = texture->Height();
|
|
|
|
dc.Flush();
|
|
dc.GetDrawContext()->BindTexture(0, texture);
|
|
dc.Draw()->Rect(bounds_.x, bounds_.y, bounds_.w, bounds_.h, 0xFFFFFFFF);
|
|
dc.Flush();
|
|
dc.RebindTexture();
|
|
|
|
// Draw the gear icon
|
|
const AtlasImage *gearImage = dc.Draw()->GetAtlas()->getImage(ImageID("I_GEAR_SMALL"));
|
|
dc.Draw()->DrawImage(ImageID("I_GEAR_SMALL"), bounds_.x + 1, bounds_.y2() - gearImage->h - 1, 1.0f);
|
|
}
|
|
|
|
std::string DescribeText() const override { return ""; }
|
|
|
|
private:
|
|
Path gamePath_;
|
|
int textureWidth_ = 0;
|
|
int textureHeight_ = 0;
|
|
};
|
|
|
|
UI::View *CreateGameIconView(const Path &gamePath, UI::LayoutParams *layoutParams) {
|
|
return new SimpleGameIconView(gamePath, layoutParams);
|
|
}
|
|
|
|
PaneTitleBar::PaneTitleBar(const Path &gamePath, std::string_view title, const std::string_view settingsCategory, UI::LayoutParams *layoutParams)
|
|
: UI::LinearLayout(ORIENT_HORIZONTAL, layoutParams), gamePath_(gamePath) {
|
|
using namespace UI;
|
|
SetSpacing(10.0f);
|
|
if (!layoutParams) {
|
|
layoutParams_->width = UI::FILL_PARENT;
|
|
layoutParams_->height = ITEM_HEIGHT;
|
|
}
|
|
|
|
auto dlg = GetI18NCategory(I18NCat::DIALOG);
|
|
|
|
if (!title.empty()) {
|
|
SimpleTextView *titleView = Add(new SimpleTextView(title, new LinearLayoutParams(0.0f, Gravity::G_VCENTER, Margins(12, 0, 20, 0))));
|
|
titleView->SetBig(true);
|
|
// If using HCENTER, to balance the centering, add a spacer on the right.
|
|
}
|
|
|
|
Add(new Spacer(10.0f, new LinearLayoutParams(1.0f)));
|
|
|
|
// Now add the game icon.
|
|
if (!gamePath.empty() && g_Config.IsGameSpecific()) {
|
|
Add(new SimpleGameIconView(gamePath_, new LinearLayoutParams(WRAP_CONTENT, WRAP_CONTENT)));
|
|
}
|
|
|
|
if (!settingsCategory.empty()) {
|
|
std::string settingsUrl;
|
|
if (settingsCategory[0] == '/') {
|
|
settingsUrl = join("https://www.ppsspp.org", settingsCategory);
|
|
} else {
|
|
settingsUrl = join("https://www.ppsspp.org/docs/settings/", settingsCategory);
|
|
}
|
|
Choice *helpButton = Add(new Choice(ImageID("I_LINK_OUT_QUESTION"), new LinearLayoutParams()));
|
|
helpButton->OnClick.Add([settingsUrl](UI::EventParams &) {
|
|
System_LaunchUrl(LaunchUrlType::BROWSER_URL, settingsUrl);
|
|
});
|
|
}
|
|
}
|
|
|
|
void GameImageView::GetContentDimensions(const UIContext &dc, float &w, float &h) const {
|
|
std::shared_ptr<GameInfo> info = g_gameInfoCache->GetInfo(dc.GetDrawContext(), gamePath_, image_);
|
|
GameInfoTex *tex = GetTex(info);
|
|
w = 0;
|
|
h = 0;
|
|
if (!tex || !tex->texture) {
|
|
// Not ready yet
|
|
return;
|
|
}
|
|
Draw::Texture *texture = tex->texture;
|
|
w = tex->texture->Width();
|
|
h = tex->texture->Height();
|
|
}
|
|
|
|
GameInfoTex *GameImageView::GetTex(std::shared_ptr<GameInfo> info) const {
|
|
GameInfoTex *tex = nullptr;
|
|
switch (image_) {
|
|
case GameInfoFlags::ICON:
|
|
tex = &info->icon;
|
|
break;
|
|
case GameInfoFlags::PIC0:
|
|
tex = &info->pic0;
|
|
break;
|
|
case GameInfoFlags::PIC1:
|
|
tex = &info->pic1;
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
return tex;
|
|
}
|
|
|
|
void GameImageView::Draw(UIContext &dc) {
|
|
using namespace UI;
|
|
std::shared_ptr<GameInfo> info = g_gameInfoCache->GetInfo(dc.GetDrawContext(), gamePath_, image_);
|
|
if (!info->Ready(image_)) {
|
|
return;
|
|
}
|
|
|
|
GameInfoTex *tex = GetTex(info);
|
|
if (!tex || !tex->texture) {
|
|
return;
|
|
}
|
|
|
|
Draw::Texture *texture = tex->texture;
|
|
|
|
float textureWidth_ = texture->Width() * scale_;
|
|
float textureHeight_ = texture->Height() * scale_;
|
|
|
|
// Fade icon with the backgrounds.
|
|
double loadTime = info->icon.timeLoaded;
|
|
auto pic = info->GetPIC1();
|
|
if (pic) {
|
|
loadTime = std::max(loadTime, pic->timeLoaded);
|
|
}
|
|
uint32_t color = whiteAlpha(ease((time_now_d() - loadTime) * 3));
|
|
|
|
// Adjust size so we don't stretch the image vertically or horizontally.
|
|
// Make sure it's not wider than 144 (like Doom Legacy homebrew), ugly in the grid mode.
|
|
float nw = std::min(bounds_.h * textureWidth_ / textureHeight_, (float)bounds_.w);
|
|
int x = bounds_.x + (bounds_.w - nw) / 2.0f;
|
|
|
|
dc.Flush();
|
|
dc.GetDrawContext()->BindTexture(0, texture);
|
|
dc.Draw()->Rect(x, bounds_.y, nw, bounds_.h, color);
|
|
dc.Flush();
|
|
dc.RebindTexture();
|
|
}
|
|
|
|
void AddRotationPicker(ScreenManager *screenManager, UI::ViewGroup *parent, bool text) {
|
|
using namespace UI;
|
|
static const char *screenRotation[] = { "Auto", "Landscape", "Portrait", "Landscape Reversed", "N/A", "Landscape Auto"};
|
|
static const std::map<int, ImageID> screenRotationIcons{
|
|
{ROTATION_AUTO, ImageID("I_DEVICE_ROTATION_AUTO")},
|
|
{ROTATION_LOCKED_HORIZONTAL, ImageID("I_DEVICE_ROTATION_LANDSCAPE")},
|
|
{ROTATION_LOCKED_VERTICAL, ImageID("I_DEVICE_ROTATION_PORTRAIT")},
|
|
{ROTATION_LOCKED_HORIZONTAL180, ImageID("I_DEVICE_ROTATION_LANDSCAPE_REV")},
|
|
{ROTATION_AUTO_HORIZONTAL, ImageID("I_DEVICE_ROTATION_LANDSCAPE_AUT")},
|
|
};
|
|
|
|
auto co = GetI18NCategory(I18NCat::CONTROLS);
|
|
|
|
PopupMultiChoice *rot = parent->Add(new PopupMultiChoice(&g_Config.iScreenRotation, text ? co->T("Screen Rotation") : "", screenRotation, 0, ARRAY_SIZE(screenRotation), I18NCat::CONTROLS, screenManager, text ? nullptr : new LinearLayoutParams(ITEM_HEIGHT, ITEM_HEIGHT)));
|
|
if (!text) {
|
|
rot->SetHideTitle(true);
|
|
rot->SetIconOnly(true);
|
|
}
|
|
rot->SetChoiceIcons(screenRotationIcons);
|
|
|
|
// Portrait Reversed is not recommended on iPhone (and we also ban it in the plist).
|
|
// However it's recommended to support it on iPad, so maybe we will in the future.
|
|
rot->HideChoice(ROTATION_LOCKED_VERTICAL180);
|
|
rot->OnChoice.Add([](UI::EventParams &) {
|
|
INFO_LOG(Log::System, "New display rotation: %d", g_Config.iScreenRotation);
|
|
System_Notify(SystemNotification::ROTATE_UPDATED);
|
|
});
|
|
}
|
|
|
|
void GameInfoBGView::Draw(UIContext &dc) {
|
|
// Should only be called when visible.
|
|
std::shared_ptr<GameInfo> ginfo = g_gameInfoCache->GetInfo(dc.GetDrawContext(), gamePath_, GameInfoFlags::PIC1);
|
|
dc.Flush();
|
|
|
|
// PIC1 is the loading image, so let's only draw if it's available.
|
|
if (ginfo->Ready(GameInfoFlags::PIC1) && ginfo->pic1.texture) {
|
|
Draw::Texture *texture = ginfo->pic1.texture;
|
|
if (texture) {
|
|
const DisplayLayoutConfig &config = g_Config.GetDisplayLayoutConfig(g_display.GetDeviceOrientation());
|
|
// Similar to presentation, we want to put the game PIC1 in the same region of the screen.
|
|
FRect frame = GetScreenFrame(config.bIgnoreScreenInsets, g_display.pixel_xres, g_display.pixel_yres);
|
|
FRect rc;
|
|
CalculateDisplayOutputRect(config, &rc, texture->Width(), texture->Height(), frame, config.iInternalScreenRotation);
|
|
|
|
// Need to adjust for DPI here since we're still in the UI coordinate space here, not the pixel coordinate space used for in-game presentation.
|
|
Bounds bounds(rc.x * g_display.dpi_scale_x, rc.y * g_display.dpi_scale_y, rc.w * g_display.dpi_scale_x, rc.h * g_display.dpi_scale_y);
|
|
|
|
dc.GetDrawContext()->BindTexture(0, texture);
|
|
|
|
double loadTime = ginfo->pic1.timeLoaded;
|
|
uint32_t color = alphaMul(color_, ease((time_now_d() - loadTime) * 3));
|
|
dc.Draw()->DrawTexRect(bounds, 0, 0, 1, 1, color);
|
|
dc.Flush();
|
|
dc.RebindTexture();
|
|
}
|
|
}
|
|
}
|
|
|
|
SettingHint::SettingHint(std::string_view text, UI::View *setting)
|
|
: UI::TextView(text, new UI::LinearLayoutParams(UI::FILL_PARENT, UI::WRAP_CONTENT)), setting_(setting) {
|
|
SetTextSize(UI::TextSize::Tiny);
|
|
SetPadding(UI::Margins(12, 0, 12, 8));
|
|
SetAlign(FLAG_WRAP_TEXT);
|
|
}
|
|
|
|
void SettingHint::Draw(UIContext &dc) {
|
|
const bool enabled = setting_ ? setting_->IsEnabled() : true;
|
|
const UI::Style &style = enabled ? dc.GetTheme().itemStyle : dc.GetTheme().itemDisabledStyle;
|
|
SetTextColor(style.fgColor); // bit hacky but works
|
|
if (setting_) {
|
|
dc.FillRoundRect(style.background, bounds_, dc.GetTheme().choiceRoundRadius, roundCorners_);
|
|
}
|
|
UI::TextView::Draw(dc);
|
|
}
|
|
|
|
void SearchBar::Draw(UIContext &dc) {
|
|
using namespace UI;
|
|
|
|
dc.FillRect(dc.GetTheme().itemStyle.background, bounds_);
|
|
|
|
const ImageID searchIcon = ImageID("I_SEARCH");
|
|
const AtlasImage *image = dc.Draw()->GetAtlas()->getImage(searchIcon);
|
|
int leftMargin = 10;
|
|
if (image) {
|
|
dc.Draw()->DrawImage(searchIcon, bounds_.x + leftMargin, bounds_.centerY(), 1.0f, 0xFFFFFFFF, ALIGN_VCENTER);
|
|
leftMargin += image->w + 10;
|
|
}
|
|
dc.DrawText(searchFilter_, bounds_.x + leftMargin, bounds_.centerY(), 0xFFFFFFFF, ALIGN_VCENTER);
|
|
}
|
|
|
|
SearchBar::SearchBar(UI::LayoutParams *params) : UI::InertView(params) {
|
|
SetVisibility(UI::Visibility::V_GONE);
|
|
}
|
|
|
|
void SearchBar::GetContentDimensions(const UIContext &dc, float &w, float &h) const {
|
|
w = 0;
|
|
h = 0;
|
|
const ImageID searchIcon = ImageID("I_SEARCH");
|
|
dc.MeasureText(dc.GetTheme().uiFont, 1.0f, 1.0f, searchFilter_, &w, &h);
|
|
const AtlasImage *image = dc.Draw()->GetAtlas()->getImage(searchIcon);
|
|
if (image) {
|
|
w += image->w + 10;
|
|
h = std::max(h, (float)image->h);
|
|
}
|
|
w += 20; // Padding
|
|
h += 24;
|
|
}
|
|
|
|
bool SearchBar::Touch(const TouchInput &input) {
|
|
bool retval = UI::InertView::Touch(input);
|
|
// Search bar has a simple touch-to-cancel functionality,
|
|
// for the user to be able to get out of searches without knowing ESC (or backspacing the whole search string).
|
|
if (input.flags & TouchInputFlags::DOWN) {
|
|
if (bounds_.Contains(input.x, input.y)) {
|
|
UI::EventParams params{this};
|
|
OnCancel.Trigger(params);
|
|
return true;
|
|
}
|
|
}
|
|
return retval;
|
|
}
|
|
|
|
void ViewSearch::ApplySearchFilter(UI::ViewGroup *viewGroup, bool setKeyboardFocus) {
|
|
if (searchBar) {
|
|
searchBar->SetSearchFilter(searchFilter);
|
|
searchBar->SetVisibility(searchFilter.empty() ? UI::V_GONE : UI::V_VISIBLE);
|
|
}
|
|
|
|
if (searchFilter.empty() && searchStates.empty()) {
|
|
// We haven't hidden anything, and we're not searching, so do nothing.
|
|
searchPending = false;
|
|
return;
|
|
}
|
|
|
|
std::string filter = NormalizeForSearch(searchFilter);
|
|
|
|
searchPending = false;
|
|
// By default, everything is matching.
|
|
searchStates.resize(viewGroup->GetNumSubviews(), SearchState::MATCH);
|
|
if (filter.empty()) {
|
|
// Just quickly mark anything we hid as visible again.
|
|
for (int i = 0; i < viewGroup->GetNumSubviews(); ++i) {
|
|
UI::View *v = viewGroup->GetViewByIndex(i);
|
|
if (searchStates[i] != SearchState::MATCH)
|
|
v->SetVisibility(UI::V_VISIBLE);
|
|
}
|
|
|
|
searchStates.clear();
|
|
return;
|
|
}
|
|
|
|
UI::View *firstMatch = nullptr;
|
|
|
|
for (int i = 0; i < viewGroup->GetNumSubviews(); ++i) {
|
|
UI::View *v = viewGroup->GetViewByIndex(i);
|
|
|
|
bool match = false;
|
|
if (v->AlwaysVisibleInSearch()) {
|
|
match = true;
|
|
if (v->CanBeFocused() && !firstMatch) {
|
|
firstMatch = v;
|
|
}
|
|
} else if (v->CanBeFocused() && NormalizeForSearch(v->SearchAlias()).find(filter) != std::string::npos) {
|
|
match = true;
|
|
if (!firstMatch) {
|
|
firstMatch = v;
|
|
}
|
|
} else {
|
|
std::string label = v->DescribeText();
|
|
// This is a bit of a hack to recognize a pending game title.
|
|
if (label == "...") {
|
|
searchPending = true;
|
|
// Hide anything pending while, we'll pop-in search results as they match.
|
|
// Note: we leave it at MATCH if gone before, so we don't show it again.
|
|
if (v->GetVisibility() == UI::V_VISIBLE) {
|
|
if (searchStates[i] == SearchState::MATCH)
|
|
v->SetVisibility(UI::V_GONE);
|
|
searchStates[i] = SearchState::PENDING;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
label = NormalizeForSearch(label);
|
|
match = v->CanBeFocused() && label.find(filter) != label.npos;
|
|
if (match && !firstMatch) {
|
|
firstMatch = v;
|
|
}
|
|
}
|
|
if (match && searchStates[i] != SearchState::MATCH) {
|
|
// It was previously visible and force hidden, so show it again.
|
|
v->SetVisibility(UI::V_VISIBLE);
|
|
searchStates[i] = SearchState::MATCH;
|
|
} else if (!match && searchStates[i] == SearchState::MATCH && v->GetVisibility() == UI::V_VISIBLE) {
|
|
v->SetVisibility(UI::V_GONE);
|
|
searchStates[i] = SearchState::MISMATCH;
|
|
}
|
|
}
|
|
|
|
if (firstMatch && setKeyboardFocus) {
|
|
UI::EnableFocusMovement(true);
|
|
UI::SetFocusedView(firstMatch, UI::FocusFlags::CAUSE_OTHER);
|
|
return;
|
|
}
|
|
|
|
// Don't leave the focus on a view we just hid. A game would keep showing its background and
|
|
// playing its music, for one.
|
|
UI::View *focused = UI::GetFocusedView();
|
|
if (focused && focused->GetVisibility() == UI::V_GONE && viewGroup->ContainsSubview(focused)) {
|
|
UI::SetFocusedView(nullptr, UI::FocusFlags::CAUSE_FOCUS_MOVE);
|
|
}
|
|
}
|
|
|
|
static bool IsSearchableChar(int unichar) {
|
|
// 127 gets produced from Ctrl+Backspace on Windows for some reason.
|
|
// 0x80-0xBF are C1 controls and Latin-1 symbols like § and ½ (the key left of 1 on Nordic keyboards),
|
|
// which don't appear in titles. Accented letters start at 0xC0.
|
|
return unichar >= 0x20 && !(unichar >= 0x7F && unichar < 0xC0);
|
|
}
|
|
|
|
static bool IsFirstSearchableChar(int unichar) {
|
|
// Don't allow spaces as the first character, it looks confusing (empty search field)
|
|
return IsSearchableChar(unichar) && unichar != ' ' && unichar != '`' && unichar != '.' && unichar != ',';
|
|
}
|
|
|
|
bool ViewSearch::Key(UI::ViewGroup *viewGroup, const KeyInput &input) {
|
|
bool retval = false;
|
|
// Only one is visible at a time, so we can just grab all Char input.
|
|
if (input.flags & KeyInputFlags::CHAR) {
|
|
const int unichar = input.keyCode;
|
|
if (IsSearchableChar(unichar)) {
|
|
// Don't allow spaces as the first character, it looks confusing (empty search field)
|
|
if (searchFilter.empty() && !IsFirstSearchableChar(unichar)) {
|
|
return false;
|
|
}
|
|
|
|
// TODO: Save focus state here.
|
|
// Insert it! (todo: do it with a string insert)
|
|
char buf[8];
|
|
buf[u8_wc_toutf8(buf, unichar)] = '\0';
|
|
searchFilter += buf;
|
|
ApplySearchFilter(viewGroup, true);
|
|
retval = true;
|
|
}
|
|
} else if (input.flags & KeyInputFlags::DOWN) {
|
|
if (input.keyCode == NKCODE_DEL) {
|
|
if (!searchFilter.empty()) {
|
|
#if PPSSPP_PLATFORM(MAC) || PPSSPP_PLATFORM(IOS)
|
|
const KeyInputFlags wordMod = KeyInputFlags::ModAlt;
|
|
#else
|
|
const KeyInputFlags wordMod = KeyInputFlags::ModCtrl;
|
|
#endif
|
|
if (input.flags & wordMod) {
|
|
// Ctrl+Backspace (Option+Backspace on Apple) deletes the last word. Delete until the last space.
|
|
size_t pos = searchFilter.find_last_of(' ');
|
|
if (pos != searchFilter.npos) {
|
|
searchFilter.erase(pos);
|
|
} else {
|
|
searchFilter.clear();
|
|
}
|
|
} else {
|
|
// Remove a whole UTF-8 character, not just its last byte.
|
|
int pos = (int)searchFilter.size();
|
|
u8_dec(searchFilter.c_str(), &pos);
|
|
searchFilter.erase(pos);
|
|
}
|
|
ApplySearchFilter(viewGroup, true);
|
|
retval = true;
|
|
if (searchFilter.empty()) {
|
|
// TODO: Restore focus state here.
|
|
UI::EnableFocusMovement(false);
|
|
}
|
|
}
|
|
// With an empty filter, it's up to the owner (the game browser goes up a directory).
|
|
} else if (!searchFilter.empty() && input.keyCode == NKCODE_ESCAPE) {
|
|
searchFilter.clear();
|
|
ApplySearchFilter(viewGroup, false);
|
|
retval = true;
|
|
|
|
// TODO: Restore focus state here.
|
|
UI::EnableFocusMovement(false);
|
|
}
|
|
}
|
|
return retval;
|
|
}
|
|
|
|
PMFVideoTexture::PMFVideoTexture(std::string data) : data_(std::move(data)) {
|
|
player_ = pmf_create();
|
|
if (pmf_init(player_, (const uint8_t *)data_.data(), data_.size(), &width_, &height_) != 0) {
|
|
ERROR_LOG(Log::G3D, "Couldn't load pmf");
|
|
pmf_destroy(player_);
|
|
player_ = nullptr;
|
|
}
|
|
}
|
|
|
|
PMFVideoTexture::~PMFVideoTexture() {
|
|
DeviceLost();
|
|
if (player_) {
|
|
pmf_destroy(player_);
|
|
player_ = nullptr;
|
|
}
|
|
}
|
|
|
|
void PMFVideoTexture::DeviceLost() {
|
|
if (curFrame_) {
|
|
curFrame_->Release();
|
|
curFrame_ = nullptr;
|
|
}
|
|
}
|
|
|
|
Draw::Texture *PMFVideoTexture::Update(Draw::DrawContext *draw, double timeSeconds) {
|
|
if (!player_) {
|
|
return nullptr;
|
|
}
|
|
|
|
// Dimensions are capped by pmf_init, but use size_t arithmetic anyway
|
|
// so a regression can't overflow the allocation.
|
|
frame_.resize((size_t)width_ * (size_t)height_ * 4);
|
|
if (pmf_update(player_, timeSeconds, frame_.data())) {
|
|
DeviceLost();
|
|
|
|
Draw::TextureDesc desc{};
|
|
desc.width = width_;
|
|
desc.height = height_;
|
|
desc.depth = 1;
|
|
desc.format = Draw::DataFormat::R8G8B8A8_UNORM;
|
|
desc.generateMips = false;
|
|
desc.tag = "pmf_frame";
|
|
desc.type = Draw::TextureType::LINEAR2D;
|
|
desc.mipLevels = 1;
|
|
desc.initData.push_back(frame_.data());
|
|
|
|
curFrame_ = draw->CreateTexture(desc);
|
|
}
|
|
return curFrame_;
|
|
}
|