diff --git a/Common/File/Path.h b/Common/File/Path.h index ab77b82a3a..534a93e2a0 100644 --- a/Common/File/Path.h +++ b/Common/File/Path.h @@ -59,7 +59,7 @@ public: } bool Valid() const { return !path_.empty(); } - bool IsRoot() const { return path_ == "/"; } // Special value - only path that can end in a slash. + bool IsRoot() const { return path_ == "/"; } // Special value - only path that can end in a slash - except see below. // On Windows, "//" is the network folder that lists the servers, the other path that ends in a slash. bool IsNetworkRoot() const { #if PPSSPP_PLATFORM(WINDOWS) diff --git a/Common/Render/DrawBuffer.h b/Common/Render/DrawBuffer.h index a5ffbf4c4e..cacabff8ae 100644 --- a/Common/Render/DrawBuffer.h +++ b/Common/Render/DrawBuffer.h @@ -133,6 +133,9 @@ public: } // Results in 18 triangles. Kind of expensive for a button. void DrawImage4Grid(ImageID atlas_image, float x1, float y1, float x2, float y2, Color color = COLOR(0xFFFFFF), float corner_scale = 1.0); + void DrawImage4Grid(ImageID atlas_image, const Bounds &bounds, Color color = COLOR(0xFFFFFF), float corner_scale = 1.0) { + DrawImage4Grid(atlas_image, bounds.x, bounds.y, bounds.x2(), bounds.y2(), color, corner_scale); + } // Uses the nearest precomputed corner radius, scaled to fit, so outlines get thinner or thicker // when radius isn't one of ROUND_RECT_RADII. 18 triangles filled, 16 outlined. diff --git a/UI/CMakeLists.txt b/UI/CMakeLists.txt index 7ed465f3e1..2a6f150406 100644 --- a/UI/CMakeLists.txt +++ b/UI/CMakeLists.txt @@ -53,6 +53,8 @@ list(APPEND UISource MainScreen.cpp GameBrowser.h GameBrowser.cpp + GameButton.h + GameButton.cpp MiscScreens.h MiscScreens.cpp MiscViews.h diff --git a/UI/EmuScreen.cpp b/UI/EmuScreen.cpp index f033b153a2..4c1ab9c18b 100644 --- a/UI/EmuScreen.cpp +++ b/UI/EmuScreen.cpp @@ -665,7 +665,7 @@ void EmuScreen::sendMessage(UIMessage message, const char *value) { // temporary workaround for hotkey its freeze the ui when open chat screen using hotkey and native keyboard is enable if (g_Config.bBypassOSKWithKeyboard) { // TODO: Make translatable. - g_OSD.Show(OSDType::MESSAGE_INFO, "Disable \"Use system native keyboard\" to use chat hotkey", 2.0f); + g_OSD.Show(OSDType::MESSAGE_INFO, "Disable \"Use system native keyboard\" to use chat hotkey", 2.0f, "chatHotKey"); } else { OpenChat(true); } @@ -943,7 +943,7 @@ void EmuScreen::ProcessVKey(VirtKey virtKey, bool down) { if (SaveState::CanRewind()) { SaveState::Rewind(&ShowMessageAfterSaveStateAction); } else { - g_OSD.Show(OSDType::MESSAGE_WARNING, sc->T("norewind", "No rewind save states available"), 2.0); + g_OSD.Show(OSDType::MESSAGE_WARNING, sc->T("norewind", "No rewind save states available"), 2.0, "norewind"); } } } diff --git a/UI/GameBrowser.cpp b/UI/GameBrowser.cpp index edcff49bba..99dd05f35e 100644 --- a/UI/GameBrowser.cpp +++ b/UI/GameBrowser.cpp @@ -27,7 +27,6 @@ #include "Common/UI/Root.h" #include "Common/UI/ScreenManager.h" -#include "Common/Math/curves.h" #include "Common/Net/URL.h" #include "Common/File/FileUtil.h" #include "Common/TimeUtil.h" @@ -40,10 +39,9 @@ #include "Core/ELF/PBPReader.h" #include "Core/FileLoaders/PS1EbootFileLoader.h" #include "Core/MemMap.h" -#include "Core/Util/VideoPlayer.h" #include "UI/GameBrowser.h" -#include "UI/GamePacker.h" +#include "UI/GameButton.h" #include "UI/RemoteISOScreen.h" #include "UI/SavedataScreen.h" #include "UI/Store.h" @@ -53,720 +51,9 @@ #include "Common/Data/Text/I18n.h" #include "Core/Util/DarwinFileSystemServices.h" // For the browser -// Icon dimensions in the large list, and its padding around the icon. -static constexpr float LARGE_LIST_ICON_W = 144.0f; -static constexpr float LARGE_LIST_ICON_H = 80.0f; -static constexpr float LARGE_LIST_PAD = 6.0f; -static constexpr float LARGE_LIST_LEFT_PAD = 11.0f; -static constexpr float LARGE_LIST_INFO_SIZE = 64.0f; // Folders get bigger lines too, though not as big as the games. static constexpr float LARGE_LIST_FOLDER_H = 50.0f; -// Where the text starts in the list modes. -static float ListTextX(GameBrowserViewMode viewMode) { - if (viewMode == GameBrowserViewMode::LARGE_LIST) { - return LARGE_LIST_LEFT_PAD + LARGE_LIST_ICON_W * g_Config.fGameGridScale + 20.0f; - } - return 150.0f; -} - -// Space to the right of the text in the list modes, for the info button. -static float ListTextRightMargin(GameBrowserViewMode viewMode) { - if (viewMode == GameBrowserViewMode::LARGE_LIST) { - return LARGE_LIST_INFO_SIZE + LARGE_LIST_PAD * 2; - } - return 0.0f; -} - -// In the list views, rightText (like a game ID) goes at the right edge, as for a game with a picture. -static void DrawIconWithText(UIContext &dc, ImageID image, std::string_view text, const Bounds &bounds, GameBrowserViewMode viewMode, const UI::Style &style, std::string_view rightText = {}) { - const bool gridStyle = viewMode == GameBrowserViewMode::GRID; - float tw, th; - dc.MeasureText(dc.GetFontStyle(), gridStyle ? g_Config.fGameGridScale : 1.0, gridStyle ? g_Config.fGameGridScale : 1.0, text, &tw, &th, 0); - - const bool compact = bounds.w < 180 * (gridStyle ? g_Config.fGameGridScale : 1.0); - if (compact) { - dc.PushScissor(bounds); - const FontStyle *fontStyle = GetTextStyle(dc, UI::TextSize::Small); - dc.SetFontStyle(*GetTextStyle(dc, UI::TextSize::Small)); - - int iconSize = image == ImageID("I_UP_DIRECTORY") ? (float)dc.Draw()->GetAtlas()->getImage(image)->h : bounds.h * 0.3f; - float textWidth = 0.0f; - float textHeight = 0; - dc.MeasureTextRect(*fontStyle, 1.0f, 1.0f, text, bounds.w - 10, &textWidth, &textHeight, ALIGN_HCENTER | FLAG_WRAP_TEXT); - - int totalHeight = iconSize + (int)textHeight; - - const float y = std::max(0.0f, bounds.h / 2.0f - totalHeight / 2.0f); - - if (image.isValid()) { - const AtlasImage *img = dc.Draw()->GetAtlas()->getImage(image); - if (img && img->h > 0) { - dc.RebindTexture(); - dc.Draw()->DrawImage(image, bounds.centerX(), bounds.y + y + 2, iconSize / (float)img->h, style.fgColor, ALIGN_TOP | ALIGN_HCENTER); - } - } - - if (image != ImageID("I_UP_DIRECTORY") && image != ImageID("I_PIN") && image != ImageID("I_UNPIN")) { - dc.DrawTextRect(text, bounds.Inset(5, y + iconSize + 4, 5, 2), style.fgColor, ALIGN_HCENTER | FLAG_WRAP_TEXT); - } - dc.SetFontStyle(dc.GetTheme().uiFont); - dc.PopScissor(); - } else { - const float textX = ListTextX(viewMode); - if (viewMode == GameBrowserViewMode::LARGE_LIST) { - const float scale = g_Config.fGameGridScale; - dc.Draw()->DrawImage(image, bounds.x + LARGE_LIST_LEFT_PAD + LARGE_LIST_ICON_W * 0.5f * scale, bounds.centerY(), 0.88f * scale, style.fgColor, ALIGN_CENTER); - } else { - dc.Draw()->DrawImage(image, bounds.x + 72, bounds.centerY(), 0.88f * (gridStyle ? g_Config.fGameGridScale : 1.0), style.fgColor, ALIGN_CENTER); - } - - float tx = textX; - int availableWidth = bounds.w - textX - ListTextRightMargin(viewMode); - if (!rightText.empty()) { - float vw, vh; - dc.MeasureText(dc.GetFontStyle(), 0.7f, 0.7f, rightText, &vw, &vh, 0); - availableWidth -= vw + 20; - dc.SetFontScale(0.7f, 0.7f); - dc.DrawText(rightText, bounds.x + availableWidth + textX + 10, bounds.centerY(), style.fgColor, ALIGN_VCENTER); - dc.SetFontScale(1.0f, 1.0f); - } - float sineWidth = std::max(0.0f, (tw - availableWidth)) / 2.0f; - if (availableWidth < tw) { - tx -= (1.0f + sin(time_now_d() * 1.5f)) * sineWidth; - Bounds tb = bounds; - tb.x = bounds.x + textX; - tb.w = availableWidth; - dc.PushScissor(tb); - } - dc.DrawText(text, bounds.x + tx, bounds.centerY(), style.fgColor, ALIGN_VCENTER | FLAG_WRAP_TEXT); - if (availableWidth < tw) { - dc.PopScissor(); - } - } -} - -class GameButton : public UI::Clickable { -public: - GameButton(const Path &gamePath, GameBrowserViewMode viewMode, UI::LayoutParams *layoutParams = nullptr) - : UI::Clickable(layoutParams), viewMode_(viewMode), gamePath_(gamePath) {} - - void Draw(UIContext &dc) override; - std::string DescribeText() const override; - std::string SearchAlias() const override { - // Without the extension, or searching for "iso" would match every ISO. Only three or more - // letters counts as one, so a folder like "Game v1.2" keeps its version number. - std::string filename = gamePath_.GetFilename(); - const std::string ext = gamePath_.GetFileExtension(); - if (ext.size() >= 4 && ext.size() <= filename.size() && std::all_of(ext.begin() + 1, ext.end(), [](char c) { return c >= 'a' && c <= 'z'; })) { - filename.resize(filename.size() - ext.size()); - } - return filename; - } - void GetContentDimensions(const UIContext &dc, float &w, float &h) const override { - switch (viewMode_) { - case GameBrowserViewMode::GRID: - w = 144 * g_Config.fGameGridScale; - h = 80 * g_Config.fGameGridScale; - break; - case GameBrowserViewMode::LARGE_LIST: - w = 500; - h = LARGE_LIST_ICON_H * g_Config.fGameGridScale + LARGE_LIST_PAD * 2; - break; - default: // List - w = 500; - h = 50; - break; - } - } - const Path &GamePath() const { return gamePath_; } - - void SetHoldEnabled(bool hold) { - holdEnabled_ = hold; - } - bool Touch(const TouchInput &input) override { - hovering_ = bounds_.Contains(input.x, input.y); - if (HasInfoButton() && TouchInfoButton(input)) { - return true; - } - const bool retval = UI::Clickable::Touch(input); - if (hovering_ && (input.flags & TouchInputFlags::DOWN)) { - holdStart_ = time_now_d(); - } - if (input.flags & TouchInputFlags::UP) { - holdStart_ = 0; - } - return retval; - } - - bool Key(const KeyInput &key) override { - bool showInfo = false; - - if (HasFocus() && UI::IsInfoKey(key)) { - // If it's the button that's mapped to triangle, then show the info. - if (key.flags & KeyInputFlags::DOWN) { - infoKeyPressed_ = true; - } - if ((key.flags & KeyInputFlags::UP) && infoKeyPressed_) { - showInfo = true; - infoKeyPressed_ = false; - } - } else if (hovering_ && key.deviceId == DEVICE_ID_MOUSE && key.keyCode == NKCODE_EXT_MOUSEBUTTON_2) { - // If it's the right mouse button, and it's not otherwise mapped, show the info also. - if (key.flags & KeyInputFlags::DOWN) { - showInfoPressed_ = true; - } - if ((key.flags & KeyInputFlags::UP) && showInfoPressed_) { - showInfo = true; - showInfoPressed_ = false; - } - } - - if (showInfo) { - TriggerOnHoldClick(); - return true; - } - - if (infoSelected_ && HasFocus() && UI::IsAcceptKey(key)) { - if (key.flags & KeyInputFlags::DOWN) { - infoButtonDown_ = true; - } - if ((key.flags & KeyInputFlags::UP) && infoButtonDown_) { - infoButtonDown_ = false; - TriggerOnHoldClick(); - } - return true; - } - - // Holding the accept button works like a long press, so a gamepad can do it too. - if (holdEnabled_ && HasFocus() && UI::IsAcceptKey(key) && !(key.flags & KeyInputFlags::IS_REPEAT)) { - if (key.flags & KeyInputFlags::DOWN) { - holdStart_ = time_now_d(); - } - if (key.flags & KeyInputFlags::UP) { - holdStart_ = 0.0; - } - } - - return Clickable::Key(key); - } - - void Update() override { - // Hold button for 1.5 seconds to launch the game options - if (holdEnabled_ && holdStart_ != 0.0 && holdStart_ < time_now_d() - 1.5) { - TriggerOnHoldClick(); - } - } - - void FocusChanged(UI::FocusFlags focusFlags) override { - UI::Clickable::FocusChanged(focusFlags); - if (focusFlags & UI::FocusFlags::LOST_FOCUS) { - // Don't let a held accept button carry over from a button we've left. - holdStart_ = 0.0; - // Moving to another game selects the game, not its info button. - infoSelected_ = false; - infoButtonDown_ = false; - } - TriggerOnHighlight(focusFlags); - } - - // Left and right move between the game and its info button, so up and down still go from game to game. - bool CanMoveFocus(UI::FocusMove dir) const override { - if (!HasInfoButton()) { - return true; - } - // We handle these moves ourselves, so MoveFocus won't play the sound for them. - if (dir == UI::FocusMove::RIGHT && !infoSelected_) { - infoSelected_ = true; - UI::PlayUISound(UI::UISound::SELECT); - return false; - } - if (dir == UI::FocusMove::LEFT && infoSelected_) { - infoSelected_ = false; - UI::PlayUISound(UI::UISound::SELECT); - return false; - } - return true; - } - - void DeviceLost() override { - if (video_) { - video_->DeviceLost(); - } - } - - UI::Event OnHoldClick; - UI::Event OnHighlight; - -private: - void TriggerOnHoldClick() { - holdStart_ = 0.0; - UI::EventParams e{}; - e.v = this; - e.s = gamePath_.ToString(); - down_ = false; - OnHoldClick.Trigger(e); - } - void TriggerOnHighlight(UI::FocusFlags focusFlags) { - UI::EventParams e{}; - e.v = this; - e.s = gamePath_.ToString(); - e.a = (u32)focusFlags; - OnHighlight.Trigger(e); - } - - void UpdateVideo(const std::shared_ptr &ginfo, bool wantVideo); - - // The large list has room for an info button on each line, doing the same as a long press. - bool HasInfoButton() const { - return viewMode_ == GameBrowserViewMode::LARGE_LIST && holdEnabled_; - } - Bounds InfoButtonBounds() const { - return Bounds(bounds_.x2() - LARGE_LIST_INFO_SIZE - LARGE_LIST_PAD, bounds_.centerY() - LARGE_LIST_INFO_SIZE * 0.5f, LARGE_LIST_INFO_SIZE, LARGE_LIST_INFO_SIZE); - } - // Returns true if the info button took the input. - bool TouchInfoButton(const TouchInput &input) { - if ((input.flags & TouchInputFlags::MOUSE) && (input.buttons & 1) == 0) { - return false; - } - const bool inside = InfoButtonBounds().Contains(input.x, input.y); - if (input.flags & TouchInputFlags::DOWN) { - infoButtonDown_ = inside; - } - if (!infoButtonDown_) { - return false; - } - if (input.flags & TouchInputFlags::UP) { - infoButtonDown_ = false; - if (inside && !(input.flags & TouchInputFlags::CANCEL)) { - TriggerOnHoldClick(); - } - } - return true; - } - - const GameBrowserViewMode viewMode_; - const Path gamePath_; - std::string title_; - - // The ICON1.PMF video, played in place of the icon in the large list, like the XMB does. - std::unique_ptr video_; - Instant videoStart_; - - double holdStart_ = 0.0; - bool holdEnabled_ = true; - bool showInfoPressed_ = false; - bool infoKeyPressed_ = false; - bool hovering_ = false; - bool infoButtonDown_ = false; - // The info button has the keyboard focus, rather than the game. Changed from CanMoveFocus, hence mutable. - mutable bool infoSelected_ = false; -}; - -void GameButton::UpdateVideo(const std::shared_ptr &ginfo, bool wantVideo) { - if (!wantVideo) { - video_.reset(); - return; - } - if (video_ || !ginfo->Ready(GameInfoFlags::ICON1_PMF)) { - return; - } - std::string data; - { - std::lock_guard guard(ginfo->lock); - data = ginfo->icon1pmf; - } - if (!data.empty()) { - video_ = std::make_unique(std::move(data)); - videoStart_ = Instant::Now(); - } -} - -void GameButton::Draw(UIContext &dc) { - // Like the XMB, only the selected game plays its video. Only keyboard/gamepad focus selects, since with - // touch or the mouse, getting a video to play without launching the game was too fiddly. - const bool wantVideo = viewMode_ == GameBrowserViewMode::LARGE_LIST && HasFocus() && pmf_player_available(); - - GameInfoFlags infoFlags = GameInfoFlags::PARAM_SFO | GameInfoFlags::ICON; - if (wantVideo) { - infoFlags |= GameInfoFlags::ICON1_PMF; - } - std::shared_ptr ginfo = g_gameInfoCache->GetInfo(dc.GetDrawContext(), gamePath_, infoFlags); - UpdateVideo(ginfo, wantVideo); - Draw::Texture *texture = nullptr; - u32 color = 0, shadowColor = 0; - using namespace UI; - - UI::Style style = dc.GetTheme().itemStyle; - if (down_) { - style = dc.GetTheme().itemDownStyle; - } - - // Some types we just draw a default icon for. - ImageID imageIcon = ImageID::invalid(); - bool drawBackground = true; - switch (ginfo->fileType) { - case IdentifiedFileType::PSP_ELF: - imageIcon = ImageID("I_APP"); - // The XMB is an ELF too, but has an icon. - drawBackground = ginfo->region == GameRegion::VSH; - break; - case IdentifiedFileType::UNKNOWN_ELF: imageIcon = ImageID("I_APP"); drawBackground = false; break; - case IdentifiedFileType::PPSSPP_GE_DUMP: imageIcon = ImageID("I_DISPLAY"); break; - case IdentifiedFileType::PSX_ISO: - case IdentifiedFileType::PSP_PS1_PBP: imageIcon = ImageID("I_PSX_ISO"); break; - case IdentifiedFileType::PS2_ISO: imageIcon = ImageID("I_PS2_ISO"); break; - case IdentifiedFileType::PS3_ISO: imageIcon = ImageID("I_PS3_ISO"); break; - case IdentifiedFileType::PSP_UMD_VIDEO_ISO: imageIcon = ImageID("I_UMD_VIDEO_ISO"); break; - case IdentifiedFileType::UNKNOWN_ISO: imageIcon = ImageID("I_UNKNOWN_ISO"); break; - case IdentifiedFileType::PPSSPP_SAVESTATE: - case IdentifiedFileType::ERROR_IDENTIFYING: - case IdentifiedFileType::UNKNOWN_BIN: imageIcon = ImageID("I_FILE"); break; - case IdentifiedFileType::ARCHIVE_ZIP: imageIcon = ImageID("I_ARCHIVE_ZIP"); drawBackground = false; break; - case IdentifiedFileType::ARCHIVE_7Z: imageIcon = ImageID("I_ARCHIVE_7Z"); drawBackground = false; break; - case IdentifiedFileType::ARCHIVE_RAR: imageIcon = ImageID("I_ARCHIVE_RAR"); drawBackground = false; break; - case IdentifiedFileType::PSP_PKG: imageIcon = ImageID("I_FOLDER_UPLOAD"); drawBackground = false; break; - default: break; - } - - if (ginfo->Ready(GameInfoFlags::ICON)) { - if (ginfo->icon.texture && drawBackground) { - texture = ginfo->icon.texture; - // The type icon is only for when there's no picture, and would be drawn on top of it. - imageIcon = ImageID::invalid(); - } else if (drawBackground) { - // No icon, but drawBackground is set. Let's show a plain icon depending on type. - if (ginfo->fileType == IdentifiedFileType::PSP_ISO || ginfo->fileType == IdentifiedFileType::PSP_ISO_NP) { - imageIcon = ImageID("I_UMD"); - } else if (ginfo->fileType == IdentifiedFileType::PSX_ISO || ginfo->fileType == IdentifiedFileType::PSP_PS1_PBP) { - imageIcon = ImageID("I_PSX_ISO"); - } else if (gamePath_.Type() == PathType::HTTP) { - imageIcon = ginfo->fileType == IdentifiedFileType::UNKNOWN ? ImageID("I_LINK_OUT_QUESTION") : ImageID("I_LINK_OUT"); - } else { - imageIcon = ImageID("I_APP"); - } - } - } else { - // Make sure that pending HTTP icons show something, instead of just blank. - if (gamePath_.Type() == PathType::HTTP) { - imageIcon = ImageID("I_LINK_OUT_QUESTION"); - } - } - - Bounds overlayBounds = bounds_; - u32 overlayColor = 0; - if (holdEnabled_ && holdStart_ != 0.0) { - double time_held = time_now_d() - holdStart_; - overlayColor = whiteAlpha(time_held / 2.5f); - if (holdStart_ != 0.0) { - double time_held = time_now_d() - holdStart_; - int holdFrameCount = (int)(time_held * 60.0f); - if (holdFrameCount > 60) { - // Blink before launching by holding - if (((holdFrameCount >> 3) & 1) == 0) - overlayColor = 0x0; - } - } - } - - int x = bounds_.x; - int y = bounds_.y; - int w = viewMode_ == GameBrowserViewMode::GRID ? bounds_.w : 144; - int h = bounds_.h; - if (viewMode_ == GameBrowserViewMode::LARGE_LIST) { - x += LARGE_LIST_LEFT_PAD; - y += LARGE_LIST_PAD; - w = LARGE_LIST_ICON_W * g_Config.fGameGridScale; - h = LARGE_LIST_ICON_H * g_Config.fGameGridScale; - } - const float textX = ListTextX(viewMode_); - - const bool rowFocused = HasFocus() && !infoSelected_; - if (viewMode_ != GameBrowserViewMode::GRID || !texture) { - if (rowFocused) - 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 && video_) { - Draw::Texture *frame = video_->Update(dc.GetDrawContext(), videoStart_.ElapsedSeconds()); - if (frame) { - texture = frame; - } - } - - 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. - // Make sure it's not wider than 144 (like Doom Legacy homebrew), ugly in the grid mode. - float nw = std::min(h * tw / th, (float)w); - x += (w - nw) / 2.0f; - w = nw; - } - - int txOffset = down_ ? 4 : 0; - if (viewMode_ != GameBrowserViewMode::GRID) txOffset = 0; - - // Render button - int dropsize = 10; - if (texture) { - if (viewMode_ == GameBrowserViewMode::LIST) { - x += 4; - } - 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(); - } - - 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(); - } - - if (viewMode_ == GameBrowserViewMode::GRID && g_Config.bShowIDOnGameIcon && ginfo->fileType != IdentifiedFileType::PSP_ELF && !ginfo->id_version.empty()) { - std::string_view idStr = ginfo->id_version; - if (ginfo->fileType == IdentifiedFileType::PSP_SAVEDATA_DIRECTORY) { - auto ga = GetI18NCategory(I18NCat::GAME); - idStr = ga->T("SaveData"); - } - dc.SetFontScale(0.5f * g_Config.fGameGridScale, 0.5f * g_Config.fGameGridScale); - dc.DrawText(idStr, bounds_.x + 5, y + 1, 0xFF000000, ALIGN_TOPLEFT); - dc.DrawText(idStr, bounds_.x + 4, y, dc.GetTheme().infoStyle.fgColor, ALIGN_TOPLEFT); - dc.SetFontScale(1.0f, 1.0f); - } - - if (imageIcon.isValid()) { - Style style = dc.GetTheme().itemStyle; - - if (rowFocused) style = dc.GetTheme().itemFocusedStyle; - if (down_) style = dc.GetTheme().itemDownStyle; - if (!IsEnabled()) style = dc.GetTheme().itemDisabledStyle; - - DrawIconWithText(dc, imageIcon, ginfo->GetTitle(), bounds_, viewMode_, style, g_Config.bShowIDOnGameIcon ? std::string_view(ginfo->id_version) : std::string_view()); - - if (overlayColor) { - dc.FillRect(Drawable(overlayColor), overlayBounds); - } - } else { - char discNumInfo[8]; - if (ginfo->disc_total > 1) - snprintf(discNumInfo, sizeof(discNumInfo), "-DISC%d", ginfo->disc_number); - else - discNumInfo[0] = '\0'; - - dc.Draw()->Flush(); - dc.RebindTexture(); - dc.SetFontStyle(dc.GetTheme().uiFont); - if (viewMode_ != GameBrowserViewMode::GRID) { - float tw, th; - dc.Draw()->Flush(); - dc.PushScissor(bounds_); - const std::string currentTitle = ginfo->GetTitle(); - if (!currentTitle.empty()) { - title_ = ReplaceAll(currentTitle, "\n", " "); - } - - dc.MeasureText(dc.GetFontStyle(), 1.0f, 1.0f, title_, &tw, &th, 0); - - int availableWidth = bounds_.w - textX - ListTextRightMargin(viewMode_); - if (g_Config.bShowIDOnGameIcon) { - float vw, vh; - dc.MeasureText(dc.GetFontStyle(), 0.7f, 0.7f, ginfo->id_version, &vw, &vh, 0); - availableWidth -= vw + 20; - dc.SetFontScale(0.7f, 0.7f); - dc.DrawText(ginfo->id_version, bounds_.x + availableWidth + textX + 10, bounds_.centerY(), style.fgColor, ALIGN_VCENTER); - dc.SetFontScale(1.0f, 1.0f); - } - float sineWidth = std::max(0.0f, (tw - availableWidth)) / 2.0f; - - float tx = textX; - if (availableWidth < tw) { - tx -= (1.0f + sin(time_now_d() * 1.5f)) * sineWidth; - Bounds tb = bounds_; - tb.x = bounds_.x + textX; - tb.w = availableWidth; - dc.PushScissor(tb); - } - dc.DrawText(title_, bounds_.x + tx, bounds_.centerY(), style.fgColor, ALIGN_VCENTER); - if (availableWidth < tw) { - dc.PopScissor(); - } - dc.Draw()->Flush(); - dc.PopScissor(); - } else if (!texture) { - dc.Draw()->Flush(); - dc.PushScissor(bounds_); - dc.DrawText(title_, bounds_.x + 4, bounds_.centerY(), style.fgColor, ALIGN_VCENTER); - dc.Draw()->Flush(); - dc.PopScissor(); - } else { - dc.Draw()->Flush(); - } - } - - if (ginfo->hasConfig && !ginfo->id.empty()) { - const AtlasImage *gearImage = dc.Draw()->GetAtlas()->getImage(ImageID("I_GEAR_SMALL")); - if (gearImage) { - if (viewMode_ == GameBrowserViewMode::GRID) { - dc.Draw()->DrawImage(ImageID("I_GEAR_SMALL"), bounds_.x, y + h - gearImage->h * g_Config.fGameGridScale, g_Config.fGameGridScale); - } else if (viewMode_ == GameBrowserViewMode::LARGE_LIST) { - dc.Draw()->DrawImage(ImageID("I_GEAR_SMALL"), bounds_.x + LARGE_LIST_LEFT_PAD, y + h - gearImage->h * g_Config.fGameGridScale, g_Config.fGameGridScale); - } else { - dc.Draw()->DrawImage(ImageID("I_GEAR_SMALL"), bounds_.x + 4, y, 1.0f); - } - } - } - - const int regionIndex = (int)ginfo->region; - if (g_Config.bShowRegionOnGameIcon && regionIndex >= 0 && regionIndex < (int)GameRegion::COUNT) { - const ImageID regionIcons[(int)GameRegion::COUNT] = { - ImageID("I_FLAG_JP"), - ImageID("I_FLAG_US"), - ImageID("I_FLAG_EU"), - ImageID("I_FLAG_HK"), - ImageID("I_FLAG_AS"), - ImageID("I_FLAG_KO"), - }; - const AtlasImage *image = dc.Draw()->GetAtlas()->getImage(regionIcons[regionIndex]); - if (image) { - if (viewMode_ == GameBrowserViewMode::GRID) { - dc.Draw()->DrawImage(regionIcons[regionIndex], bounds_.x + bounds_.w - (image->w + 5) * g_Config.fGameGridScale, - y + h - (image->h + 5) * g_Config.fGameGridScale, g_Config.fGameGridScale); - } else if (viewMode_ == GameBrowserViewMode::LARGE_LIST) { - dc.Draw()->DrawImage(regionIcons[regionIndex], bounds_.x + LARGE_LIST_LEFT_PAD + (LARGE_LIST_ICON_W - image->w - 5) * g_Config.fGameGridScale, - y + h - (image->h + 5) * g_Config.fGameGridScale, g_Config.fGameGridScale); - } else { - dc.Draw()->DrawImage(regionIcons[regionIndex], bounds_.x + 4, y + h - image->h - 5, 1.0f); - } - } - } - - if (HasInfoButton()) { - const Bounds infoBounds = InfoButtonBounds(); - Style infoStyle = HasFocus() ? dc.GetTheme().itemFocusedStyle : dc.GetTheme().itemStyle; - if (infoButtonDown_) { - infoStyle = dc.GetTheme().itemDownStyle; - dc.FillRoundRect(infoStyle.background, infoBounds, dc.GetTheme().choiceRoundRadius); - } else if (HasFocus() && infoSelected_) { - dc.FillRoundRect(infoStyle.background, infoBounds, dc.GetTheme().choiceRoundRadius); - } - dc.Draw()->DrawImage(ImageID("I_INFO"), infoBounds.centerX(), infoBounds.centerY(), 1.0f, infoStyle.fgColor, ALIGN_CENTER); - } - - if (overlayColor) { - dc.FillRect(Drawable(overlayColor), overlayBounds); - } - - // Covers the icon while the game is being compressed. Clicking opens the game screen instead of playing. - GamePacker::Status packStatus; - if (GamePacker::IsPacking(gamePath_) && GamePacker::GetStatus(&packStatus)) { - auto ga = GetI18NCategory(I18NCat::GAME); - const Bounds iconBounds(x, y, w, h); - dc.FillRect(Drawable(0xD0505050), iconBounds); - const float scale = viewMode_ == GameBrowserViewMode::GRID || viewMode_ == GameBrowserViewMode::LARGE_LIST ? 0.8f * g_Config.fGameGridScale : 0.6f; - dc.SetFontStyle(dc.GetTheme().uiFont); - dc.SetFontScale(scale, scale); - const std::string text = std::string(ga->T("Compressing...")) + StringFromFormat("\n%d%%", (int)(packStatus.progress * 100.0f)); - dc.DrawTextRectSqueeze(text, iconBounds, 0xFFFFFFFF, ALIGN_CENTER); - dc.SetFontScale(1.0f, 1.0f); - } - - dc.RebindTexture(); -} - -std::string GameButton::DescribeText() const { - // LOW: search calls this for every game in the list, which mustn't hold up the visible ones. - std::shared_ptr ginfo = g_gameInfoCache->GetInfo(nullptr, gamePath_, GameInfoFlags::PARAM_SFO, nullptr, GameInfoFlags::EMPTY, TaskPriority::LOW); - if (!ginfo->Ready(GameInfoFlags::PARAM_SFO)) - return "..."; - auto u = GetI18NCategory(I18NCat::UI_ELEMENTS); - return ginfo->GetTitle(); -} - -class DirButton : public UI::Button { -public: - DirButton(const Path &path, GameBrowserViewMode viewMode, UI::LayoutParams *layoutParams) - : UI::Button(path.ToString(), layoutParams), path_(path), viewMode_(viewMode), absolute_(false) {} - DirButton(const Path &path, const std::string &text, GameBrowserViewMode viewMode, UI::LayoutParams *layoutParams = 0) - : UI::Button(text, layoutParams), path_(path), viewMode_(viewMode), absolute_(true) {} - - void Draw(UIContext &dc) override; - void GetContentDimensions(const UIContext &dc, float &w, float &h) const override { - UI::Button::GetContentDimensions(dc, w, h); - if (viewMode_ == GameBrowserViewMode::LARGE_LIST) { - h = std::max(h, LARGE_LIST_FOLDER_H * g_Config.fGameGridScale + LARGE_LIST_PAD * 2); - } - } - - const Path &GetPath() const { - return path_; - } - - bool PathAbsolute() const { - return absolute_; - } - - void SetPinned(bool pin) { - pinned_ = pin; - } - - std::string DescribeText() const override { - return std::string(Button::GetText()); // From Button - } - -private: - Path path_; - GameBrowserViewMode viewMode_; - bool absolute_; - bool pinned_ = false; -}; - -void DirButton::Draw(UIContext &dc) { - using namespace UI; - - std::string_view text(GetText()); - ImageID image = ImageID(pinned_ ? "I_FOLDER_PINNED" : "I_FOLDER"); - if (path_.IsNetworkRoot()) { - image = ImageID("I_NETWORK"); - } else if (text == "..") { - image = ImageID("I_UP_DIRECTORY"); - text = ""; - } - - Style style = dc.GetTheme().itemStyle; - - if (HasFocus()) style = dc.GetTheme().itemFocusedStyle; - if (down_) style = dc.GetTheme().itemDownStyle; - if (!IsEnabled()) style = dc.GetTheme().itemDisabledStyle; - - dc.FillRect(style.background, bounds_); - DrawIconWithText(dc, image, text, bounds_, viewMode_, style); -} - GameBrowser::GameBrowser(int token, const Path &path, BrowseFlags browseFlags, bool portrait, int *viewMode, ScreenManager *screenManager, std::string_view lastText, std::string_view lastLink, UI::LayoutParams *layoutParams) : LinearLayout(ORIENT_VERTICAL, layoutParams), viewMode_(viewMode), browseFlags_(browseFlags), portrait_(portrait), lastText_(lastText), lastLink_(lastLink), screenManager_(screenManager), token_(token) { using namespace UI; @@ -787,19 +74,19 @@ GameBrowser::GameBrowser(int token, const Path &path, BrowseFlags browseFlags, b Refresh(); } -void GameBrowser::FocusGame(const Path &gamePath) { +void GameBrowser::FocusGame(const Path &GetPath) { // If the button's still there, just focus it. That keeps the search results and scroll position. if (gameList_) { for (int i = 0; i < gameList_->GetNumSubviews(); i++) { GameButton *button = dynamic_cast(gameList_->GetViewByIndex(i)); - if (button && button->GamePath() == gamePath && button->GetVisibility() == UI::V_VISIBLE) { + if (button && button->GetPath() == GetPath && button->GetVisibility() == UI::V_VISIBLE) { button->SetFocus(UI::FocusFlags::CAUSE_RESTORE); return; } } } // Otherwise, Refresh focuses it when it recreates the buttons. - focusPath_ = gamePath; + focusPath_ = GetPath; refreshPending_ = true; } @@ -1139,7 +426,7 @@ void GameBrowser::Refresh() { Add(gameList_); // Find games in the current directory and create new ones. - std::vector dirButtons; + std::vector dirButtons; std::vector gameButtons; listingPending_ = !path_.IsListingReady(); @@ -1149,7 +436,7 @@ void GameBrowser::Refresh() { std::vector filenames; if (HasSpecialFiles(filenames)) { for (size_t i = 0; i < filenames.size(); i++) { - gameButtons.push_back(new GameButton(filenames[i], lastViewMode_, new UI::LinearLayoutParams(gridStyle ? UI::WRAP_CONTENT : UI::FILL_PARENT, UI::WRAP_CONTENT))); + gameButtons.push_back(new GameButton(GameButtonType::Game, filenames[i], lastViewMode_, new UI::LinearLayoutParams(gridStyle ? UI::WRAP_CONTENT : UI::FILL_PARENT, UI::WRAP_CONTENT))); } } else if (!listingPending_) { std::vector fileInfo; @@ -1173,11 +460,13 @@ void GameBrowser::Refresh() { if (!isGame && !isSaveData) { if (browseFlags_ & BrowseFlags::NAVIGATE) { - const std::string name = fileInfo[i].fullName.IsNetworkRoot() ? std::string(mm->T("Network")) : fileInfo[i].name; - dirButtons.push_back(new DirButton(fileInfo[i].fullName, name, lastViewMode_, new UI::LinearLayoutParams(UI::FILL_PARENT, UI::FILL_PARENT))); + dirButtons.push_back(new GameButton(GameButtonType::Dir, fileInfo[i].fullName, lastViewMode_, new UI::LinearLayoutParams(UI::FILL_PARENT, UI::FILL_PARENT))); + if (fileInfo[i].fullName.IsNetworkRoot()) { + dirButtons.back()->SetAsAbsolute(); + } } } else { - gameButtons.push_back(new GameButton(fileInfo[i].fullName, lastViewMode_, new UI::LinearLayoutParams(gridStyle ? UI::WRAP_CONTENT : UI::FILL_PARENT, UI::WRAP_CONTENT))); + gameButtons.push_back(new GameButton(GameButtonType::Game, fileInfo[i].fullName, lastViewMode_, new UI::LinearLayoutParams(gridStyle ? UI::WRAP_CONTENT : UI::FILL_PARENT, UI::WRAP_CONTENT))); } } @@ -1190,8 +479,7 @@ void GameBrowser::Refresh() { if (!fileInfo.empty()) { for (size_t i = 0; i < fileInfo.size(); i++) { if (!fileInfo[i].isDirectory) { - GameButton *b = new GameButton(fileInfo[i].fullName, lastViewMode_, new UI::LinearLayoutParams(UI::FILL_PARENT, UI::WRAP_CONTENT)); - b->SetHoldEnabled(false); + GameButton *b = new GameButton(GameButtonType::Other, fileInfo[i].fullName, lastViewMode_, new UI::LinearLayoutParams(UI::FILL_PARENT, UI::WRAP_CONTENT)); gameButtons.push_back(b); } } @@ -1199,19 +487,20 @@ void GameBrowser::Refresh() { } } - DirButton *upButton = nullptr; + GameButton *upButton = nullptr; if (browseFlags_ & BrowseFlags::NAVIGATE) { if (path_.CanNavigateUp()) { - upButton = gameList_->Add(new DirButton(Path(".."), lastViewMode_, new UI::LinearLayoutParams(UI::FILL_PARENT, UI::FILL_PARENT))); + upButton = gameList_->Add(new GameButton(GameButtonType::Dir, Path(".."), lastViewMode_, new UI::LinearLayoutParams(UI::FILL_PARENT, UI::FILL_PARENT))); upButton->OnClick.Handle(this, &GameBrowser::NavigateClick); } // Add any pinned paths before other directories. auto pinnedPaths = GetPinnedPaths(); for (const auto &pinnedPath : pinnedPaths) { - DirButton *pinnedDir = gameList_->Add(new DirButton(pinnedPath, pinnedPath.GetFilename(), lastViewMode_, new UI::LinearLayoutParams(UI::FILL_PARENT, UI::FILL_PARENT))); + GameButton *pinnedDir = gameList_->Add(new GameButton(GameButtonType::Dir, pinnedPath, lastViewMode_, new UI::LinearLayoutParams(UI::FILL_PARENT, UI::FILL_PARENT))); pinnedDir->OnClick.Handle(this, &GameBrowser::NavigateClick); pinnedDir->SetPinned(true); + pinnedDir->SetAsAbsolute(); } } @@ -1232,7 +521,7 @@ void GameBrowser::Refresh() { b->OnHoldClick.Handle(this, &GameBrowser::GameButtonHoldClick); b->OnHighlight.Handle(this, &GameBrowser::GameButtonHighlight); - if (!focusPath_.empty() && b->GamePath() == focusPath_ && b->SetFocus(FocusFlags::CAUSE_RESTORE)) { + if (!focusPath_.empty() && b->GetPath() == focusPath_ && b->SetFocus(FocusFlags::CAUSE_RESTORE)) { UI::ScrollFocusedViewIntoView(); } } @@ -1250,6 +539,7 @@ void GameBrowser::Refresh() { } // Show a button to toggle pinning at the very end. + // TODO: That's not very friendly. if ((browseFlags_ & BrowseFlags::PIN) && !path_.GetPath().empty()) { std::string caption = ""; // IsCurrentPathPinned() ? "-" : "+"; if (!gridStyle) { @@ -1317,16 +607,22 @@ std::vector GameBrowser::GetPinnedPaths() const { } void GameBrowser::GameButtonClick(UI::EventParams &e) { - GameButton *button = static_cast(e.v); + GameButton *button = dynamic_cast(e.v); + if (!button) { + return; + } UI::EventParams e2{}; - e2.s = button->GamePath().ToString(); + e2.s = button->GetPath().ToString(); OnChoice.Trigger(e2); } void GameBrowser::GameButtonHoldClick(UI::EventParams &e) { - GameButton *button = static_cast(e.v); + GameButton *button = dynamic_cast(e.v); + if (!button) { + return; + } UI::EventParams e2{}; - e2.s = button->GamePath().ToString(); + e2.s = button->GetPath().ToString(); OnHoldChoice.Trigger(e2); } @@ -1335,15 +631,18 @@ void GameBrowser::GameButtonHighlight(UI::EventParams &e) { } void GameBrowser::NavigateClick(UI::EventParams &e) { - DirButton *button = static_cast(e.v); + GameButton *button = dynamic_cast(e.v); + if (!button) { + return; + } Path text = button->GetPath(); - if (button->PathAbsolute()) { + if (button->NavigateAsAbsolute()) { path_.SetPath(text); } else if (text.ToString() == "..") { NavigateUp(); return; } else { - path_.Navigate(text.ToString()); + path_.Navigate(text.GetFilename()); } focusFirstItem_ = true; PathChanged(); diff --git a/UI/GameButton.cpp b/UI/GameButton.cpp new file mode 100644 index 0000000000..452bcd2e5f --- /dev/null +++ b/UI/GameButton.cpp @@ -0,0 +1,462 @@ +// Copyright (c) 2012- 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 +#include + +#include "Common/Data/Text/I18n.h" +#include "Common/Math/curves.h" +#include "Common/StringUtils.h" +#include "Common/TimeUtil.h" +#include "Common/UI/Context.h" +#include "Core/Config.h" +#include "Core/Loaders.h" +#include "Core/Util/VideoPlayer.h" +#include "UI/GameButton.h" +#include "UI/GameInfoCache.h" +#include "UI/GamePacker.h" + + +// Where the text starts in the list modes. +static float ListTextX(GameBrowserViewMode viewMode) { + if (viewMode == GameBrowserViewMode::LARGE_LIST) { + return LARGE_LIST_LEFT_PAD + LARGE_LIST_ICON_W * g_Config.fGameGridScale + 20.0f; + } + return 150.0f; +} + +// Space to the right of the text in the list modes, for the info button. +static float ListTextRightMargin(GameBrowserViewMode viewMode) { + if (viewMode == GameBrowserViewMode::LARGE_LIST) { + return LARGE_LIST_INFO_SIZE + LARGE_LIST_PAD * 2; + } + return 0.0f; +} + + +void GameButton::UpdateVideo(const std::shared_ptr &ginfo, bool wantVideo) { + if (!wantVideo) { + video_.reset(); + return; + } + if (video_ || !ginfo->Ready(GameInfoFlags::ICON1_PMF)) { + return; + } + std::string data; + { + std::lock_guard guard(ginfo->lock); + data = ginfo->icon1pmf; + } + if (!data.empty()) { + video_ = std::make_unique(std::move(data)); + videoStart_ = Instant::Now(); + } +} + +// These are the built-in icons used for file types that don't have one (or simply files missing one). +static ImageID ChooseGameIcon(const std::shared_ptr &ginfo) { + if (ginfo->GetFilePath().Type() == PathType::HTTP) { + return ginfo->fileType == IdentifiedFileType::UNKNOWN ? ImageID("I_LINK_OUT_QUESTION") : ImageID("I_LINK_OUT"); + } + + switch (ginfo->fileType) { + case IdentifiedFileType::PSP_ELF: + if (ginfo->region == GameRegion::VSH) { + return ImageID::invalid(); // it has a (preset) icon. + } + // The XMB is an ELF too, but has an icon. + return ImageID("I_APP"); + case IdentifiedFileType::UNKNOWN_ELF: return ImageID("I_APP"); break; + case IdentifiedFileType::PPSSPP_GE_DUMP: return ImageID("I_DISPLAY"); break; + case IdentifiedFileType::PSX_ISO: + case IdentifiedFileType::PSP_PS1_PBP: return ImageID("I_PSX_ISO"); break; + case IdentifiedFileType::PS2_ISO: return ImageID("I_PS2_ISO"); break; + case IdentifiedFileType::PS3_ISO: return ImageID("I_PS3_ISO"); break; + case IdentifiedFileType::PSP_UMD_VIDEO_ISO: return ImageID("I_UMD_VIDEO_ISO"); break; + case IdentifiedFileType::UNKNOWN_ISO: return ImageID("I_UNKNOWN_ISO"); break; + case IdentifiedFileType::PPSSPP_SAVESTATE: + case IdentifiedFileType::ERROR_IDENTIFYING: + case IdentifiedFileType::UNKNOWN_BIN: return ImageID("I_FILE"); break; + case IdentifiedFileType::ARCHIVE_ZIP: return ImageID("I_ARCHIVE_ZIP"); break; + case IdentifiedFileType::ARCHIVE_7Z: return ImageID("I_ARCHIVE_7Z"); break; + case IdentifiedFileType::ARCHIVE_RAR: return ImageID("I_ARCHIVE_RAR"); break; + case IdentifiedFileType::PSP_PKG: return ImageID("I_FOLDER_UPLOAD"); break; + case IdentifiedFileType::NORMAL_DIRECTORY: return ImageID("I_FOLDER"); break; + + case IdentifiedFileType::PSP_ISO: + case IdentifiedFileType::PSP_ISO_NP: + return ImageID("I_APP"); // This will practically always be replaced by a real icon. + + default: + return ImageID::invalid(); + } +} + +void GameButton::GetContentDimensions(const UIContext &dc, float &w, float &h) const { + switch (viewMode_) { + case GameBrowserViewMode::GRID: + w = 144 * g_Config.fGameGridScale; + h = 80 * g_Config.fGameGridScale; + break; + case GameBrowserViewMode::LARGE_LIST: + w = 500; + if (type_ == GameButtonType::Dir) { + h = UI::ITEM_HEIGHT; + } else { + h = LARGE_LIST_ICON_H * g_Config.fGameGridScale + LARGE_LIST_PAD * 2; + } + break; + default: // List + w = 500; + h = 56; + break; + } +} + +static const ImageID regionIcons[(int)GameRegion::COUNT] = { + ImageID("I_FLAG_JP"), + ImageID("I_FLAG_US"), + ImageID("I_FLAG_EU"), + ImageID("I_FLAG_HK"), + ImageID("I_FLAG_AS"), + ImageID("I_FLAG_KO"), +}; + +void GameButton::Draw(UIContext &dc) { + // Like the XMB, only the selected game plays its video. Only keyboard/gamepad focus selects, since with + // touch or the mouse, getting a video to play without launching the game was too fiddly. + const bool wantVideo = viewMode_ == GameBrowserViewMode::LARGE_LIST && HasFocus() && pmf_player_available(); + + std::shared_ptr ginfo; + + std::string text; + std::string id_version; + Draw::Texture *texture = nullptr; + ImageID imageIcon; + GameRegion region = GameRegion::UNKNOWN; + IdentifiedFileType fileType = IdentifiedFileType::UNKNOWN; + + if (type_ == GameButtonType::Dir) { + text = gamePath_.GetFilename(); + imageIcon = ImageID(IsPinned() ? "I_FOLDER_PINNED" : "I_FOLDER"); + if (gamePath_.IsNetworkRoot()) { + imageIcon = ImageID("I_NETWORK"); + auto mm = GetI18NCategory(I18NCat::MAINMENU); + text = mm->T("Network"); + } else if (text == "..") { + imageIcon = ImageID("I_UP_DIRECTORY"); + text = ""; + } + fileType = IdentifiedFileType::NORMAL_DIRECTORY; + } else { + GameInfoFlags infoFlags = GameInfoFlags::PARAM_SFO | GameInfoFlags::ICON; + if (wantVideo) { + infoFlags |= GameInfoFlags::ICON1_PMF; + } + ginfo = g_gameInfoCache->GetInfo(dc.GetDrawContext(), gamePath_, infoFlags); + UpdateVideo(ginfo, wantVideo); + imageIcon = ChooseGameIcon(ginfo); + if (ginfo->Ready(GameInfoFlags::ICON)) { + if (ginfo->icon.texture) { + texture = ginfo->icon.texture; + // The type icon is only for when there's no picture, and would be drawn on top of it. + imageIcon = ImageID::invalid(); + } else { + // No icon, but drawBackground is set. Let's show a plain icon depending on type. + imageIcon = ChooseGameIcon(ginfo); + } + } else if (gamePath_.Type() == PathType::HTTP) { + // Make sure that pending HTTP icons show something, instead of just blank. + // Maybe we should show something for other file types too. + imageIcon = ImageID("I_LINK_OUT_QUESTION"); + } + text = ginfo->GetTitle(); + text = ReplaceAll(text, "\n", " "); + + id_version = ginfo->id_version; + region = ginfo->region; + fileType = ginfo->fileType; + + if (ginfo->disc_total > 1) { + text += StringFromFormat("-DISC%d", ginfo->disc_number); + } + } + + if (video_) { + Draw::Texture *frame = video_->Update(dc.GetDrawContext(), videoStart_.ElapsedSeconds()); + if (frame) { + texture = frame; + } + } + + u32 color = 0, shadowColor = 0; + using namespace UI; + + UI::Style style = dc.GetTheme().itemStyle; + const bool rowFocused = HasFocus() && !infoSelected_; + if (rowFocused) style = dc.GetTheme().itemFocusedStyle; + if (down_) style = dc.GetTheme().itemDownStyle; + if (!IsEnabled()) style = dc.GetTheme().itemDisabledStyle; + + if (viewMode_ != GameBrowserViewMode::GRID || !texture) { + Drawable bg = style.background; + dc.Draw()->Flush(); + dc.RebindTexture(); + dc.FillRect(bg, bounds_); + dc.Draw()->Flush(); + } + + // Some types we just draw a default icon for. + Bounds iconBounds = bounds_; + iconBounds.w = viewMode_ == GameBrowserViewMode::GRID ? bounds_.w : 144; + if (viewMode_ == GameBrowserViewMode::LARGE_LIST) { + iconBounds.x += LARGE_LIST_LEFT_PAD; + iconBounds.y += LARGE_LIST_PAD; + iconBounds.w = LARGE_LIST_ICON_W * g_Config.fGameGridScale; + iconBounds.h = LARGE_LIST_ICON_H * g_Config.fGameGridScale; + } + const float textX = ListTextX(viewMode_); + + if (texture && ginfo) { + 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. + // Make sure it's not wider than 144 (like Doom Legacy homebrew), ugly in the grid mode. + float nw = std::min(iconBounds.h * tw / th, (float)iconBounds.w); + iconBounds.x += (iconBounds.w - nw) / 2.0f; + iconBounds.w = nw; + } + + int txOffset = down_ ? 4 : 0; + if (viewMode_ != GameBrowserViewMode::GRID) { + txOffset = 0; + } + + Bounds overlayBounds = bounds_; + + // Render the icon, all modes have this. + int dropsize = 10; + if (texture) { + if (viewMode_ == GameBrowserViewMode::LIST) { + iconBounds.x += 4; + } + if (txOffset) { + dropsize = 3; + iconBounds.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.DrawRectDropShadow(iconBounds, 10.0f, pulse, alphaMul(color, pulse)); + dc.Draw()->Flush(); + } else { + dc.Draw()->Flush(); + dc.RebindTexture(); + dc.DrawRectDropShadow(iconBounds, 8.0f, 0.7f, shadowColor); + // 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(); + } + + dc.Draw()->Flush(); + dc.GetDrawContext()->BindTexture(0, texture); + dc.Draw()->DrawTexRect(iconBounds, 0, 0, 1, 1, color); + dc.Draw()->Flush(); + } + + if (viewMode_ == GameBrowserViewMode::GRID && g_Config.bShowIDOnGameIcon && fileType != IdentifiedFileType::PSP_ELF && !id_version.empty()) { + std::string_view idStr = id_version; + if (fileType == IdentifiedFileType::PSP_SAVEDATA_DIRECTORY) { + auto ga = GetI18NCategory(I18NCat::GAME); + idStr = ga->T("SaveData"); + } + dc.SetFontScale(0.5f * g_Config.fGameGridScale, 0.5f * g_Config.fGameGridScale); + dc.DrawText(idStr, bounds_.x + 5, iconBounds.y + 1, 0xFF000000, ALIGN_TOPLEFT); + dc.DrawText(idStr, bounds_.x + 4, iconBounds.y, dc.GetTheme().infoStyle.fgColor, ALIGN_TOPLEFT); + dc.SetFontScale(1.0f, 1.0f); + } + + if (viewMode_ == GameBrowserViewMode::GRID) { + if (imageIcon.isValid()) { + Bounds bounds = bounds_; + + float tw, th; + dc.MeasureText(dc.GetFontStyle(), g_Config.fGameGridScale, g_Config.fGameGridScale, text, &tw, &th, 0); + dc.PushScissor(bounds); + const FontStyle *fontStyle = GetTextStyle(dc, UI::TextSize::Small); + dc.SetFontStyle(*GetTextStyle(dc, UI::TextSize::Small)); + + int iconSize = imageIcon == ImageID("I_UP_DIRECTORY") ? (float)dc.Draw()->GetAtlas()->getImage(imageIcon)->h : bounds.h * 0.3f; + float textWidth = 0.0f; + float textHeight = 0; + dc.MeasureTextRect(*fontStyle, 1.0f, 1.0f, text, bounds.w - 10, &textWidth, &textHeight, ALIGN_HCENTER | FLAG_WRAP_TEXT); + + int totalHeight = iconSize + (int)textHeight; + + const float y = std::max(0.0f, bounds.h / 2.0f - totalHeight / 2.0f); + + const AtlasImage *img = dc.Draw()->GetAtlas()->getImage(imageIcon); + if (img && img->h > 0) { + dc.RebindTexture(); + dc.Draw()->DrawImage(imageIcon, bounds.centerX(), bounds.y + y + 2, iconSize / (float)img->h, style.fgColor, ALIGN_TOP | ALIGN_HCENTER); + } + + if (imageIcon != ImageID("I_UP_DIRECTORY") && imageIcon != ImageID("I_PIN") && imageIcon != ImageID("I_UNPIN")) { + dc.DrawTextRect(text, bounds.Inset(5, y + iconSize + 4, 5, 2), style.fgColor, ALIGN_HCENTER | FLAG_WRAP_TEXT); + } + dc.PopScissor(); + dc.SetFontStyle(dc.GetTheme().uiFont); + } else if (!texture) { + // Just draw the text, centered. + dc.Draw()->Flush(); + dc.PushScissor(bounds_); + dc.DrawText(title_, bounds_.x + 4, bounds_.centerY(), style.fgColor, ALIGN_VCENTER); + dc.Draw()->Flush(); + dc.PopScissor(); + } + } else { + Bounds bounds = bounds_; + + std::string_view rightText = g_Config.bShowIDOnGameIcon ? std::string_view(id_version) : std::string_view(); + + float tw, th; + dc.MeasureText(dc.GetFontStyle(), 1.0f, 1.0f, text, &tw, &th, 0); + + if (imageIcon.isValid()) { + const float textX = ListTextX(viewMode_); + if (viewMode_ == GameBrowserViewMode::LARGE_LIST) { + const float scale = g_Config.fGameGridScale; + dc.Draw()->DrawImage(imageIcon, bounds.x + LARGE_LIST_LEFT_PAD + LARGE_LIST_ICON_W * 0.5f * scale, bounds.centerY(), 0.88f * scale, style.fgColor, ALIGN_CENTER); + } else { + dc.Draw()->DrawImage(imageIcon, bounds.x + 72, bounds.centerY(), 0.88f, style.fgColor, ALIGN_CENTER); + } + } + + float tx = textX; + int availableWidth = bounds.w - textX - ListTextRightMargin(viewMode_); + if (!rightText.empty()) { + float vw, vh; + dc.MeasureText(dc.GetFontStyle(), 0.7f, 0.7f, rightText, &vw, &vh, 0); + availableWidth -= vw + 20; + dc.SetFontScale(0.7f, 0.7f); + dc.DrawText(rightText, bounds.x + availableWidth + textX + 10, bounds.centerY(), style.fgColor, ALIGN_VCENTER); + dc.SetFontScale(1.0f, 1.0f); + } + float sineWidth = std::max(0.0f, (tw - availableWidth)) / 2.0f; + if (availableWidth < tw) { + tx -= (1.0f + sin(time_now_d() * 1.5f)) * sineWidth; + Bounds tb = bounds; + tb.x = bounds.x + textX; + tb.w = availableWidth; + dc.PushScissor(tb); + } + dc.DrawText(text, bounds.x + tx, bounds.centerY(), style.fgColor, ALIGN_VCENTER | FLAG_WRAP_TEXT); + if (availableWidth < tw) { + dc.PopScissor(); + } + } + + if (ginfo && ginfo->hasConfig && !ginfo->id.empty()) { + const AtlasImage *gearImage = dc.Draw()->GetAtlas()->getImage(ImageID("I_GEAR_SMALL")); + if (gearImage) { + if (viewMode_ == GameBrowserViewMode::GRID) { + dc.Draw()->DrawImage(ImageID("I_GEAR_SMALL"), bounds_.x, iconBounds.y2() - gearImage->h * g_Config.fGameGridScale, g_Config.fGameGridScale); + } else if (viewMode_ == GameBrowserViewMode::LARGE_LIST) { + dc.Draw()->DrawImage(ImageID("I_GEAR_SMALL"), bounds_.x + LARGE_LIST_LEFT_PAD, iconBounds.y2() - gearImage->h * g_Config.fGameGridScale, g_Config.fGameGridScale); + } else { + dc.Draw()->DrawImage(ImageID("I_GEAR_SMALL"), bounds_.x + 4, iconBounds.y, 1.0f); + } + } + } + + const int regionIndex = (int)region; + if (g_Config.bShowRegionOnGameIcon && region != GameRegion::UNKNOWN && regionIndex >= 0 && regionIndex < (int)GameRegion::COUNT) { + const AtlasImage *regionImage = dc.Draw()->GetAtlas()->getImage(regionIcons[regionIndex]); + if (regionImage) { + if (viewMode_ == GameBrowserViewMode::GRID) { + dc.Draw()->DrawImage(regionIcons[regionIndex], bounds_.x + bounds_.w - (regionImage->w + 5) * g_Config.fGameGridScale, + iconBounds.y2() - (regionImage->h + 5) * g_Config.fGameGridScale, g_Config.fGameGridScale); + } else if (viewMode_ == GameBrowserViewMode::LARGE_LIST) { + dc.Draw()->DrawImage(regionIcons[regionIndex], bounds_.x + LARGE_LIST_LEFT_PAD + (LARGE_LIST_ICON_W - regionImage->w - 5) * g_Config.fGameGridScale, + iconBounds.y2() - (regionImage->h + 5) * g_Config.fGameGridScale, g_Config.fGameGridScale); + } else { + dc.Draw()->DrawImage(regionIcons[regionIndex], bounds_.x + 4, iconBounds.y2() - regionImage->h - 5, 1.0f); + } + } + } + + if (HasInfoButton()) { + const Bounds infoBounds = InfoButtonBounds(); + Style infoStyle = HasFocus() ? dc.GetTheme().itemFocusedStyle : dc.GetTheme().itemStyle; + if (infoButtonDown_) { + infoStyle = dc.GetTheme().itemDownStyle; + dc.FillRoundRect(infoStyle.background, infoBounds, dc.GetTheme().choiceRoundRadius); + } else if (HasFocus() && infoSelected_) { + dc.FillRoundRect(infoStyle.background, infoBounds, dc.GetTheme().choiceRoundRadius); + } + dc.Draw()->DrawImage(ImageID("I_INFO"), infoBounds.centerX(), infoBounds.centerY(), 1.0f, infoStyle.fgColor, ALIGN_CENTER); + } + + u32 overlayColor = 0; + if (holdEnabled_ && holdStart_ != 0.0) { + double time_held = time_now_d() - holdStart_; + overlayColor = whiteAlpha(time_held / 2.5f); + if (holdStart_ != 0.0) { + double time_held = time_now_d() - holdStart_; + int holdFrameCount = (int)(time_held * 60.0f); + if (holdFrameCount > 60) { + // Blink before launching by holding + if (((holdFrameCount >> 3) & 1) == 0) + overlayColor = 0x0; + } + } + } + + if (overlayColor) { + dc.FillRect(Drawable(overlayColor), overlayBounds); + } + + // Covers the icon while the game is being compressed. Clicking opens the game screen instead of playing. + GamePacker::Status packStatus; + if (GamePacker::IsPacking(gamePath_) && GamePacker::GetStatus(&packStatus)) { + auto ga = GetI18NCategory(I18NCat::GAME); + dc.FillRect(Drawable(0xD0505050), iconBounds); + const float scale = viewMode_ == GameBrowserViewMode::GRID || viewMode_ == GameBrowserViewMode::LARGE_LIST ? 0.8f * g_Config.fGameGridScale : 0.6f; + dc.SetFontStyle(dc.GetTheme().uiFont); + dc.SetFontScale(scale, scale); + const std::string text = std::string(ga->T("Compressing...")) + StringFromFormat("\n%d%%", (int)(packStatus.progress * 100.0f)); + dc.DrawTextRectSqueeze(text, iconBounds, 0xFFFFFFFF, ALIGN_CENTER); + dc.SetFontScale(1.0f, 1.0f); + } + + dc.RebindTexture(); +} + +std::string GameButton::DescribeText() const { + // LOW: search calls this for every game in the list, which mustn't hold up the visible ones. + std::shared_ptr ginfo = g_gameInfoCache->GetInfo(nullptr, gamePath_, GameInfoFlags::PARAM_SFO, nullptr, GameInfoFlags::EMPTY, TaskPriority::LOW); + if (!ginfo->Ready(GameInfoFlags::PARAM_SFO)) + return "..."; + auto u = GetI18NCategory(I18NCat::UI_ELEMENTS); + return ginfo->GetTitle(); +} diff --git a/UI/GameButton.h b/UI/GameButton.h new file mode 100644 index 0000000000..3a691f91aa --- /dev/null +++ b/UI/GameButton.h @@ -0,0 +1,269 @@ +// Copyright (c) 2012- 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/. + + +#pragma once + +#include +#include +#include +#include + +#include "Common/File/Path.h" +#include "Common/TimeUtil.h" +#include "Common/UI/View.h" +#include "Common/UI/Root.h" +#include "Core/Config.h" +#include "Core/ConfigValues.h" +#include "UI/MiscViews.h" + +class GameInfo; + +// Icon dimensions in the large list, and its padding around the icon. +static constexpr float LARGE_LIST_ICON_W = 144.0f; +static constexpr float LARGE_LIST_ICON_H = 80.0f; +static constexpr float LARGE_LIST_PAD = 6.0f; +static constexpr float LARGE_LIST_LEFT_PAD = 11.0f; +static constexpr float LARGE_LIST_INFO_SIZE = 64.0f; + +enum class GameButtonType { + Game, + Other, + Dir, +}; + +class GameButton : public UI::Clickable { +public: + GameButton(GameButtonType type, const Path &gamePath, GameBrowserViewMode viewMode, UI::LayoutParams *layoutParams = nullptr) + : UI::Clickable(layoutParams), type_(type), viewMode_(viewMode), gamePath_(gamePath) {} + + void Draw(UIContext &dc) override; + std::string DescribeText() const override; + std::string SearchAlias() const override { + // Without the extension, or searching for "iso" would match every ISO. Only three or more + // letters counts as one, so a folder like "Game v1.2" keeps its version number. + std::string filename = gamePath_.GetFilename(); + const std::string ext = gamePath_.GetFileExtension(); + if (ext.size() >= 4 && ext.size() <= filename.size() && std::all_of(ext.begin() + 1, ext.end(), [](char c) { return c >= 'a' && c <= 'z'; })) { + filename.resize(filename.size() - ext.size()); + } + return filename; + } + void GetContentDimensions(const UIContext &dc, float &w, float &h) const override; + + bool IsHoldEnabled() const { + return type_ == GameButtonType::Game; + } + bool Touch(const TouchInput &input) override { + hovering_ = bounds_.Contains(input.x, input.y); + if (HasInfoButton() && TouchInfoButton(input)) { + return true; + } + const bool retval = UI::Clickable::Touch(input); + if (hovering_ && (input.flags & TouchInputFlags::DOWN)) { + holdStart_ = time_now_d(); + } + if (input.flags & TouchInputFlags::UP) { + holdStart_ = 0; + } + return retval; + } + + bool Key(const KeyInput &key) override { + bool showInfo = false; + + if (HasFocus() && UI::IsInfoKey(key)) { + // If it's the button that's mapped to triangle, then show the info. + if (key.flags & KeyInputFlags::DOWN) { + infoKeyPressed_ = true; + } + if ((key.flags & KeyInputFlags::UP) && infoKeyPressed_) { + showInfo = true; + infoKeyPressed_ = false; + } + } else if (hovering_ && key.deviceId == DEVICE_ID_MOUSE && key.keyCode == NKCODE_EXT_MOUSEBUTTON_2) { + // If it's the right mouse button, and it's not otherwise mapped, show the info also. + if (key.flags & KeyInputFlags::DOWN) { + showInfoPressed_ = true; + } + if ((key.flags & KeyInputFlags::UP) && showInfoPressed_) { + showInfo = true; + showInfoPressed_ = false; + } + } + + if (showInfo) { + TriggerOnHoldClick(); + return true; + } + + if (infoSelected_ && HasFocus() && UI::IsAcceptKey(key)) { + if (key.flags & KeyInputFlags::DOWN) { + infoButtonDown_ = true; + } + if ((key.flags & KeyInputFlags::UP) && infoButtonDown_) { + infoButtonDown_ = false; + TriggerOnHoldClick(); + } + return true; + } + + // Holding the accept button works like a long press, so a gamepad can do it too. + if (IsHoldEnabled() && HasFocus() && UI::IsAcceptKey(key) && !(key.flags & KeyInputFlags::IS_REPEAT)) { + if (key.flags & KeyInputFlags::DOWN) { + holdStart_ = time_now_d(); + } + if (key.flags & KeyInputFlags::UP) { + holdStart_ = 0.0; + } + } + + return Clickable::Key(key); + } + + void Update() override { + // Hold button for 1.5 seconds to launch the game options + if (IsHoldEnabled() && holdStart_ != 0.0 && holdStart_ < time_now_d() - 1.5) { + TriggerOnHoldClick(); + } + } + + void FocusChanged(UI::FocusFlags focusFlags) override { + UI::Clickable::FocusChanged(focusFlags); + if (focusFlags & UI::FocusFlags::LOST_FOCUS) { + // Don't let a held accept button carry over from a button we've left. + holdStart_ = 0.0; + // Moving to another game selects the game, not its info button. + infoSelected_ = false; + infoButtonDown_ = false; + } + TriggerOnHighlight(focusFlags); + } + + // Left and right move between the game and its info button, so up and down still go from game to game. + bool CanMoveFocus(UI::FocusMove dir) const override { + if (!HasInfoButton()) { + return true; + } + // We handle these moves ourselves, so MoveFocus won't play the sound for them. + if (dir == UI::FocusMove::RIGHT && !infoSelected_) { + infoSelected_ = true; + UI::PlayUISound(UI::UISound::SELECT); + return false; + } + if (dir == UI::FocusMove::LEFT && infoSelected_) { + infoSelected_ = false; + UI::PlayUISound(UI::UISound::SELECT); + return false; + } + return true; + } + + void DeviceLost() override { + if (video_) { + video_->DeviceLost(); + } + } + + // Previously in DirButton + + // This only determines if the little pin icon is shown. + void SetPinned(bool pin) { pinned_ = pin; } + bool IsPinned() const { return pinned_; } + + void SetAsAbsolute() { absolute_ = true; } + bool NavigateAsAbsolute() const { return absolute_; } + + const Path &GetPath() const { return gamePath_; } + + UI::Event OnHoldClick; + UI::Event OnHighlight; + +private: + void TriggerOnHoldClick() { + holdStart_ = 0.0; + UI::EventParams e{}; + e.v = this; + e.s = gamePath_.ToString(); + down_ = false; + OnHoldClick.Trigger(e); + } + void TriggerOnHighlight(UI::FocusFlags focusFlags) { + UI::EventParams e{}; + e.v = this; + e.s = gamePath_.ToString(); + e.a = (u32)focusFlags; + OnHighlight.Trigger(e); + } + + void UpdateVideo(const std::shared_ptr &ginfo, bool wantVideo); + + // The large list has room for an info button on each line, doing the same as a long press. + bool HasInfoButton() const { + return viewMode_ == GameBrowserViewMode::LARGE_LIST && type_ == GameButtonType::Game; + } + Bounds InfoButtonBounds() const { + return Bounds(bounds_.x2() - LARGE_LIST_INFO_SIZE - LARGE_LIST_PAD, bounds_.centerY() - LARGE_LIST_INFO_SIZE * 0.5f, LARGE_LIST_INFO_SIZE, LARGE_LIST_INFO_SIZE); + } + // Returns true if the info button took the input. + bool TouchInfoButton(const TouchInput &input) { + if ((input.flags & TouchInputFlags::MOUSE) && (input.buttons & 1) == 0) { + return false; + } + const bool inside = InfoButtonBounds().Contains(input.x, input.y); + if (input.flags & TouchInputFlags::DOWN) { + infoButtonDown_ = inside; + } + if (!infoButtonDown_) { + return false; + } + if (input.flags & TouchInputFlags::UP) { + infoButtonDown_ = false; + if (inside && !(input.flags & TouchInputFlags::CANCEL)) { + TriggerOnHoldClick(); + } + } + return true; + } + + const GameBrowserViewMode viewMode_; + const Path gamePath_; + std::string title_; + + // The ICON1.PMF video, played in place of the icon in the large list, like the XMB does. + std::unique_ptr video_; + Instant videoStart_; + + double holdStart_ = 0.0; + bool holdEnabled_ = true; + bool showInfoPressed_ = false; + bool infoKeyPressed_ = false; + bool hovering_ = false; + bool infoButtonDown_ = false; + // The info button has the keyboard focus, rather than the game. Changed from CanMoveFocus, hence mutable. + mutable bool infoSelected_ = false; + + // Previously in DirButton + bool pinned_ = false; + bool absolute_ = false; + + GameButtonType type_; +}; + +// Also used for the folder buttons. +void DrawIconWithTextGrid(UIContext &dc, ImageID image, std::string_view text, const Bounds &bounds, const UI::Style &style); +void DrawIconWithTextList(UIContext &dc, ImageID image, std::string_view text, const Bounds &bounds, GameBrowserViewMode viewMode, const UI::Style &style, std::string_view rightText = {}); diff --git a/UI/MiscViews.cpp b/UI/MiscViews.cpp index ec5fa7ec97..a95bc6fa30 100644 --- a/UI/MiscViews.cpp +++ b/UI/MiscViews.cpp @@ -67,7 +67,7 @@ TopBar::TopBar(const UIContext &ctx, TopBarFlags flags, std::string_view title, } if (!title.empty()) { - TextView *titleView = Add(new TextView(title, ALIGN_VCENTER, false, new LinearLayoutParams(1.0f, Gravity::G_VCENTER))); + TextView *titleView = Add(new TextView(title, ALIGN_VCENTER | FLAG_ELLIPSIZE_TEXT, false, new LinearLayoutParams(1.0f, Gravity::G_VCENTER))); titleView->SetTextColor(ctx.GetTheme().itemStyle.fgColor); titleView->SetBig(true); if (flags & TopBarFlags::DropShadowTitle) { diff --git a/UI/PauseScreen.cpp b/UI/PauseScreen.cpp index f83c6cbc67..5208b93a8d 100644 --- a/UI/PauseScreen.cpp +++ b/UI/PauseScreen.cpp @@ -83,7 +83,7 @@ void copyDeepLinkForPath(std::string_view filePath); void ShowMessageAfterSaveStateAction(SaveState::Status status, std::string_view message, std::string_view metadata) { if (!message.empty()) { g_OSD.Show(status == SaveState::Status::SUCCESS ? OSDType::MESSAGE_SUCCESS : OSDType::MESSAGE_ERROR, - message, metadata, status == SaveState::Status::SUCCESS ? 2.0 : 5.0); + message, metadata, status == SaveState::Status::SUCCESS ? 2.0 : 5.0, "savestateaction"); } } diff --git a/UI/UI.vcxproj b/UI/UI.vcxproj index 052e68b0f0..5160aded30 100644 --- a/UI/UI.vcxproj +++ b/UI/UI.vcxproj @@ -44,6 +44,7 @@ + @@ -118,6 +119,7 @@ + diff --git a/UI/UI.vcxproj.filters b/UI/UI.vcxproj.filters index ea7e73f043..fe27a86b8b 100644 --- a/UI/UI.vcxproj.filters +++ b/UI/UI.vcxproj.filters @@ -196,6 +196,9 @@ Views + + Views + @@ -372,6 +375,9 @@ Views + + Views + diff --git a/UWP/UI_UWP/UI_UWP.vcxproj b/UWP/UI_UWP/UI_UWP.vcxproj index c1bfdc6177..7785a56d7e 100644 --- a/UWP/UI_UWP/UI_UWP.vcxproj +++ b/UWP/UI_UWP/UI_UWP.vcxproj @@ -103,6 +103,7 @@ + @@ -170,6 +171,7 @@ + diff --git a/UWP/UI_UWP/UI_UWP.vcxproj.filters b/UWP/UI_UWP/UI_UWP.vcxproj.filters index ed9ddb9258..ee3797984f 100644 --- a/UWP/UI_UWP/UI_UWP.vcxproj.filters +++ b/UWP/UI_UWP/UI_UWP.vcxproj.filters @@ -188,6 +188,7 @@ Screens + @@ -353,6 +354,7 @@ Screens + diff --git a/android/jni/Android.mk b/android/jni/Android.mk index 56b8712f5b..a97bfbed1a 100644 --- a/android/jni/Android.mk +++ b/android/jni/Android.mk @@ -1067,6 +1067,7 @@ LOCAL_SRC_FILES := \ $(SRC)/UI/EmuScreen.cpp \ $(SRC)/UI/ImposeHome.cpp \ $(SRC)/UI/GameBrowser.cpp \ + $(SRC)/UI/GameButton.cpp \ $(SRC)/UI/MainScreen.cpp \ $(SRC)/UI/TabbedDialogScreen.cpp \ $(SRC)/UI/SettingsPage.cpp \