CommonHostInterface: Add save state selector UI
This commit is contained in:
parent
7c2244f20f
commit
2a710798cc
|
@ -9,6 +9,8 @@ add_library(frontend-common
|
|||
imgui_styles.h
|
||||
ini_settings_interface.cpp
|
||||
ini_settings_interface.h
|
||||
save_state_selector_ui.cpp
|
||||
save_state_selector_ui.h
|
||||
)
|
||||
|
||||
if(SDL2_FOUND)
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
#include "core/game_list.h"
|
||||
#include "core/gpu.h"
|
||||
#include "core/system.h"
|
||||
#include "save_state_selector_ui.h"
|
||||
#include "scmversion/scmversion.h"
|
||||
#ifdef WITH_SDL2
|
||||
#include "sdl_audio_stream.h"
|
||||
|
@ -32,6 +33,8 @@ bool CommonHostInterface::Initialize()
|
|||
if (!FileSystem::SetWorkingDirectory(m_user_directory.c_str()))
|
||||
Log_ErrorPrintf("Failed to set working directory to '%s'", m_user_directory.c_str());
|
||||
|
||||
m_save_state_selector_ui = std::make_unique<FrontendCommon::SaveStateSelectorUI>(this);
|
||||
|
||||
RegisterGeneralHotkeys();
|
||||
RegisterGraphicsHotkeys();
|
||||
RegisterSaveStateHotkeys();
|
||||
|
@ -360,6 +363,14 @@ void CommonHostInterface::OnControllerTypeChanged(u32 slot)
|
|||
UpdateInputMap();
|
||||
}
|
||||
|
||||
void CommonHostInterface::DrawImGuiWindows()
|
||||
{
|
||||
HostInterface::DrawImGuiWindows();
|
||||
|
||||
if (m_save_state_selector_ui->IsOpen())
|
||||
m_save_state_selector_ui->Draw();
|
||||
}
|
||||
|
||||
void CommonHostInterface::SetDefaultSettings(SettingsInterface& si)
|
||||
{
|
||||
HostInterface::SetDefaultSettings(si);
|
||||
|
@ -773,6 +784,27 @@ void CommonHostInterface::RegisterGraphicsHotkeys()
|
|||
|
||||
void CommonHostInterface::RegisterSaveStateHotkeys()
|
||||
{
|
||||
RegisterHotkey(StaticString("Save States"), StaticString("LoadSelectedSaveState"),
|
||||
StaticString("Load From Selected Slot"), [this](bool pressed) {
|
||||
if (!pressed)
|
||||
m_save_state_selector_ui->LoadCurrentSlot();
|
||||
});
|
||||
RegisterHotkey(StaticString("Save States"), StaticString("SaveSelectedSaveState"),
|
||||
StaticString("Save To Selected Slot"), [this](bool pressed) {
|
||||
if (!pressed)
|
||||
m_save_state_selector_ui->SaveCurrentSlot();
|
||||
});
|
||||
RegisterHotkey(StaticString("Save States"), StaticString("SelectPreviousSaveStateSlot"),
|
||||
StaticString("Select Previous Save Slot"), [this](bool pressed) {
|
||||
if (!pressed)
|
||||
m_save_state_selector_ui->SelectPreviousSlot();
|
||||
});
|
||||
RegisterHotkey(StaticString("Save States"), StaticString("SelectNextSaveStateSlot"),
|
||||
StaticString("Select Next Save Slot"), [this](bool pressed) {
|
||||
if (!pressed)
|
||||
m_save_state_selector_ui->SelectNextSlot();
|
||||
});
|
||||
|
||||
for (u32 global_i = 0; global_i < 2; global_i++)
|
||||
{
|
||||
const bool global = ConvertToBoolUnchecked(global_i);
|
||||
|
|
|
@ -13,6 +13,10 @@
|
|||
|
||||
class ControllerInterface;
|
||||
|
||||
namespace FrontendCommon {
|
||||
class SaveStateSelectorUI;
|
||||
}
|
||||
|
||||
class CommonHostInterface : public HostInterface
|
||||
{
|
||||
public:
|
||||
|
@ -72,6 +76,7 @@ protected:
|
|||
virtual void OnSystemPaused(bool paused) override;
|
||||
virtual void OnSystemDestroyed() override;
|
||||
virtual void OnControllerTypeChanged(u32 slot) override;
|
||||
virtual void DrawImGuiWindows() override;
|
||||
|
||||
virtual void SetDefaultSettings(SettingsInterface& si) override;
|
||||
|
||||
|
@ -115,6 +120,8 @@ private:
|
|||
|
||||
HotkeyInfoList m_hotkeys;
|
||||
|
||||
std::unique_ptr<FrontendCommon::SaveStateSelectorUI> m_save_state_selector_ui;
|
||||
|
||||
// input key maps
|
||||
std::map<HostKeyCode, InputButtonHandler> m_keyboard_input_handlers;
|
||||
|
||||
|
|
|
@ -60,6 +60,7 @@
|
|||
<ClCompile Include="icon.cpp" />
|
||||
<ClCompile Include="imgui_styles.cpp" />
|
||||
<ClCompile Include="ini_settings_interface.cpp" />
|
||||
<ClCompile Include="save_state_selector_ui.cpp" />
|
||||
<ClCompile Include="sdl_audio_stream.cpp" />
|
||||
<ClCompile Include="sdl_controller_interface.cpp" />
|
||||
<ClCompile Include="sdl_initializer.cpp" />
|
||||
|
@ -70,6 +71,7 @@
|
|||
<ClInclude Include="icon.h" />
|
||||
<ClInclude Include="imgui_styles.h" />
|
||||
<ClInclude Include="ini_settings_interface.h" />
|
||||
<ClInclude Include="save_state_selector_ui.h" />
|
||||
<ClInclude Include="sdl_audio_stream.h" />
|
||||
<ClInclude Include="sdl_controller_interface.h" />
|
||||
<ClInclude Include="sdl_initializer.h" />
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
<ClCompile Include="common_host_interface.cpp" />
|
||||
<ClCompile Include="ini_settings_interface.cpp" />
|
||||
<ClCompile Include="controller_interface.cpp" />
|
||||
<ClCompile Include="save_state_selector_ui.cpp" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="icon.h" />
|
||||
|
@ -19,6 +20,7 @@
|
|||
<ClInclude Include="common_host_interface.h" />
|
||||
<ClInclude Include="ini_settings_interface.h" />
|
||||
<ClInclude Include="controller_interface.h" />
|
||||
<ClInclude Include="save_state_selector_ui.h" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="font_roboto_regular.inl" />
|
||||
|
|
|
@ -0,0 +1,257 @@
|
|||
#include "save_state_selector_ui.h"
|
||||
#include "common/log.h"
|
||||
#include "common/timestamp.h"
|
||||
#include "core/host_display.h"
|
||||
#include "core/system.h"
|
||||
#include "icon.h"
|
||||
#include "imgui.h"
|
||||
Log_SetChannel(SaveStateSelectorUI);
|
||||
|
||||
namespace FrontendCommon {
|
||||
|
||||
SaveStateSelectorUI::SaveStateSelectorUI(CommonHostInterface* host_interface) : m_host_interface(host_interface) {}
|
||||
|
||||
SaveStateSelectorUI::~SaveStateSelectorUI() = default;
|
||||
|
||||
void SaveStateSelectorUI::Open(float open_time /* = DEFAULT_OPEN_TIME */)
|
||||
{
|
||||
m_open_timer.Reset();
|
||||
m_open_time = open_time;
|
||||
|
||||
if (m_open)
|
||||
return;
|
||||
|
||||
m_open = true;
|
||||
RefreshList();
|
||||
}
|
||||
|
||||
void SaveStateSelectorUI::Close()
|
||||
{
|
||||
if (!m_open)
|
||||
return;
|
||||
|
||||
m_open = false;
|
||||
}
|
||||
|
||||
void SaveStateSelectorUI::ClearList()
|
||||
{
|
||||
m_slots.clear();
|
||||
}
|
||||
|
||||
void SaveStateSelectorUI::RefreshList()
|
||||
{
|
||||
ClearList();
|
||||
|
||||
const System* system = m_host_interface->GetSystem();
|
||||
if (system && !system->GetRunningCode().empty())
|
||||
{
|
||||
for (s32 i = 1; i <= HostInterface::GLOBAL_SAVE_STATE_SLOTS; i++)
|
||||
{
|
||||
std::optional<HostInterface::ExtendedSaveStateInfo> ssi =
|
||||
m_host_interface->GetExtendedSaveStateInfo(system->GetRunningCode().c_str(), i);
|
||||
|
||||
ListEntry li;
|
||||
if (ssi)
|
||||
InitializeListEntry(&li, &ssi.value());
|
||||
else
|
||||
InitializePlaceholderListEntry(&li, i, false);
|
||||
|
||||
m_slots.push_back(std::move(li));
|
||||
}
|
||||
}
|
||||
|
||||
for (s32 i = 1; i <= HostInterface::GLOBAL_SAVE_STATE_SLOTS; i++)
|
||||
{
|
||||
std::optional<HostInterface::ExtendedSaveStateInfo> ssi = m_host_interface->GetExtendedSaveStateInfo(nullptr, i);
|
||||
|
||||
ListEntry li;
|
||||
if (ssi)
|
||||
InitializeListEntry(&li, &ssi.value());
|
||||
else
|
||||
InitializePlaceholderListEntry(&li, i, true);
|
||||
|
||||
m_slots.push_back(std::move(li));
|
||||
}
|
||||
|
||||
if (m_slots.empty() || m_current_selection >= m_slots.size())
|
||||
m_current_selection = 0;
|
||||
}
|
||||
|
||||
const char* SaveStateSelectorUI::GetSelectedStatePath() const
|
||||
{
|
||||
if (m_slots.empty() || m_slots[m_current_selection].path.empty())
|
||||
return nullptr;
|
||||
|
||||
return m_slots[m_current_selection].path.c_str();
|
||||
}
|
||||
|
||||
s32 SaveStateSelectorUI::GetSelectedStateSlot() const
|
||||
{
|
||||
if (m_slots.empty())
|
||||
return 0;
|
||||
|
||||
return m_slots[m_current_selection].slot;
|
||||
}
|
||||
|
||||
void SaveStateSelectorUI::SelectNextSlot()
|
||||
{
|
||||
if (!m_open)
|
||||
{
|
||||
Open();
|
||||
return;
|
||||
}
|
||||
|
||||
ResetOpenTimer();
|
||||
m_current_selection = (m_current_selection == static_cast<u32>(m_slots.size() - 1)) ? 0 : (m_current_selection + 1);
|
||||
}
|
||||
|
||||
void SaveStateSelectorUI::SelectPreviousSlot()
|
||||
{
|
||||
if (!m_open)
|
||||
{
|
||||
Open();
|
||||
return;
|
||||
}
|
||||
|
||||
ResetOpenTimer();
|
||||
m_current_selection =
|
||||
(m_current_selection == 0) ? (static_cast<u32>(m_slots.size()) - 1u) : (m_current_selection - 1);
|
||||
}
|
||||
|
||||
void SaveStateSelectorUI::InitializeListEntry(ListEntry* li, HostInterface::ExtendedSaveStateInfo* ssi)
|
||||
{
|
||||
li->title = std::move(ssi->title);
|
||||
li->game_code = std::move(ssi->game_code);
|
||||
li->path = std::move(ssi->path);
|
||||
li->formatted_timestamp = Timestamp::FromUnixTimestamp(ssi->timestamp).ToString("%c");
|
||||
li->slot = ssi->slot;
|
||||
li->global = ssi->global;
|
||||
|
||||
li->preview_texture.reset();
|
||||
if (ssi && !ssi->screenshot_data.empty())
|
||||
{
|
||||
li->preview_texture = m_host_interface->GetDisplay()->CreateTexture(ssi->screenshot_width, ssi->screenshot_height,
|
||||
ssi->screenshot_data.data(),
|
||||
sizeof(u32) * ssi->screenshot_width, false);
|
||||
}
|
||||
else
|
||||
{
|
||||
li->preview_texture =
|
||||
m_host_interface->GetDisplay()->CreateTexture(PLACEHOLDER_ICON_WIDTH, PLACEHOLDER_ICON_HEIGHT,
|
||||
PLACEHOLDER_ICON_DATA, sizeof(u32) * PLACEHOLDER_ICON_WIDTH, false);
|
||||
}
|
||||
|
||||
if (!li->preview_texture)
|
||||
Log_ErrorPrintf("Failed to upload save state image to GPU");
|
||||
}
|
||||
|
||||
void SaveStateSelectorUI::InitializePlaceholderListEntry(ListEntry* li, s32 slot, bool global)
|
||||
{
|
||||
li->title = "No Save State";
|
||||
std::string().swap(li->game_code);
|
||||
std::string().swap(li->path);
|
||||
std::string().swap(li->formatted_timestamp);
|
||||
li->slot = slot;
|
||||
li->global = global;
|
||||
|
||||
li->preview_texture =
|
||||
m_host_interface->GetDisplay()->CreateTexture(PLACEHOLDER_ICON_WIDTH, PLACEHOLDER_ICON_HEIGHT,
|
||||
PLACEHOLDER_ICON_DATA, sizeof(u32) * PLACEHOLDER_ICON_WIDTH, false);
|
||||
|
||||
if (!li->preview_texture)
|
||||
Log_ErrorPrintf("Failed to upload save state image to GPU");
|
||||
}
|
||||
|
||||
void SaveStateSelectorUI::Draw()
|
||||
{
|
||||
const float framebuffer_scale = ImGui::GetIO().DisplayFramebufferScale.x;
|
||||
const float window_width = ImGui::GetIO().DisplaySize.x * (2.0f / 3.0f);
|
||||
const float window_height = ImGui::GetIO().DisplaySize.y * 0.5f;
|
||||
const float rounding = 4.0f * framebuffer_scale;
|
||||
ImGui::SetNextWindowSize(ImVec2(window_width, window_height), ImGuiCond_Always);
|
||||
ImGui::SetNextWindowPos(ImVec2(ImGui::GetIO().DisplaySize.x * 0.5f, ImGui::GetIO().DisplaySize.y * 0.5f),
|
||||
ImGuiCond_Always, ImVec2(0.5f, 0.5f));
|
||||
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.11f, 0.15f, 0.17f, 0.8f));
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f));
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, rounding);
|
||||
|
||||
if (ImGui::Begin("##save_state_selector", nullptr,
|
||||
ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoInputs | ImGuiWindowFlags_NoTitleBar))
|
||||
{
|
||||
const float padding = 10.0f * framebuffer_scale;
|
||||
const ImVec2 image_size = ImVec2(128.0f * framebuffer_scale, (128.0f / (4.0f / 3.0f)) * framebuffer_scale);
|
||||
const float item_height = image_size.y + padding * 2.0f;
|
||||
const float text_indent = image_size.x + padding + padding;
|
||||
|
||||
for (size_t i = 0; i < m_slots.size(); i++)
|
||||
{
|
||||
const ListEntry& entry = m_slots[i];
|
||||
const float y_start = item_height * static_cast<float>(i);
|
||||
|
||||
if (i == m_current_selection)
|
||||
{
|
||||
ImGui::SetCursorPosY(y_start);
|
||||
ImGui::SetScrollHereY();
|
||||
|
||||
const ImVec2 p_start(ImGui::GetCursorScreenPos());
|
||||
const ImVec2 p_end(p_start.x + window_width, p_start.y + item_height);
|
||||
ImGui::GetWindowDrawList()->AddRectFilled(p_start, p_end, ImColor(0.22f, 0.30f, 0.34f, 0.9f), rounding);
|
||||
}
|
||||
|
||||
if (entry.preview_texture)
|
||||
{
|
||||
ImGui::SetCursorPosY(y_start + padding);
|
||||
ImGui::SetCursorPosX(padding);
|
||||
ImGui::Image(reinterpret_cast<ImTextureID>(entry.preview_texture->GetHandle()), image_size);
|
||||
}
|
||||
|
||||
ImGui::SetCursorPosY(y_start + padding);
|
||||
|
||||
ImGui::Indent(text_indent);
|
||||
|
||||
ImGui::Text("%s Slot %d", entry.global ? "Global" : (entry.game_code.empty() ? "Game" : entry.game_code.c_str()),
|
||||
entry.slot);
|
||||
ImGui::TextUnformatted(entry.title.c_str());
|
||||
ImGui::TextUnformatted(entry.formatted_timestamp.c_str());
|
||||
ImGui::TextUnformatted(entry.path.c_str());
|
||||
|
||||
ImGui::Unindent(text_indent);
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::PopStyleVar(2);
|
||||
ImGui::PopStyleColor();
|
||||
ImGui::End();
|
||||
|
||||
// auto-close
|
||||
if (m_open_timer.GetTimeSeconds() >= m_open_time)
|
||||
Close();
|
||||
}
|
||||
|
||||
void SaveStateSelectorUI::LoadCurrentSlot()
|
||||
{
|
||||
if (m_current_selection >= m_slots.size())
|
||||
{
|
||||
RefreshList();
|
||||
return;
|
||||
}
|
||||
|
||||
const ListEntry& le = m_slots.at(m_current_selection);
|
||||
m_host_interface->LoadState(le.global, le.slot);
|
||||
Close();
|
||||
}
|
||||
|
||||
void SaveStateSelectorUI::SaveCurrentSlot()
|
||||
{
|
||||
if (m_current_selection >= m_slots.size())
|
||||
{
|
||||
RefreshList();
|
||||
return;
|
||||
}
|
||||
|
||||
const ListEntry& le = m_slots.at(m_current_selection);
|
||||
m_host_interface->SaveState(le.global, le.slot);
|
||||
Close();
|
||||
}
|
||||
|
||||
} // namespace FrontendCommon
|
|
@ -0,0 +1,63 @@
|
|||
#pragma once
|
||||
#include "common_host_interface.h"
|
||||
#include "common/timer.h"
|
||||
#include <memory>
|
||||
|
||||
class HostDisplayTexture;
|
||||
|
||||
namespace FrontendCommon {
|
||||
|
||||
class SaveStateSelectorUI
|
||||
{
|
||||
public:
|
||||
static constexpr float DEFAULT_OPEN_TIME = 5.0f;
|
||||
|
||||
SaveStateSelectorUI(CommonHostInterface* host_interface);
|
||||
~SaveStateSelectorUI();
|
||||
|
||||
ALWAYS_INLINE bool IsOpen() const { return m_open; }
|
||||
ALWAYS_INLINE void ResetOpenTimer() { m_open_timer.Reset(); }
|
||||
|
||||
void Open(float open_time = DEFAULT_OPEN_TIME);
|
||||
void Close();
|
||||
|
||||
void ClearList();
|
||||
void RefreshList();
|
||||
|
||||
const char* GetSelectedStatePath() const;
|
||||
s32 GetSelectedStateSlot() const;
|
||||
|
||||
void SelectNextSlot();
|
||||
void SelectPreviousSlot();
|
||||
|
||||
void Draw();
|
||||
|
||||
void LoadCurrentSlot();
|
||||
void SaveCurrentSlot();
|
||||
|
||||
private:
|
||||
struct ListEntry
|
||||
{
|
||||
std::string path;
|
||||
std::string game_code;
|
||||
std::string title;
|
||||
std::string formatted_timestamp;
|
||||
std::unique_ptr<HostDisplayTexture> preview_texture;
|
||||
s32 slot;
|
||||
bool global;
|
||||
};
|
||||
|
||||
void InitializePlaceholderListEntry(ListEntry* li, s32 slot, bool global);
|
||||
void InitializeListEntry(ListEntry* li, HostInterface::ExtendedSaveStateInfo* ssi);
|
||||
|
||||
CommonHostInterface* m_host_interface;
|
||||
std::vector<ListEntry> m_slots;
|
||||
u32 m_current_selection = 0;
|
||||
|
||||
Common::Timer m_open_timer;
|
||||
float m_open_time = 0.0f;
|
||||
|
||||
bool m_open = false;
|
||||
};
|
||||
|
||||
} // namespace FrontendCommon
|
Loading…
Reference in New Issue