Misc: Backports from PCSX2 UI

This commit is contained in:
Connor McLaughlin 2022-10-23 14:09:54 +10:00
parent 8438506206
commit 72dfbaf6cc
37 changed files with 1037 additions and 271 deletions

View file

@ -1922,14 +1922,22 @@ FileSystem::POSIXLock::POSIXLock(int fd)
} }
else else
{ {
Log_ErrorPrintf("lockf() failed: %d", fd); Log_ErrorPrintf("lockf() failed: %d", errno);
m_fd = -1; m_fd = -1;
} }
} }
FileSystem::POSIXLock::POSIXLock(std::FILE* fp) FileSystem::POSIXLock::POSIXLock(std::FILE* fp)
{ {
POSIXLock(fileno(fp)); m_fd = fileno(fp);
if (m_fd >= 0)
{
if (lockf(m_fd, F_LOCK, 0) != 0)
{
Log_ErrorPrintf("lockf() failed: %d", errno);
m_fd = -1;
}
}
} }
FileSystem::POSIXLock::~POSIXLock() FileSystem::POSIXLock::~POSIXLock()

View file

@ -18,6 +18,17 @@ static void* GetProcAddressCallback(const char* name)
return ::GetProcAddress(GetModuleHandleA("opengl32.dll"), name); return ::GetProcAddress(GetModuleHandleA("opengl32.dll"), name);
} }
static bool ReloadWGL(HDC dc)
{
if (!gladLoadWGLLoader([](const char* name) -> void* { return wglGetProcAddress(name); }, dc))
{
Log_ErrorPrint("Loading GLAD WGL functions failed");
return false;
}
return true;
}
namespace GL { namespace GL {
ContextWGL::ContextWGL(const WindowInfo& wi) : Context(wi) {} ContextWGL::ContextWGL(const WindowInfo& wi) : Context(wi) {}
@ -51,8 +62,8 @@ bool ContextWGL::Initialize(const Version* versions_to_try, size_t num_versions_
} }
else else
{ {
Log_ErrorPrint("ContextWGL must always start with a valid surface."); if (!CreatePBuffer())
return false; return false;
} }
// Everything including core/ES requires a dummy profile to load the WGL extensions. // Everything including core/ES requires a dummy profile to load the WGL extensions.
@ -149,8 +160,8 @@ std::unique_ptr<Context> ContextWGL::CreateSharedContext(const WindowInfo& wi)
} }
else else
{ {
Log_ErrorPrint("PBuffer not implemented"); if (!context->CreatePBuffer())
return nullptr; return nullptr;
} }
if (m_version.profile == Profile::NoProfile) if (m_version.profile == Profile::NoProfile)
@ -305,11 +316,37 @@ bool ContextWGL::CreatePBuffer()
static constexpr const int pb_attribs[] = {0, 0}; static constexpr const int pb_attribs[] = {0, 0};
HGLRC temp_rc = nullptr;
ScopedGuard temp_rc_guard([&temp_rc, hdc]() {
if (temp_rc)
{
wglMakeCurrent(hdc, nullptr);
wglDeleteContext(temp_rc);
}
});
if (!GLAD_WGL_ARB_pbuffer)
{
// we're probably running completely surfaceless... need a temporary context.
temp_rc = wglCreateContext(hdc);
if (!temp_rc || !wglMakeCurrent(hdc, temp_rc))
{
Log_ErrorPrint("Failed to create temporary context to load WGL for pbuffer.");
return false;
}
if (!ReloadWGL(hdc) || !GLAD_WGL_ARB_pbuffer)
{
Log_ErrorPrint("Missing WGL_ARB_pbuffer");
return false;
}
}
AssertMsg(m_pixel_format.has_value(), "Has pixel format for pbuffer"); AssertMsg(m_pixel_format.has_value(), "Has pixel format for pbuffer");
HPBUFFERARB pbuffer = wglCreatePbufferARB(hdc, m_pixel_format.value(), 1, 1, pb_attribs); HPBUFFERARB pbuffer = wglCreatePbufferARB(hdc, m_pixel_format.value(), 1, 1, pb_attribs);
if (!pbuffer) if (!pbuffer)
{ {
Log_ErrorPrint("(ContextWGL::CreatePBuffer) wglCreatePbufferARB() failed"); Log_ErrorPrintf("(ContextWGL::CreatePBuffer) wglCreatePbufferARB() failed");
return false; return false;
} }
@ -318,7 +355,7 @@ bool ContextWGL::CreatePBuffer()
m_dc = wglGetPbufferDCARB(pbuffer); m_dc = wglGetPbufferDCARB(pbuffer);
if (!m_dc) if (!m_dc)
{ {
Log_ErrorPrint("(ContextWGL::CreatePbuffer) wglGetPbufferDCARB() failed"); Log_ErrorPrintf("(ContextWGL::CreatePbuffer) wglGetPbufferDCARB() failed");
return false; return false;
} }
@ -326,6 +363,7 @@ bool ContextWGL::CreatePBuffer()
m_dummy_dc = hdc; m_dummy_dc = hdc;
m_pbuffer = pbuffer; m_pbuffer = pbuffer;
temp_rc_guard.Run();
pbuffer_guard.Cancel(); pbuffer_guard.Cancel();
hdc_guard.Cancel(); hdc_guard.Cancel();
hwnd_guard.Cancel(); hwnd_guard.Cancel();
@ -401,7 +439,7 @@ bool ContextWGL::CreateVersionContext(const Version& version, HGLRC share_contex
if ((version.major_version >= 2 && !GLAD_WGL_EXT_create_context_es2_profile) || if ((version.major_version >= 2 && !GLAD_WGL_EXT_create_context_es2_profile) ||
(version.major_version < 2 && !GLAD_WGL_EXT_create_context_es_profile)) (version.major_version < 2 && !GLAD_WGL_EXT_create_context_es_profile))
{ {
Log_ErrorPrint("WGL_EXT_create_context_es_profile not supported"); Log_ErrorPrintf("WGL_EXT_create_context_es_profile not supported");
return false; return false;
} }
@ -437,11 +475,8 @@ bool ContextWGL::CreateVersionContext(const Version& version, HGLRC share_contex
} }
// re-init glad-wgl // re-init glad-wgl
if (make_current && !gladLoadWGLLoader([](const char* name) -> void* { return wglGetProcAddress(name); }, m_dc)) if (make_current && !ReloadWGL(m_dc))
{
Log_ErrorPrint("Loading GLAD WGL functions failed");
return false; return false;
}
wglDeleteContext(m_rc); wglDeleteContext(m_rc);
} }

View file

@ -12,16 +12,14 @@ class ScopedGuard final
public: public:
ALWAYS_INLINE ScopedGuard(T&& func) : m_func(std::forward<T>(func)) {} ALWAYS_INLINE ScopedGuard(T&& func) : m_func(std::forward<T>(func)) {}
ALWAYS_INLINE ScopedGuard(ScopedGuard&& other) : m_func(std::move(other.m_func)) { other.m_func = nullptr; } ALWAYS_INLINE ScopedGuard(ScopedGuard&& other) : m_func(std::move(other.m_func)) { other.m_func = nullptr; }
ALWAYS_INLINE ~ScopedGuard() { Invoke(); }
ALWAYS_INLINE ~ScopedGuard() { Run(); }
ScopedGuard(const ScopedGuard&) = delete; ScopedGuard(const ScopedGuard&) = delete;
void operator=(const ScopedGuard&) = delete; void operator=(const ScopedGuard&) = delete;
/// Prevents the function from being invoked when we go out of scope. /// Runs the destructor function now instead of when we go out of scope.
ALWAYS_INLINE void Cancel() { m_func.reset(); } ALWAYS_INLINE void Run()
/// Explicitly fires the function.
ALWAYS_INLINE void Invoke()
{ {
if (!m_func.has_value()) if (!m_func.has_value())
return; return;
@ -30,6 +28,9 @@ public:
m_func.reset(); m_func.reset();
} }
/// Prevents the function from being invoked when we go out of scope.
ALWAYS_INLINE void Cancel() { m_func.reset(); }
private: private:
std::optional<T> m_func; std::optional<T> m_func;
}; };

View file

@ -241,6 +241,12 @@ public:
return m_pStringData->pBuffer; return m_pStringData->pBuffer;
} }
// returns a string view for this string
std::string_view GetStringView() const
{
return IsEmpty() ? std::string_view() : std::string_view(GetCharArray(), GetLength());
}
// creates a new string from the specified format // creates a new string from the specified format
static String FromFormat(const char* FormatString, ...) printflike(1, 2); static String FromFormat(const char* FormatString, ...) printflike(1, 2);
@ -250,10 +256,7 @@ public:
// m_pStringData->pBuffer[i]; } // m_pStringData->pBuffer[i]; }
operator const char*() const { return GetCharArray(); } operator const char*() const { return GetCharArray(); }
operator char*() { return GetWriteableCharArray(); } operator char*() { return GetWriteableCharArray(); }
operator std::string_view() const operator std::string_view() const { return GetStringView(); }
{
return IsEmpty() ? std::string_view() : std::string_view(GetCharArray(), GetLength());
}
// Will use the string data provided. // Will use the string data provided.
String& operator=(const String& copyString) String& operator=(const String& copyString)

View file

@ -549,7 +549,7 @@ bool Vulkan::Context::CreateDevice(VkSurfaceKHR surface, bool enable_validation_
Log_ErrorPrintf("Vulkan: Failed to find an acceptable graphics queue."); Log_ErrorPrintf("Vulkan: Failed to find an acceptable graphics queue.");
return false; return false;
} }
if (surface && m_present_queue_family_index == queue_family_count) if (surface != VK_NULL_HANDLE && m_present_queue_family_index == queue_family_count)
{ {
Log_ErrorPrintf("Vulkan: Failed to find an acceptable present queue."); Log_ErrorPrintf("Vulkan: Failed to find an acceptable present queue.");
return false; return false;
@ -583,7 +583,7 @@ bool Vulkan::Context::CreateDevice(VkSurfaceKHR surface, bool enable_validation_
}}; }};
device_info.queueCreateInfoCount = 1; device_info.queueCreateInfoCount = 1;
if (m_graphics_queue_family_index != m_present_queue_family_index) if (surface != VK_NULL_HANDLE && m_graphics_queue_family_index != m_present_queue_family_index)
{ {
device_info.queueCreateInfoCount = 2; device_info.queueCreateInfoCount = 2;
} }

View file

@ -25,7 +25,7 @@ class Context
public: public:
enum : u32 enum : u32
{ {
NUM_COMMAND_BUFFERS = 2 NUM_COMMAND_BUFFERS = 3
}; };
struct OptionalExtensions struct OptionalExtensions

View file

@ -1067,7 +1067,7 @@ bool GPU_HW_Vulkan::CompilePipelines()
} }
} }
batch_shader_guard.Invoke(); batch_shader_guard.Run();
VkShaderModule fullscreen_quad_vertex_shader = VkShaderModule fullscreen_quad_vertex_shader =
g_vulkan_shader_cache->GetVertexShader(shadergen.GenerateScreenQuadVertexShader()); g_vulkan_shader_cache->GetVertexShader(shadergen.GenerateScreenQuadVertexShader());

View file

@ -478,6 +478,11 @@ void NoGUIHost::PlatformWindowFocusLost()
}); });
} }
void NoGUIHost::PlatformDevicesChanged()
{
Host::RunOnCPUThread([]() { InputManager::ReloadDevices(); });
}
bool NoGUIHost::GetSavedPlatformWindowGeometry(s32* x, s32* y, s32* width, s32* height) bool NoGUIHost::GetSavedPlatformWindowGeometry(s32* x, s32* y, s32* width, s32* height)
{ {
auto lock = Host::GetSettingsLock(); auto lock = Host::GetSettingsLock();
@ -687,6 +692,11 @@ bool NoGUIHost::AcquireHostDisplay(RenderAPI api)
return false; return false;
} }
// reload input sources, since it might use the window handle
{
auto lock = Host::GetSettingsLock();
InputManager::ReloadSources(*Host::GetSettingsInterface(), lock);
}
return true; return true;
} }
@ -708,6 +718,9 @@ void NoGUIHost::ReleaseHostDisplay()
if (!g_host_display) if (!g_host_display)
return; return;
// close input sources, since it might use the window handle
InputManager::CloseSources();
CommonHost::ReleaseHostDisplayResources(); CommonHost::ReleaseHostDisplayResources();
ImGuiManager::Shutdown(); ImGuiManager::Shutdown();
g_host_display.reset(); g_host_display.reset();

View file

@ -30,6 +30,7 @@ void ProcessPlatformKeyEvent(s32 key, bool pressed);
void ProcessPlatformTextEvent(const char* text); void ProcessPlatformTextEvent(const char* text);
void PlatformWindowFocusGained(); void PlatformWindowFocusGained();
void PlatformWindowFocusLost(); void PlatformWindowFocusLost();
void PlatformDevicesChanged();
bool GetSavedPlatformWindowGeometry(s32* x, s32* y, s32* width, s32* height); bool GetSavedPlatformWindowGeometry(s32* x, s32* y, s32* width, s32* height);
void SavePlatformWindowGeometry(s32 x, s32 y, s32 width, s32 height); void SavePlatformWindowGeometry(s32 x, s32 y, s32 width, s32 height);
} // namespace NoGUIHost } // namespace NoGUIHost

View file

@ -9,6 +9,7 @@
#include "nogui_host.h" #include "nogui_host.h"
#include "resource.h" #include "resource.h"
#include "win32_key_names.h" #include "win32_key_names.h"
#include <Dbt.h>
#include <shellapi.h> #include <shellapi.h>
#include <tchar.h> #include <tchar.h>
Log_SetChannel(Win32HostInterface); Log_SetChannel(Win32HostInterface);
@ -122,6 +123,11 @@ bool Win32NoGUIPlatform::CreatePlatformWindow(std::string title)
if (m_fullscreen.load(std::memory_order_acquire)) if (m_fullscreen.load(std::memory_order_acquire))
SetFullscreen(true); SetFullscreen(true);
// We use these notifications to detect when a controller is connected or disconnected.
DEV_BROADCAST_DEVICEINTERFACE_W filter = {sizeof(DEV_BROADCAST_DEVICEINTERFACE_W), DBT_DEVTYP_DEVICEINTERFACE};
m_dev_notify_handle =
RegisterDeviceNotificationW(hwnd, &filter, DEVICE_NOTIFY_WINDOW_HANDLE | DEVICE_NOTIFY_ALL_INTERFACE_CLASSES);
return true; return true;
} }
@ -130,6 +136,12 @@ void Win32NoGUIPlatform::DestroyPlatformWindow()
if (!m_hwnd) if (!m_hwnd)
return; return;
if (m_dev_notify_handle)
{
UnregisterDeviceNotification(m_dev_notify_handle);
m_dev_notify_handle = NULL;
}
RECT rc; RECT rc;
if (!m_fullscreen.load(std::memory_order_acquire) && GetWindowRect(m_hwnd, &rc)) if (!m_fullscreen.load(std::memory_order_acquire) && GetWindowRect(m_hwnd, &rc))
{ {
@ -393,6 +405,13 @@ LRESULT CALLBACK Win32NoGUIPlatform::WndProc(HWND hwnd, UINT msg, WPARAM wParam,
} }
break; break;
case WM_DEVICECHANGE:
{
if (wParam == DBT_DEVNODES_CHANGED)
NoGUIHost::PlatformDevicesChanged();
}
break;
case WM_FUNC: case WM_FUNC:
{ {
std::function<void()>* pfunc = reinterpret_cast<std::function<void()>*>(lParam); std::function<void()>* pfunc = reinterpret_cast<std::function<void()>*>(lParam);

View file

@ -59,4 +59,6 @@ private:
std::atomic_bool m_fullscreen{false}; std::atomic_bool m_fullscreen{false};
DWORD m_last_mouse_buttons = 0; DWORD m_last_mouse_buttons = 0;
HDEVNOTIFY m_dev_notify_handle = NULL;
}; };

View file

@ -24,10 +24,6 @@
#include "settingwidgetbinder.h" #include "settingwidgetbinder.h"
#include "util/cd_image.h" #include "util/cd_image.h"
#ifdef WITH_CHEEVOS
#include "frontend-common/achievements.h"
#endif
#include <QtCore/QDebug> #include <QtCore/QDebug>
#include <QtCore/QFile> #include <QtCore/QFile>
#include <QtCore/QFileInfo> #include <QtCore/QFileInfo>
@ -43,6 +39,15 @@
#include <QtWidgets/QStyleFactory> #include <QtWidgets/QStyleFactory>
#include <cmath> #include <cmath>
#ifdef WITH_CHEEVOS
#include "frontend-common/achievements.h"
#endif
#ifdef _WIN32
#include "common/windows_headers.h"
#include <Dbt.h>
#endif
Log_SetChannel(MainWindow); Log_SetChannel(MainWindow);
static constexpr char DISC_IMAGE_FILTER[] = QT_TRANSLATE_NOOP( static constexpr char DISC_IMAGE_FILTER[] = QT_TRANSLATE_NOOP(
@ -101,6 +106,10 @@ MainWindow::~MainWindow()
// we compare here, since recreate destroys the window later // we compare here, since recreate destroys the window later
if (g_main_window == this) if (g_main_window == this)
g_main_window = nullptr; g_main_window = nullptr;
#ifdef _WIN32
unregisterForDeviceNotifications();
#endif
} }
void MainWindow::updateApplicationTheme() void MainWindow::updateApplicationTheme()
@ -129,6 +138,10 @@ void MainWindow::initialize()
if (Achievements::IsUsingRAIntegration()) if (Achievements::IsUsingRAIntegration())
Achievements::RAIntegration::MainWindowChanged((void*)winId()); Achievements::RAIntegration::MainWindowChanged((void*)winId());
#endif #endif
#ifdef _WIN32
registerForDeviceNotifications();
#endif
} }
void MainWindow::reportError(const QString& title, const QString& message) void MainWindow::reportError(const QString& title, const QString& message)
@ -143,6 +156,48 @@ bool MainWindow::confirmMessage(const QString& title, const QString& message)
return (QMessageBox::question(this, title, message) == QMessageBox::Yes); return (QMessageBox::question(this, title, message) == QMessageBox::Yes);
} }
void MainWindow::registerForDeviceNotifications()
{
#ifdef _WIN32
// We use these notifications to detect when a controller is connected or disconnected.
DEV_BROADCAST_DEVICEINTERFACE_W filter = {sizeof(DEV_BROADCAST_DEVICEINTERFACE_W), DBT_DEVTYP_DEVICEINTERFACE};
m_device_notification_handle = RegisterDeviceNotificationW(
(HANDLE)winId(), &filter, DEVICE_NOTIFY_WINDOW_HANDLE | DEVICE_NOTIFY_ALL_INTERFACE_CLASSES);
#endif
}
void MainWindow::unregisterForDeviceNotifications()
{
#ifdef _WIN32
if (!m_device_notification_handle)
return;
UnregisterDeviceNotification(static_cast<HDEVNOTIFY>(m_device_notification_handle));
m_device_notification_handle = nullptr;
#endif
}
#ifdef _WIN32
bool MainWindow::nativeEvent(const QByteArray& eventType, void* message, qintptr* result)
{
static constexpr const char win_type[] = "windows_generic_MSG";
if (eventType == QByteArray(win_type, sizeof(win_type) - 1))
{
const MSG* msg = static_cast<const MSG*>(message);
if (msg->message == WM_DEVICECHANGE && msg->wParam == DBT_DEVNODES_CHANGED)
{
g_emu_thread->reloadInputDevices();
*result = 1;
return true;
}
}
return QMainWindow::nativeEvent(eventType, message, result);
}
#endif
bool MainWindow::createDisplay(bool fullscreen, bool render_to_main) bool MainWindow::createDisplay(bool fullscreen, bool render_to_main)
{ {
Log_DevPrintf("createDisplay(%u, %u)", static_cast<u32>(fullscreen), static_cast<u32>(render_to_main)); Log_DevPrintf("createDisplay(%u, %u)", static_cast<u32>(fullscreen), static_cast<u32>(render_to_main));
@ -650,6 +705,9 @@ void MainWindow::recreate()
if (s_system_valid) if (s_system_valid)
requestShutdown(false, true, true); requestShutdown(false, true, true);
// We need to close input sources, because e.g. DInput uses our window handle.
g_emu_thread->closeInputSources();
close(); close();
g_main_window = nullptr; g_main_window = nullptr;
@ -657,6 +715,9 @@ void MainWindow::recreate()
new_main_window->initialize(); new_main_window->initialize();
new_main_window->show(); new_main_window->show();
deleteLater(); deleteLater();
// Reload the sources we just closed.
g_emu_thread->reloadInputSources();
} }
void MainWindow::populateGameListContextMenu(const GameList::Entry* entry, QWidget* parent_window, QMenu* menu) void MainWindow::populateGameListContextMenu(const GameList::Entry* entry, QWidget* parent_window, QMenu* menu)
@ -1658,7 +1719,7 @@ void MainWindow::updateWindowState(bool force_visible)
return; return;
const bool hide_window = !isRenderingToMain() && shouldHideMainWindow(); const bool hide_window = !isRenderingToMain() && shouldHideMainWindow();
const bool disable_resize = Host::GetBaseBoolSettingValue("Main", "DisableWindowResize", false); const bool disable_resize = Host::GetBoolSettingValue("Main", "DisableWindowResize", false);
const bool has_window = s_system_valid || m_display_widget; const bool has_window = s_system_valid || m_display_widget;
// Need to test both valid and display widget because of startup (vm invalid while window is created). // Need to test both valid and display widget because of startup (vm invalid while window is created).
@ -1730,8 +1791,8 @@ bool MainWindow::shouldHideMouseCursor() const
bool MainWindow::shouldHideMainWindow() const bool MainWindow::shouldHideMainWindow() const
{ {
return Host::GetBaseBoolSettingValue("Main", "HideMainWindowWhenRunning", false) || isRenderingFullscreen() || return Host::GetBaseBoolSettingValue("Main", "HideMainWindowWhenRunning", false) ||
QtHost::InNoGUIMode(); (g_emu_thread->shouldRenderToMain() && isRenderingFullscreen()) || QtHost::InNoGUIMode();
} }
void MainWindow::switchToGameListView() void MainWindow::switchToGameListView()
@ -2412,7 +2473,7 @@ bool MainWindow::requestShutdown(bool allow_confirm /* = true */, bool allow_sav
// reshow the main window during display updates, because otherwise fullscreen transitions and renderer switches // reshow the main window during display updates, because otherwise fullscreen transitions and renderer switches
// would briefly show and then hide the main window. So instead, we do it on shutdown, here. Except if we're in // would briefly show and then hide the main window. So instead, we do it on shutdown, here. Except if we're in
// batch mode, when we're going to exit anyway. // batch mode, when we're going to exit anyway.
if (!isRenderingToMain() && isHidden() && !QtHost::InBatchMode()) if (!isRenderingToMain() && isHidden() && !QtHost::InBatchMode() && !g_emu_thread->isRunningFullscreenUI())
updateWindowState(true); updateWindowState(true);
// Now we can actually shut down the VM. // Now we can actually shut down the VM.

View file

@ -169,6 +169,10 @@ protected:
void dragEnterEvent(QDragEnterEvent* event) override; void dragEnterEvent(QDragEnterEvent* event) override;
void dropEvent(QDropEvent* event) override; void dropEvent(QDropEvent* event) override;
#ifdef _WIN32
bool nativeEvent(const QByteArray& eventType, void* message, qintptr* result) override;
#endif
private: private:
static void setStyleFromSettings(); static void setStyleFromSettings();
static void setIconThemeFromSettings(); static void setIconThemeFromSettings();
@ -218,6 +222,9 @@ private:
void setTheme(const QString& theme); void setTheme(const QString& theme);
void recreate(); void recreate();
void registerForDeviceNotifications();
void unregisterForDeviceNotifications();
/// Fills menu with save state info and handlers. /// Fills menu with save state info and handlers.
void populateGameListContextMenu(const GameList::Entry* entry, QWidget* parent_window, QMenu* menu); void populateGameListContextMenu(const GameList::Entry* entry, QWidget* parent_window, QMenu* menu);
@ -271,6 +278,10 @@ private:
bool m_is_closing = false; bool m_is_closing = false;
GDBServer* m_gdb_server = nullptr; GDBServer* m_gdb_server = nullptr;
#ifdef _WIN32
void* m_device_notification_handle = nullptr;
#endif
}; };
extern MainWindow* g_main_window; extern MainWindow* g_main_window;

View file

@ -376,7 +376,7 @@ void QtHost::SetDefaultSettings(SettingsInterface& si, bool system, bool control
bool EmuThread::shouldRenderToMain() const bool EmuThread::shouldRenderToMain() const
{ {
return !Host::GetBaseBoolSettingValue("Main", "RenderToSeparateWindow", false) && !QtHost::InNoGUIMode(); return !Host::GetBoolSettingValue("Main", "RenderToSeparateWindow", false) && !QtHost::InNoGUIMode();
} }
void Host::RequestResizeHostDisplay(s32 new_window_width, s32 new_window_height) void Host::RequestResizeHostDisplay(s32 new_window_width, s32 new_window_height)
@ -895,6 +895,28 @@ void EmuThread::reloadInputBindings()
InputManager::ReloadBindings(*si, *bindings_si); InputManager::ReloadBindings(*si, *bindings_si);
} }
void EmuThread::reloadInputDevices()
{
if (!isOnThread())
{
QMetaObject::invokeMethod(this, &EmuThread::reloadInputDevices, Qt::QueuedConnection);
return;
}
InputManager::ReloadDevices();
}
void EmuThread::closeInputSources()
{
if (!isOnThread())
{
QMetaObject::invokeMethod(this, &EmuThread::reloadInputDevices, Qt::BlockingQueuedConnection);
return;
}
InputManager::CloseSources();
}
void EmuThread::enumerateInputDevices() void EmuThread::enumerateInputDevices()
{ {
if (!isOnThread()) if (!isOnThread())

View file

@ -148,6 +148,8 @@ public Q_SLOTS:
void updateEmuFolders(); void updateEmuFolders();
void reloadInputSources(); void reloadInputSources();
void reloadInputBindings(); void reloadInputBindings();
void reloadInputDevices();
void closeInputSources();
void enumerateInputDevices(); void enumerateInputDevices();
void enumerateVibrationMotors(); void enumerateVibrationMotors();
void startFullscreenUI(); void startFullscreenUI();

View file

@ -478,7 +478,7 @@ void Achievements::UpdateSettings(const Settings& old_config)
if (!g_settings.achievements_enabled) if (!g_settings.achievements_enabled)
{ {
// we're done here // we're done here
OnSystemShutdown(); Shutdown();
return; return;
} }
@ -510,7 +510,7 @@ void Achievements::UpdateSettings(const Settings& old_config)
g_settings.achievements_use_first_disc_from_playlist != old_config.achievements_use_first_disc_from_playlist || g_settings.achievements_use_first_disc_from_playlist != old_config.achievements_use_first_disc_from_playlist ||
g_settings.achievements_rich_presence != old_config.achievements_rich_presence) g_settings.achievements_rich_presence != old_config.achievements_rich_presence)
{ {
OnSystemShutdown(); Shutdown();
Initialize(); Initialize();
return; return;
} }
@ -603,14 +603,11 @@ void Achievements::SetChallengeMode(bool enabled)
GetUserUnlocks(); GetUserUnlocks();
} }
bool Achievements::OnSystemShutdown() bool Achievements::Shutdown()
{ {
#ifdef WITH_RAINTEGRATION #ifdef WITH_RAINTEGRATION
if (IsUsingRAIntegration()) if (IsUsingRAIntegration())
{ {
if (!RA_ConfirmLoadNewRom(true))
return false;
RA_SetPaused(false); RA_SetPaused(false);
RA_ActivateGame(0); RA_ActivateGame(0);
return true; return true;

View file

@ -91,7 +91,7 @@ void UpdateSettings(const Settings& old_config);
bool ConfirmSystemReset(); bool ConfirmSystemReset();
/// Called when the system is being shut down. If Shutdown() returns false, the shutdown should be aborted. /// Called when the system is being shut down. If Shutdown() returns false, the shutdown should be aborted.
bool OnSystemShutdown(); bool Shutdown();
/// Called when the system is being paused and resumed. /// Called when the system is being paused and resumed.
void OnSystemPaused(bool paused); void OnSystemPaused(bool paused);

View file

@ -32,8 +32,8 @@
#include "imgui_fullscreen.h" #include "imgui_fullscreen.h"
#include "imgui_manager.h" #include "imgui_manager.h"
#include "imgui_overlays.h" #include "imgui_overlays.h"
#include "platform_misc.h"
#include "input_manager.h" #include "input_manager.h"
#include "platform_misc.h"
#include "scmversion/scmversion.h" #include "scmversion/scmversion.h"
#include "util/audio_stream.h" #include "util/audio_stream.h"
#include "util/ini_settings_interface.h" #include "util/ini_settings_interface.h"
@ -116,7 +116,7 @@ void CommonHost::Shutdown()
#endif #endif
#ifdef WITH_CHEEVOS #ifdef WITH_CHEEVOS
Achievements::OnSystemShutdown(); Achievements::Shutdown();
#endif #endif
InputManager::CloseSources(); InputManager::CloseSources();
@ -391,7 +391,8 @@ void CommonHost::UpdateSessionTime(const std::string& new_serial)
if (!s_session_serial.empty()) if (!s_session_serial.empty())
{ {
// round up to seconds // round up to seconds
const std::time_t etime = static_cast<std::time_t>(std::round(Common::Timer::ConvertValueToSeconds(ctime - s_session_start_time))); const std::time_t etime =
static_cast<std::time_t>(std::round(Common::Timer::ConvertValueToSeconds(ctime - s_session_start_time)));
const std::time_t wtime = std::time(nullptr); const std::time_t wtime = std::time(nullptr);
GameList::AddPlayedTimeForSerial(s_session_serial, wtime, etime); GameList::AddPlayedTimeForSerial(s_session_serial, wtime, etime);
} }
@ -400,6 +401,12 @@ void CommonHost::UpdateSessionTime(const std::string& new_serial)
s_session_start_time = ctime; s_session_start_time = ctime;
} }
u64 CommonHost::GetSessionPlayedTime()
{
const u64 ctime = Common::Timer::GetCurrentValue();
return static_cast<u64>(std::round(Common::Timer::ConvertValueToSeconds(ctime - s_session_start_time)));
}
void Host::SetPadVibrationIntensity(u32 pad_index, float large_or_single_motor_intensity, float small_motor_intensity) void Host::SetPadVibrationIntensity(u32 pad_index, float large_or_single_motor_intensity, float small_motor_intensity)
{ {
InputManager::SetPadVibrationIntensity(pad_index, large_or_single_motor_intensity, small_motor_intensity); InputManager::SetPadVibrationIntensity(pad_index, large_or_single_motor_intensity, small_motor_intensity);

View file

@ -32,6 +32,9 @@ void PumpMessagesOnCPUThread();
bool CreateHostDisplayResources(); bool CreateHostDisplayResources();
void ReleaseHostDisplayResources(); void ReleaseHostDisplayResources();
/// Returns the time elapsed in the current play session.
u64 GetSessionPlayedTime();
#ifdef WITH_CUBEB #ifdef WITH_CUBEB
std::unique_ptr<AudioStream> CreateCubebAudioStream(u32 sample_rate, u32 channels, u32 buffer_ms, u32 latency_ms, std::unique_ptr<AudioStream> CreateCubebAudioStream(u32 sample_rate, u32 channels, u32 buffer_ms, u32 latency_ms,
AudioStretchMode stretch); AudioStretchMode stretch);

View file

@ -85,10 +85,10 @@ bool DInputSource::Initialize(SettingsInterface& si, std::unique_lock<std::mutex
// need to release the lock while we're enumerating, because we call winId(). // need to release the lock while we're enumerating, because we call winId().
settings_lock.unlock(); settings_lock.unlock();
HWND toplevel_window = static_cast<HWND>(Host::GetTopLevelWindowHandle()); m_toplevel_window = static_cast<HWND>(Host::GetTopLevelWindowHandle());
AddDevices(toplevel_window);
settings_lock.lock(); settings_lock.lock();
ReloadDevices();
return true; return true;
} }
@ -112,16 +112,29 @@ static BOOL CALLBACK EnumCallback(LPCDIDEVICEINSTANCEW lpddi, LPVOID pvRef)
return DIENUM_CONTINUE; return DIENUM_CONTINUE;
} }
void DInputSource::AddDevices(HWND toplevel_window) bool DInputSource::ReloadDevices()
{ {
// detect any removals
PollEvents();
std::vector<DIDEVICEINSTANCEW> devices; std::vector<DIDEVICEINSTANCEW> devices;
m_dinput->EnumDevices(DI8DEVCLASS_GAMECTRL, EnumCallback, &devices, DIEDFL_ATTACHEDONLY); m_dinput->EnumDevices(DI8DEVCLASS_GAMECTRL, EnumCallback, &devices, DIEDFL_ATTACHEDONLY);
Log_InfoPrintf("Enumerated %zu devices", devices.size()); Log_VerbosePrintf("Enumerated %zu devices", devices.size());
bool changed = false;
for (DIDEVICEINSTANCEW inst : devices) for (DIDEVICEINSTANCEW inst : devices)
{ {
// do we already have this one?
if (std::any_of(m_controllers.begin(), m_controllers.end(),
[&inst](const ControllerData& cd) { return inst.guidInstance == cd.guid; }))
{
// yup, so skip it
continue;
}
ControllerData cd; ControllerData cd;
cd.guid = inst.guidInstance;
HRESULT hr = m_dinput->CreateDevice(inst.guidInstance, cd.device.GetAddressOf(), nullptr); HRESULT hr = m_dinput->CreateDevice(inst.guidInstance, cd.device.GetAddressOf(), nullptr);
if (FAILED(hr)) if (FAILED(hr))
{ {
@ -130,21 +143,24 @@ void DInputSource::AddDevices(HWND toplevel_window)
} }
const std::string name(StringUtil::WideStringToUTF8String(inst.tszProductName)); const std::string name(StringUtil::WideStringToUTF8String(inst.tszProductName));
if (AddDevice(cd, toplevel_window, name)) if (AddDevice(cd, name))
{ {
const u32 index = static_cast<u32>(m_controllers.size()); const u32 index = static_cast<u32>(m_controllers.size());
m_controllers.push_back(std::move(cd)); m_controllers.push_back(std::move(cd));
Host::OnInputDeviceConnected(GetDeviceIdentifier(index), name); Host::OnInputDeviceConnected(GetDeviceIdentifier(index), name);
changed = true;
} }
} }
return changed;
} }
bool DInputSource::AddDevice(ControllerData& cd, HWND toplevel_window, const std::string& name) bool DInputSource::AddDevice(ControllerData& cd, const std::string& name)
{ {
HRESULT hr = cd.device->SetCooperativeLevel(toplevel_window, DISCL_BACKGROUND | DISCL_EXCLUSIVE); HRESULT hr = cd.device->SetCooperativeLevel(m_toplevel_window, DISCL_BACKGROUND | DISCL_EXCLUSIVE);
if (FAILED(hr)) if (FAILED(hr))
{ {
hr = cd.device->SetCooperativeLevel(toplevel_window, DISCL_BACKGROUND | DISCL_NONEXCLUSIVE); hr = cd.device->SetCooperativeLevel(m_toplevel_window, DISCL_BACKGROUND | DISCL_NONEXCLUSIVE);
if (FAILED(hr)) if (FAILED(hr))
{ {
Log_ErrorPrintf("Failed to set cooperative level for '%s'", name.c_str()); Log_ErrorPrintf("Failed to set cooperative level for '%s'", name.c_str());
@ -225,9 +241,6 @@ void DInputSource::PollEvents()
for (size_t i = 0; i < m_controllers.size();) for (size_t i = 0; i < m_controllers.size();)
{ {
ControllerData& cd = m_controllers[i]; ControllerData& cd = m_controllers[i];
if (!cd.device)
continue;
if (cd.needs_poll) if (cd.needs_poll)
cd.device->Poll(); cd.device->Poll();

View file

@ -32,6 +32,7 @@ public:
bool Initialize(SettingsInterface& si, std::unique_lock<std::mutex>& settings_lock) override; bool Initialize(SettingsInterface& si, std::unique_lock<std::mutex>& settings_lock) override;
void UpdateSettings(SettingsInterface& si, std::unique_lock<std::mutex>& settings_lock) override; void UpdateSettings(SettingsInterface& si, std::unique_lock<std::mutex>& settings_lock) override;
bool ReloadDevices() override;
void Shutdown() override; void Shutdown() override;
void PollEvents() override; void PollEvents() override;
@ -54,6 +55,7 @@ private:
{ {
ComPtr<IDirectInputDevice8W> device; ComPtr<IDirectInputDevice8W> device;
DIJOYSTATE last_state = {}; DIJOYSTATE last_state = {};
GUID guid = {};
std::vector<u32> axis_offsets; std::vector<u32> axis_offsets;
u32 num_buttons = 0; u32 num_buttons = 0;
@ -68,8 +70,7 @@ private:
static std::array<bool, NUM_HAT_DIRECTIONS> GetHatButtons(DWORD hat); static std::array<bool, NUM_HAT_DIRECTIONS> GetHatButtons(DWORD hat);
static std::string GetDeviceIdentifier(u32 index); static std::string GetDeviceIdentifier(u32 index);
void AddDevices(HWND toplevel_window); bool AddDevice(ControllerData& cd, const std::string& name);
bool AddDevice(ControllerData& cd, HWND toplevel_window, const std::string& name);
void CheckForStateChanges(size_t index, const DIJOYSTATE& new_state); void CheckForStateChanges(size_t index, const DIJOYSTATE& new_state);
@ -78,4 +79,5 @@ private:
HMODULE m_dinput_module{}; HMODULE m_dinput_module{};
LPCDIDATAFORMAT m_joystick_data_format{}; LPCDIDATAFORMAT m_joystick_data_format{};
ComPtr<IDirectInput8W> m_dinput; ComPtr<IDirectInput8W> m_dinput;
HWND m_toplevel_window = NULL;
}; };

View file

@ -85,6 +85,7 @@ using ImGuiFullscreen::CenterImage;
using ImGuiFullscreen::CloseChoiceDialog; using ImGuiFullscreen::CloseChoiceDialog;
using ImGuiFullscreen::CloseFileSelector; using ImGuiFullscreen::CloseFileSelector;
using ImGuiFullscreen::DPIScale; using ImGuiFullscreen::DPIScale;
using ImGuiFullscreen::DrawShadowedText;
using ImGuiFullscreen::EndFullscreenColumns; using ImGuiFullscreen::EndFullscreenColumns;
using ImGuiFullscreen::EndFullscreenColumnWindow; using ImGuiFullscreen::EndFullscreenColumnWindow;
using ImGuiFullscreen::EndFullscreenWindow; using ImGuiFullscreen::EndFullscreenWindow;
@ -308,6 +309,11 @@ static void DrawIntRangeSetting(SettingsInterface* bsi, const char* title, const
const char* format = "%d", bool enabled = true, const char* format = "%d", bool enabled = true,
float height = ImGuiFullscreen::LAYOUT_MENU_BUTTON_HEIGHT, ImFont* font = g_large_font, float height = ImGuiFullscreen::LAYOUT_MENU_BUTTON_HEIGHT, ImFont* font = g_large_font,
ImFont* summary_font = g_medium_font); ImFont* summary_font = g_medium_font);
static void DrawIntSpinBoxSetting(SettingsInterface* bsi, const char* title, const char* summary, const char* section,
const char* key, int default_value, int min_value, int max_value, int step_value,
const char* format = "%d", bool enabled = true,
float height = ImGuiFullscreen::LAYOUT_MENU_BUTTON_HEIGHT,
ImFont* font = g_large_font, ImFont* summary_font = g_medium_font);
static void DrawFloatRangeSetting(SettingsInterface* bsi, const char* title, const char* summary, const char* section, static void DrawFloatRangeSetting(SettingsInterface* bsi, const char* title, const char* summary, const char* section,
const char* key, float default_value, float min_value, float max_value, const char* key, float default_value, float min_value, float max_value,
const char* format = "%f", float multiplier = 1.0f, bool enabled = true, const char* format = "%f", float multiplier = 1.0f, bool enabled = true,
@ -395,7 +401,7 @@ static u32 PopulateSaveStateListEntries(const std::string& title, const std::str
static bool OpenLoadStateSelectorForGame(const std::string& game_path); static bool OpenLoadStateSelectorForGame(const std::string& game_path);
static bool OpenSaveStateSelector(bool is_loading); static bool OpenSaveStateSelector(bool is_loading);
static void CloseSaveStateSelector(); static void CloseSaveStateSelector();
static void DrawSaveStateSelector(bool is_loading, bool fullscreen); static void DrawSaveStateSelector(bool is_loading);
static bool OpenLoadStateSelectorForGameResume(const GameList::Entry* entry); static bool OpenLoadStateSelectorForGameResume(const GameList::Entry* entry);
static void DrawResumeStateSelector(); static void DrawResumeStateSelector();
static void DoLoadState(std::string path); static void DoLoadState(std::string path);
@ -403,6 +409,7 @@ static void DoSaveState(s32 slot, bool global);
static std::vector<SaveStateListEntry> s_save_state_selector_slots; static std::vector<SaveStateListEntry> s_save_state_selector_slots;
static std::string s_save_state_selector_game_path; static std::string s_save_state_selector_game_path;
static s32 s_save_state_selector_submenu_index = -1;
static bool s_save_state_selector_open = false; static bool s_save_state_selector_open = false;
static bool s_save_state_selector_loading = true; static bool s_save_state_selector_loading = true;
static bool s_save_state_selector_resuming = false; static bool s_save_state_selector_resuming = false;
@ -773,7 +780,7 @@ void FullscreenUI::Render()
if (s_save_state_selector_resuming) if (s_save_state_selector_resuming)
DrawResumeStateSelector(); DrawResumeStateSelector();
else else
DrawSaveStateSelector(s_save_state_selector_loading, false); DrawSaveStateSelector(s_save_state_selector_loading);
} }
if (s_about_window_open) if (s_about_window_open)
@ -1748,6 +1755,133 @@ void FullscreenUI::DrawIntRectSetting(SettingsInterface* bsi, const char* title,
ImGui::PopFont(); ImGui::PopFont();
} }
void FullscreenUI::DrawIntSpinBoxSetting(SettingsInterface* bsi, const char* title, const char* summary,
const char* section, const char* key, int default_value, int min_value,
int max_value, int step_value, const char* format, bool enabled, float height,
ImFont* font, ImFont* summary_font)
{
const bool game_settings = IsEditingGameSettings(bsi);
const std::optional<int> value =
bsi->GetOptionalIntValue(section, key, game_settings ? std::nullopt : std::optional<int>(default_value));
TinyString value_text;
if (value.has_value())
value_text.Format(format, value.value());
else
value_text = "Use Global Setting";
static bool manual_input = false;
static u32 repeat_count = 0;
if (MenuButtonWithValue(title, summary, value_text, enabled, height, font, summary_font))
{
ImGui::OpenPopup(title);
manual_input = false;
}
ImGui::SetNextWindowSize(LayoutScale(500.0f, 190.0f));
ImGui::SetNextWindowPos(ImGui::GetIO().DisplaySize * 0.5f, ImGuiCond_Always, ImVec2(0.5f, 0.5f));
ImGui::PushFont(g_large_font);
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, LayoutScale(10.0f));
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, LayoutScale(ImGuiFullscreen::LAYOUT_MENU_BUTTON_X_PADDING,
ImGuiFullscreen::LAYOUT_MENU_BUTTON_Y_PADDING));
ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 0.0f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, LayoutScale(20.0f, 20.0f));
bool is_open = true;
if (ImGui::BeginPopupModal(title, &is_open,
ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove))
{
BeginMenuButtons();
s32 dlg_value = static_cast<s32>(value.value_or(default_value));
bool dlg_value_changed = false;
char str_value[32];
std::snprintf(str_value, std::size(str_value), format, dlg_value);
if (manual_input)
{
const float end = ImGui::GetCurrentWindow()->WorkRect.GetWidth();
ImGui::SetNextItemWidth(end);
std::snprintf(str_value, std::size(str_value), "%d", dlg_value);
if (ImGui::InputText("##value", str_value, std::size(str_value), ImGuiInputTextFlags_CharsDecimal))
{
dlg_value = StringUtil::FromChars<s32>(str_value).value_or(dlg_value);
dlg_value_changed = true;
}
ImGui::SetCursorPosY(ImGui::GetCursorPosY() + LayoutScale(10.0f));
}
else
{
const ImVec2& padding(ImGui::GetStyle().FramePadding);
ImVec2 button_pos(ImGui::GetCursorPos());
// Align value text in middle.
ImGui::SetCursorPosY(
button_pos.y +
((LayoutScale(LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY) + padding.y * 2.0f) - g_large_font->FontSize) * 0.5f);
ImGui::TextUnformatted(str_value);
s32 step = 0;
if (FloatingButton(ICON_FA_CHEVRON_UP, padding.x, button_pos.y, -1.0f, -1.0f, 1.0f, 0.0f, true, g_large_font,
&button_pos, true))
{
step = step_value;
}
if (FloatingButton(ICON_FA_CHEVRON_DOWN, button_pos.x - padding.x, button_pos.y, -1.0f, -1.0f, -1.0f, 0.0f, true,
g_large_font, &button_pos, true))
{
step = -step_value;
}
if (FloatingButton(ICON_FA_KEYBOARD, button_pos.x - padding.x, button_pos.y, -1.0f, -1.0f, -1.0f, 0.0f, true,
g_large_font, &button_pos))
{
manual_input = true;
}
if (FloatingButton(ICON_FA_TRASH, button_pos.x - padding.x, button_pos.y, -1.0f, -1.0f, -1.0f, 0.0f, true,
g_large_font, &button_pos))
{
dlg_value = default_value;
dlg_value_changed = true;
}
if (step != 0)
{
dlg_value += step;
dlg_value_changed = true;
}
ImGui::SetCursorPosY(button_pos.y + (padding.y * 2.0f) +
LayoutScale(LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY + 10.0f));
}
if (dlg_value_changed)
{
dlg_value = std::clamp(dlg_value, min_value, max_value);
if (IsEditingGameSettings(bsi) && dlg_value == default_value)
bsi->DeleteValue(section, key);
else
bsi->SetIntValue(section, key, dlg_value);
SetSettingsChanged(bsi);
}
if (MenuButtonWithoutSummary("OK", true, LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY, g_large_font, ImVec2(0.5f, 0.0f)))
{
ImGui::CloseCurrentPopup();
}
EndMenuButtons();
ImGui::EndPopup();
}
ImGui::PopStyleVar(4);
ImGui::PopFont();
}
void FullscreenUI::DrawStringListSetting(SettingsInterface* bsi, const char* title, const char* summary, void FullscreenUI::DrawStringListSetting(SettingsInterface* bsi, const char* title, const char* summary,
const char* section, const char* key, const char* default_value, const char* section, const char* key, const char* default_value,
const char* const* options, const char* const* option_values, const char* const* options, const char* const* option_values,
@ -1955,6 +2089,7 @@ void FullscreenUI::DrawFolderSetting(SettingsInterface* bsi, const char* title,
SetSettingsChanged(bsi); SetSettingsChanged(bsi);
// Host::RunOnCPUThread(&Host::Internal::UpdateEmuFolders); // Host::RunOnCPUThread(&Host::Internal::UpdateEmuFolders);
s_cover_image_map.clear();
CloseFileSelector(); CloseFileSelector();
}); });
@ -2368,8 +2503,9 @@ void FullscreenUI::DrawInterfaceSettingsPage()
#endif #endif
MenuHeading("On-Screen Display"); MenuHeading("On-Screen Display");
DrawIntRangeSetting(bsi, ICON_FA_SEARCH " OSD Scale", "Determines how large the on-screen messages and monitor are.", DrawIntSpinBoxSetting(bsi, ICON_FA_SEARCH " OSD Scale",
"Display", "OSDScale", 100, 25, 500, "%d%%"); "Determines how large the on-screen messages and monitor are.", "Display", "OSDScale", 100, 25,
500, 1, "%d%%");
DrawToggleSetting(bsi, ICON_FA_LIST " Show OSD Messages", "Shows on-screen-display messages when events occur.", DrawToggleSetting(bsi, ICON_FA_LIST " Show OSD Messages", "Shows on-screen-display messages when events occur.",
"Display", "ShowOSDMessages", true); "Display", "ShowOSDMessages", true);
DrawToggleSetting( DrawToggleSetting(
@ -4200,6 +4336,8 @@ void FullscreenUI::DrawAdvancedSettingsPage()
void FullscreenUI::DrawPauseMenu(MainWindowType type) void FullscreenUI::DrawPauseMenu(MainWindowType type)
{ {
SmallString buffer;
ImDrawList* dl = ImGui::GetBackgroundDrawList(); ImDrawList* dl = ImGui::GetBackgroundDrawList();
const ImVec2 display_size(ImGui::GetIO().DisplaySize); const ImVec2 display_size(ImGui::GetIO().DisplaySize);
dl->AddRectFilled(ImVec2(0.0f, 0.0f), display_size, IM_COL32(0x21, 0x21, 0x21, 200)); dl->AddRectFilled(ImVec2(0.0f, 0.0f), display_size, IM_COL32(0x21, 0x21, 0x21, 200));
@ -4209,15 +4347,14 @@ void FullscreenUI::DrawPauseMenu(MainWindowType type)
const std::string& title = System::GetRunningTitle(); const std::string& title = System::GetRunningTitle();
const std::string& serial = System::GetRunningSerial(); const std::string& serial = System::GetRunningSerial();
SmallString subtitle;
if (!serial.empty()) if (!serial.empty())
subtitle.Format("%s - ", serial.c_str()); buffer.Format("%s - ", serial.c_str());
subtitle.AppendString(Path::GetFileName(System::GetRunningPath())); buffer.AppendString(Path::GetFileName(System::GetRunningPath()));
const ImVec2 title_size( const ImVec2 title_size(
g_large_font->CalcTextSizeA(g_large_font->FontSize, std::numeric_limits<float>::max(), -1.0f, title.c_str())); g_large_font->CalcTextSizeA(g_large_font->FontSize, std::numeric_limits<float>::max(), -1.0f, title.c_str()));
const ImVec2 subtitle_size( const ImVec2 subtitle_size(
g_medium_font->CalcTextSizeA(g_medium_font->FontSize, std::numeric_limits<float>::max(), -1.0f, subtitle)); g_medium_font->CalcTextSizeA(g_medium_font->FontSize, std::numeric_limits<float>::max(), -1.0f, buffer));
ImVec2 title_pos(display_size.x - LayoutScale(20.0f + 50.0f + 20.0f) - title_size.x, ImVec2 title_pos(display_size.x - LayoutScale(20.0f + 50.0f + 20.0f) - title_size.x,
display_size.y - LayoutScale(20.0f + 50.0f)); display_size.y - LayoutScale(20.0f + 50.0f));
@ -4244,14 +4381,14 @@ void FullscreenUI::DrawPauseMenu(MainWindowType type)
subtitle_pos.x -= rp_height; subtitle_pos.x -= rp_height;
subtitle_pos.y -= rp_height; subtitle_pos.y -= rp_height;
dl->AddText(g_medium_font, g_medium_font->FontSize, rp_pos, IM_COL32(255, 255, 255, 255), rp.data(), DrawShadowedText(dl, g_medium_font, rp_pos, IM_COL32(255, 255, 255, 255), rp.data(), rp.data() + rp.size(),
rp.data() + rp.size(), wrap_width); wrap_width);
} }
} }
#endif #endif
dl->AddText(g_large_font, g_large_font->FontSize, title_pos, IM_COL32(255, 255, 255, 255), title.c_str()); DrawShadowedText(dl, g_large_font, title_pos, IM_COL32(255, 255, 255, 255), title.c_str());
dl->AddText(g_medium_font, g_medium_font->FontSize, subtitle_pos, IM_COL32(255, 255, 255, 255), subtitle); DrawShadowedText(dl, g_medium_font, subtitle_pos, IM_COL32(255, 255, 255, 255), buffer);
const ImVec2 image_min(display_size.x - LayoutScale(20.0f + 50.0f) - rp_height, const ImVec2 image_min(display_size.x - LayoutScale(20.0f + 50.0f) - rp_height,
display_size.y - LayoutScale(20.0f + 50.0f) - rp_height); display_size.y - LayoutScale(20.0f + 50.0f) - rp_height);
@ -4259,6 +4396,43 @@ void FullscreenUI::DrawPauseMenu(MainWindowType type)
dl->AddImage(GetCoverForCurrentGame(), image_min, image_max); dl->AddImage(GetCoverForCurrentGame(), image_min, image_max);
} }
// current time / play time
{
buffer.Fmt("{:%X}", fmt::localtime(std::time(nullptr)));
const ImVec2 time_size(g_large_font->CalcTextSizeA(g_large_font->FontSize, std::numeric_limits<float>::max(), -1.0f,
buffer.GetCharArray(),
buffer.GetCharArray() + buffer.GetLength()));
const ImVec2 time_pos(display_size.x - LayoutScale(10.0f) - time_size.x, LayoutScale(10.0f));
DrawShadowedText(dl, g_large_font, time_pos, IM_COL32(255, 255, 255, 255), buffer.GetCharArray(),
buffer.GetCharArray() + buffer.GetLength());
const std::string& serial = System::GetRunningSerial();
if (!serial.empty())
{
const std::time_t cached_played_time = GameList::GetCachedPlayedTimeForSerial(serial);
const std::time_t session_time = static_cast<std::time_t>(CommonHost::GetSessionPlayedTime());
buffer.Fmt("Session: {}", GameList::FormatTimespan(session_time, true).GetStringView());
const ImVec2 session_size(g_medium_font->CalcTextSizeA(g_medium_font->FontSize, std::numeric_limits<float>::max(),
-1.0f, buffer.GetCharArray(),
buffer.GetCharArray() + buffer.GetLength()));
const ImVec2 session_pos(display_size.x - LayoutScale(10.0f) - session_size.x,
time_pos.y + g_large_font->FontSize + LayoutScale(4.0f));
DrawShadowedText(dl, g_medium_font, session_pos, IM_COL32(255, 255, 255, 255), buffer.GetCharArray(),
buffer.GetCharArray() + buffer.GetLength());
buffer.Fmt("All Time: {}", GameList::FormatTimespan(cached_played_time + session_time, true).GetStringView());
const ImVec2 total_size(g_medium_font->CalcTextSizeA(g_medium_font->FontSize, std::numeric_limits<float>::max(),
-1.0f, buffer.GetCharArray(),
buffer.GetCharArray() + buffer.GetLength()));
const ImVec2 total_pos(display_size.x - LayoutScale(10.0f) - total_size.x,
session_pos.y + g_medium_font->FontSize + LayoutScale(4.0f));
DrawShadowedText(dl, g_medium_font, total_pos, IM_COL32(255, 255, 255, 255), buffer.GetCharArray(),
buffer.GetCharArray() + buffer.GetLength());
}
}
const ImVec2 window_size(LayoutScale(500.0f, LAYOUT_SCREEN_HEIGHT)); const ImVec2 window_size(LayoutScale(500.0f, LAYOUT_SCREEN_HEIGHT));
const ImVec2 window_pos(0.0f, display_size.y - window_size.y); const ImVec2 window_pos(0.0f, display_size.y - window_size.y);
@ -4420,12 +4594,14 @@ void FullscreenUI::DrawPauseMenu(MainWindowType type)
void FullscreenUI::InitializePlaceholderSaveStateListEntry(SaveStateListEntry* li, const std::string& title, void FullscreenUI::InitializePlaceholderSaveStateListEntry(SaveStateListEntry* li, const std::string& title,
const std::string& serial, s32 slot, bool global) const std::string& serial, s32 slot, bool global)
{ {
li->title = fmt::format("{0} {1} Slot {2}##{1}_slot_{2}", title, global ? "Global" : "Game", slot); li->title = (global || slot > 0) ? fmt::format("{0} Slot {1}##{0}_slot_{1}", global ? "Global" : "Game", slot) :
li->summary = "No Save State"; std::string("Quick Save");
li->summary = "No save present in this slot.";
li->path = {}; li->path = {};
li->timestamp = 0; li->timestamp = 0;
li->slot = slot; li->slot = slot;
li->preview_texture = {}; li->preview_texture = {};
li->global = global;
} }
bool FullscreenUI::InitializeSaveStateListEntry(SaveStateListEntry* li, const std::string& title, bool FullscreenUI::InitializeSaveStateListEntry(SaveStateListEntry* li, const std::string& title,
@ -4442,17 +4618,18 @@ bool FullscreenUI::InitializeSaveStateListEntry(SaveStateListEntry* li, const st
if (global) if (global)
{ {
li->title = StringUtil::StdStringFromFormat("Global Save %d - %s##global_slot_%d", slot, ssi->title.c_str(), slot); li->title = fmt::format("Global Slot {0} - {1}##global_slot_{0}", slot, ssi->serial);
} }
else else
{ {
li->title = StringUtil::StdStringFromFormat("%s Slot %d##game_slot_%d", ssi->title.c_str(), slot, slot); li->title = (slot > 0) ? fmt::format("Game Slot {0}##game_slot_{0}", slot) : std::string("Game Quick Save");
} }
li->summary = fmt::format("{} - Saved {:%c}", ssi->serial.c_str(), fmt::localtime(ssi->timestamp)); li->summary = fmt::format("Saved {:%c}", fmt::localtime(ssi->timestamp));
li->timestamp = ssi->timestamp; li->timestamp = ssi->timestamp;
li->slot = slot; li->slot = slot;
li->path = std::move(filename); li->path = std::move(filename);
li->global = global;
PopulateSaveStateScreenshot(li, &ssi.value()); PopulateSaveStateScreenshot(li, &ssi.value());
return true; return true;
@ -4571,125 +4748,297 @@ void FullscreenUI::CloseSaveStateSelector()
ReturnToMainWindow(); ReturnToMainWindow();
} }
void FullscreenUI::DrawSaveStateSelector(bool is_loading, bool fullscreen) void FullscreenUI::DrawSaveStateSelector(bool is_loading)
{ {
if (fullscreen) ImGuiIO& io = ImGui::GetIO();
ImGui::SetNextWindowPos(ImVec2(0.0f, 0.0f));
ImGui::SetNextWindowSize(io.DisplaySize);
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f));
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f);
ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, 0.0f);
ImGui::PushStyleVar(ImGuiStyleVar_ChildBorderSize, 0.0f);
const char* window_title = is_loading ? "Load State" : "Save State";
ImGui::OpenPopup(window_title);
bool is_open = true;
const bool valid =
ImGui::BeginPopupModal(window_title, &is_open,
ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove |
ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoBackground);
if (!valid || !is_open)
{ {
if (!BeginFullscreenColumns()) if (valid)
{ ImGui::EndPopup();
EndFullscreenColumns();
return;
}
if (!BeginFullscreenColumnWindow(0.0f, LAYOUT_SCREEN_WIDTH, "save_state_selector_slots")) ImGui::PopStyleVar(5);
{ if (!is_open)
EndFullscreenColumnWindow();
EndFullscreenColumns();
return;
}
}
else
{
const char* window_title = is_loading ? "Load State" : "Save State";
ImGui::PushFont(g_large_font);
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, LayoutScale(10.0f));
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, LayoutScale(ImGuiFullscreen::LAYOUT_MENU_BUTTON_X_PADDING,
ImGuiFullscreen::LAYOUT_MENU_BUTTON_Y_PADDING));
ImGui::SetNextWindowSize(LayoutScale(1000.0f, 680.0f));
ImGui::SetNextWindowPos(ImGui::GetIO().DisplaySize * 0.5f, ImGuiCond_Always, ImVec2(0.5f, 0.5f));
ImGui::OpenPopup(window_title);
bool is_open = !WantsToCloseMenu();
if (!ImGui::BeginPopupModal(window_title, &is_open,
ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove) ||
!is_open)
{
ImGui::PopStyleVar(2);
ImGui::PopFont();
CloseSaveStateSelector(); CloseSaveStateSelector();
return; return;
}
} }
BeginMenuButtons(); ImVec2 heading_size = ImVec2(
io.DisplaySize.x, LayoutScale(LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY + LAYOUT_MENU_BUTTON_Y_PADDING * 2.0f + 2.0f));
static constexpr float padding = 10.0f; ImGui::PushStyleColor(ImGuiCol_ChildBg, ModAlpha(UIPrimaryColor, 0.9f));
static constexpr float button_height = 96.0f;
static constexpr float max_image_width = 96.0f;
static constexpr float max_image_height = 96.0f;
for (const SaveStateListEntry& entry : s_save_state_selector_slots) if (ImGui::BeginChild("state_titlebar", heading_size, false, ImGuiWindowFlags_NoNav))
{ {
ImRect bb; BeginNavBar();
bool visible, hovered; if (NavButton(ICON_FA_BACKWARD, true, true))
bool pressed = MenuButtonFrame(entry.title.c_str(), true, button_height, &visible, &hovered, &bb.Min, &bb.Max); CloseSaveStateSelector();
if (!visible)
continue;
ImVec2 pos(bb.Min); NavTitle(is_loading ? "Load State" : "Save State");
EndNavBar();
ImGui::EndChild();
}
// use aspect ratio of screenshot to determine height ImGui::PopStyleColor();
const GPUTexture* image = entry.preview_texture ? entry.preview_texture.get() : GetPlaceholderTexture().get(); ImGui::PushStyleColor(ImGuiCol_ChildBg, ModAlpha(UIBackgroundColor, 0.9f));
const float image_height = ImGui::SetCursorPos(ImVec2(0.0f, heading_size.y));
max_image_width / (static_cast<float>(image->GetWidth()) / static_cast<float>(image->GetHeight()));
const float image_margin = (max_image_height - image_height) / 2.0f;
const ImRect image_bb(ImVec2(pos.x, pos.y + LayoutScale(image_margin)),
pos + LayoutScale(max_image_width, image_margin + image_height));
pos.x += LayoutScale(max_image_width + padding);
ImRect text_bb(pos, ImVec2(bb.Max.x, pos.y + g_large_font->FontSize)); bool close_handled = false;
ImGui::PushFont(g_large_font); if (s_save_state_selector_open &&
ImGui::RenderTextClipped(text_bb.Min, text_bb.Max, entry.title.c_str(), nullptr, nullptr, ImVec2(0.0f, 0.0f), ImGui::BeginChild("state_list", ImVec2(io.DisplaySize.x, io.DisplaySize.y - heading_size.y), false,
&text_bb); ImGuiWindowFlags_NavFlattened))
ImGui::PopFont(); {
BeginMenuButtons();
ImGui::PushFont(g_medium_font); const ImGuiStyle& style = ImGui::GetStyle();
if (!entry.summary.empty()) const float title_spacing = LayoutScale(10.0f);
const float summary_spacing = LayoutScale(4.0f);
const float item_spacing = LayoutScale(20.0f);
const float item_width_with_spacing = std::floor(LayoutScale(LAYOUT_SCREEN_WIDTH / 4.0f));
const float item_width = item_width_with_spacing - item_spacing;
const float image_width = item_width - (style.FramePadding.x * 2.0f);
const float image_height = image_width / 1.33f;
const ImVec2 image_size(image_width, image_height);
const float item_height = (style.FramePadding.y * 2.0f) + image_height + title_spacing + g_large_font->FontSize +
summary_spacing + g_medium_font->FontSize;
const ImVec2 item_size(item_width, item_height);
const u32 grid_count_x = static_cast<u32>(std::floor(ImGui::GetWindowWidth() / item_width_with_spacing));
const float start_x =
(static_cast<float>(ImGui::GetWindowWidth()) - (item_width_with_spacing * static_cast<float>(grid_count_x))) *
0.5f;
u32 grid_x = 0;
u32 grid_y = 0;
ImGui::SetCursorPos(ImVec2(start_x, 0.0f));
for (u32 i = 0; i < s_save_state_selector_slots.size(); i++)
{ {
text_bb.Min.y = text_bb.Max.y + LayoutScale(4.0f); if (i == 0)
text_bb.Max.y = text_bb.Min.y + g_medium_font->FontSize; ResetFocusHere();
ImGui::RenderTextClipped(text_bb.Min, text_bb.Max, entry.summary.c_str(), nullptr, nullptr, ImVec2(0.0f, 0.0f),
&text_bb);
}
if (!entry.path.empty()) const SaveStateListEntry& entry = s_save_state_selector_slots[i];
{ ImGuiWindow* window = ImGui::GetCurrentWindow();
text_bb.Min.y = text_bb.Max.y + LayoutScale(4.0f); if (window->SkipItems)
text_bb.Max.y = text_bb.Min.y + g_medium_font->FontSize; continue;
ImGui::RenderTextClipped(text_bb.Min, text_bb.Max, entry.path.c_str(), nullptr, nullptr, ImVec2(0.0f, 0.0f),
&text_bb);
}
ImGui::PopFont(); const ImGuiID id = window->GetID(static_cast<int>(i));
const ImVec2 pos(window->DC.CursorPos);
ImRect bb(pos, pos + item_size);
ImGui::ItemSize(item_size);
if (ImGui::ItemAdd(bb, id))
{
bool held;
bool hovered;
bool pressed = ImGui::ButtonBehavior(bb, id, &hovered, &held, 0);
if (hovered)
{
const ImU32 col = ImGui::GetColorU32(held ? ImGuiCol_ButtonActive : ImGuiCol_ButtonHovered, 1.0f);
ImGui::GetWindowDrawList()->AddImage( const float t = std::min(static_cast<float>(std::abs(std::sin(ImGui::GetTime() * 0.75) * 1.1)), 1.0f);
static_cast<ImTextureID>(entry.preview_texture ? entry.preview_texture.get() : GetPlaceholderTexture().get()), ImGui::PushStyleColor(ImGuiCol_Border, ImGui::GetColorU32(ImGuiCol_Border, t));
image_bb.Min, image_bb.Max);
if (pressed) ImGui::RenderFrame(bb.Min, bb.Max, col, true, 0.0f);
{
if (is_loading) ImGui::PopStyleColor();
DoLoadState(entry.path); }
bb.Min += style.FramePadding;
bb.Max -= style.FramePadding;
GPUTexture* const screenshot =
entry.preview_texture ? entry.preview_texture.get() : GetPlaceholderTexture().get();
const ImRect image_rect(
CenterImage(ImRect(bb.Min, bb.Min + image_size),
ImVec2(static_cast<float>(screenshot->GetWidth()), static_cast<float>(screenshot->GetHeight()))));
ImGui::GetWindowDrawList()->AddImage(screenshot, image_rect.Min, image_rect.Max, ImVec2(0.0f, 0.0f),
ImVec2(1.0f, 1.0f), IM_COL32(255, 255, 255, 255));
const ImVec2 title_pos(bb.Min.x, bb.Min.y + image_height + title_spacing);
const ImRect title_bb(title_pos, ImVec2(bb.Max.x, title_pos.y + g_large_font->FontSize));
ImGui::PushFont(g_large_font);
ImGui::RenderTextClipped(title_bb.Min, title_bb.Max, entry.title.c_str(), nullptr, nullptr, ImVec2(0.0f, 0.0f),
&title_bb);
ImGui::PopFont();
if (!entry.summary.empty())
{
const ImVec2 summary_pos(bb.Min.x, title_pos.y + g_large_font->FontSize + summary_spacing);
const ImRect summary_bb(summary_pos, ImVec2(bb.Max.x, summary_pos.y + g_medium_font->FontSize));
ImGui::PushFont(g_medium_font);
ImGui::RenderTextClipped(summary_bb.Min, summary_bb.Max, entry.summary.c_str(), nullptr, nullptr,
ImVec2(0.0f, 0.0f), &summary_bb);
ImGui::PopFont();
}
if (pressed)
{
if (is_loading)
{
DoLoadState(entry.path);
CloseSaveStateSelector();
break;
}
else
{
DoSaveState(entry.slot, entry.global);
CloseSaveStateSelector();
break;
}
}
if (hovered && (ImGui::IsItemClicked(ImGuiMouseButton_Right) ||
ImGui::IsNavInputTest(ImGuiNavInput_Input, ImGuiNavReadMode_Pressed)))
{
s_save_state_selector_submenu_index = static_cast<s32>(i);
}
if (static_cast<s32>(i) == s_save_state_selector_submenu_index)
{
// can't use a choice dialog here, because we're already in a modal...
ImGuiFullscreen::PushResetLayout();
ImGui::PushFont(g_large_font);
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, LayoutScale(10.0f));
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding,
LayoutScale(LAYOUT_MENU_BUTTON_X_PADDING, LAYOUT_MENU_BUTTON_Y_PADDING));
ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 0.0f);
ImGui::PushStyleColor(ImGuiCol_Text, UIPrimaryTextColor);
ImGui::PushStyleColor(ImGuiCol_TitleBg, UIPrimaryDarkColor);
ImGui::PushStyleColor(ImGuiCol_TitleBgActive, UIPrimaryColor);
ImGui::PushStyleColor(ImGuiCol_PopupBg, MulAlpha(UIBackgroundColor, 0.95f));
const float width = LayoutScale(600.0f);
const float title_height =
g_large_font->FontSize + ImGui::GetStyle().FramePadding.y * 2.0f + ImGui::GetStyle().WindowPadding.y * 2.0f;
const float height =
title_height +
LayoutScale(LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY + (LAYOUT_MENU_BUTTON_Y_PADDING * 2.0f)) * 3.0f;
ImGui::SetNextWindowSize(ImVec2(width, height));
ImGui::SetNextWindowPos(ImGui::GetIO().DisplaySize * 0.5f, ImGuiCond_Always, ImVec2(0.5f, 0.5f));
ImGui::OpenPopup(entry.title.c_str());
// don't let the back button flow through to the main window
bool submenu_open = !WantsToCloseMenu();
close_handled ^= submenu_open;
bool closed = false;
if (ImGui::BeginPopupModal(entry.title.c_str(), &is_open,
ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove))
{
ImGui::PushStyleColor(ImGuiCol_Text, UIBackgroundTextColor);
BeginMenuButtons();
if (ActiveButton(is_loading ? ICON_FA_FOLDER_OPEN " Load State" : ICON_FA_FOLDER_OPEN " Save State", false,
true, LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY))
{
if (is_loading)
DoLoadState(std::move(entry.path));
else
DoSaveState(entry.slot, entry.global);
CloseSaveStateSelector();
closed = true;
}
if (ActiveButton(ICON_FA_FOLDER_MINUS " Delete Save", false, true, LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY))
{
if (!FileSystem::FileExists(entry.path.c_str()))
{
ShowToast({}, fmt::format("{} does not exist.", ImGuiFullscreen::RemoveHash(entry.title)));
is_open = true;
}
else if (FileSystem::DeleteFile(entry.path.c_str()))
{
ShowToast({}, fmt::format("{} deleted.", ImGuiFullscreen::RemoveHash(entry.title)));
s_save_state_selector_slots.erase(s_save_state_selector_slots.begin() + i);
if (s_save_state_selector_slots.empty())
{
CloseSaveStateSelector();
closed = true;
}
else
{
is_open = false;
}
}
else
{
ShowToast({}, fmt::format("Failed to delete {}.", ImGuiFullscreen::RemoveHash(entry.title)));
is_open = false;
}
}
if (ActiveButton(ICON_FA_WINDOW_CLOSE " Close Menu", false, true, LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY))
{
is_open = false;
}
EndMenuButtons();
ImGui::PopStyleColor();
ImGui::EndPopup();
}
if (!is_open)
{
s_save_state_selector_submenu_index = -1;
if (!closed)
QueueResetFocus();
}
ImGui::PopStyleColor(4);
ImGui::PopStyleVar(3);
ImGui::PopFont();
ImGuiFullscreen::PopResetLayout();
if (closed)
break;
}
}
grid_x++;
if (grid_x == grid_count_x)
{
grid_x = 0;
grid_y++;
ImGui::SetCursorPosX(start_x);
ImGui::SetCursorPosY(ImGui::GetCursorPosY() + item_spacing);
}
else else
DoSaveState(entry.slot, entry.global); {
ImGui::SameLine(start_x + static_cast<float>(grid_x) * (item_width + item_spacing));
}
} }
EndMenuButtons();
ImGui::EndChild();
} }
EndMenuButtons(); ImGui::PopStyleColor();
if (fullscreen) ImGui::EndPopup();
{ ImGui::PopStyleVar(5);
EndFullscreenColumnWindow();
EndFullscreenColumns(); if (!close_handled && WantsToCloseMenu())
} CloseSaveStateSelector();
else
{
ImGui::EndPopup();
ImGui::PopStyleVar(2);
ImGui::PopFont();
}
} }
bool FullscreenUI::OpenLoadStateSelectorForGameResume(const GameList::Entry* entry) bool FullscreenUI::OpenLoadStateSelectorForGameResume(const GameList::Entry* entry)
@ -4821,15 +5170,79 @@ void FullscreenUI::DoSaveState(s32 slot, bool global)
void FullscreenUI::PopulateGameListEntryList() void FullscreenUI::PopulateGameListEntryList()
{ {
const s32 sort = Host::GetBaseIntSettingValue("Main", "FullscreenUIGameSort", 0);
const bool reverse = Host::GetBaseBoolSettingValue("Main", "FullscreenUIGameSortReverse", false);
const u32 count = GameList::GetEntryCount(); const u32 count = GameList::GetEntryCount();
s_game_list_sorted_entries.resize(count); s_game_list_sorted_entries.resize(count);
for (u32 i = 0; i < count; i++) for (u32 i = 0; i < count; i++)
s_game_list_sorted_entries[i] = GameList::GetEntryByIndex(i); s_game_list_sorted_entries[i] = GameList::GetEntryByIndex(i);
// TODO: Custom sort types
std::sort(s_game_list_sorted_entries.begin(), s_game_list_sorted_entries.end(), std::sort(s_game_list_sorted_entries.begin(), s_game_list_sorted_entries.end(),
[](const GameList::Entry* lhs, const GameList::Entry* rhs) { [sort, reverse](const GameList::Entry* lhs, const GameList::Entry* rhs) {
return StringUtil::Strcasecmp(lhs->title.c_str(), rhs->title.c_str()) < 0; switch (sort)
{
case 0: // Type
{
if (lhs->type != rhs->type)
return reverse ? (lhs->type > rhs->type) : (lhs->type < rhs->type);
}
break;
case 1: // Serial
{
if (lhs->serial != rhs->serial)
return reverse ? (lhs->serial > rhs->serial) : (lhs->serial < rhs->serial);
}
break;
case 2: // Title
break;
case 3: // File Title
{
const std::string_view lhs_title(Path::GetFileTitle(lhs->path));
const std::string_view rhs_title(Path::GetFileTitle(rhs->path));
const int res = StringUtil::Strncasecmp(lhs_title.data(), rhs_title.data(),
std::min(lhs_title.size(), rhs_title.size()));
if (res != 0)
return reverse ? (res > 0) : (res < 0);
}
break;
case 4: // Time Played
{
if (lhs->total_played_time != rhs->total_played_time)
{
return reverse ? (lhs->total_played_time > rhs->total_played_time) :
(lhs->total_played_time < rhs->total_played_time);
}
}
break;
case 5: // Last Played (reversed by default)
{
if (lhs->last_played_time != rhs->last_played_time)
{
return reverse ? (lhs->last_played_time < rhs->last_played_time) :
(lhs->last_played_time > rhs->last_played_time);
}
}
break;
case 6: // Size
{
if (lhs->total_size != rhs->total_size)
{
return reverse ? (lhs->total_size > rhs->total_size) : (lhs->total_size < rhs->total_size);
}
}
break;
}
// fallback to title when all else is equal
const int res = StringUtil::Strcasecmp(lhs->title.c_str(), rhs->title.c_str());
return reverse ? (res > 0) : (res < 0);
}); });
} }
@ -5371,20 +5784,35 @@ void FullscreenUI::DrawGameListSettingsPage(const ImVec2& heading_size)
} }
} }
static constexpr const char* view_types[] = {"Game Grid", "Game List"}; MenuHeading("List Settings");
{
static constexpr const char* view_types[] = {"Game Grid", "Game List"};
static constexpr const char* sort_types[] = {"Type", "Serial", "Title", "File Title",
"Time Played", "Last Played", "Size"};
DrawIntListSetting(bsi, ICON_FA_BORDER_ALL " Default View", "Sets which view the game list will open to.", "Main",
"DefaultFullscreenUIGameView", 0, view_types, std::size(view_types));
DrawIntListSetting(bsi, ICON_FA_SORT " Sort By", "Determines which field the game list will be sorted by.", "Main",
"FullscreenUIGameSort", 0, sort_types, std::size(sort_types));
DrawToggleSetting(bsi, ICON_FA_SORT_ALPHA_DOWN " Sort Reversed",
"Reverses the game list sort order from the default (usually ascending to descending).", "Main",
"FullscreenUIGameSortReverse", false);
}
MenuHeading("Cover Settings"); MenuHeading("Cover Settings");
DrawFolderSetting(bsi, ICON_FA_FOLDER " Covers Directory", "Folders", "Covers", EmuFolders::Covers); {
if (MenuButton(ICON_FA_DOWNLOAD " Download Covers", "Downloads covers from a user-specified URL template.")) DrawFolderSetting(bsi, ICON_FA_FOLDER " Covers Directory", "Folders", "Covers", EmuFolders::Covers);
ImGui::OpenPopup("Download Covers"); if (MenuButton(ICON_FA_DOWNLOAD " Download Covers", "Downloads covers from a user-specified URL template."))
DrawIntListSetting(bsi, ICON_FA_BORDER_ALL " Default View", "Sets which view the game list will open to.", "Main", ImGui::OpenPopup("Download Covers");
"DefaultFullscreenUIGameView", 0, view_types, std::size(view_types)); }
MenuHeading("Operations"); MenuHeading("Operations");
if (MenuButton(ICON_FA_SEARCH " Scan For New Games", "Identifies any new files added to the game directories.")) {
Host::RefreshGameListAsync(false); if (MenuButton(ICON_FA_SEARCH " Scan For New Games", "Identifies any new files added to the game directories."))
if (MenuButton(ICON_FA_SEARCH_PLUS " Rescan All Games", "Forces a full rescan of all games previously identified.")) Host::RefreshGameListAsync(false);
Host::RefreshGameListAsync(true); if (MenuButton(ICON_FA_SEARCH_PLUS " Rescan All Games", "Forces a full rescan of all games previously identified."))
Host::RefreshGameListAsync(true);
}
EndMenuButtons(); EndMenuButtons();

View file

@ -82,8 +82,8 @@ static PlayedTimeEntry UpdatePlayedTimeFile(const std::string& path, const std::
static std::vector<GameList::Entry> s_entries; static std::vector<GameList::Entry> s_entries;
static std::recursive_mutex s_mutex; static std::recursive_mutex s_mutex;
static GameList::CacheMap m_cache_map; static GameList::CacheMap s_cache_map;
static std::unique_ptr<ByteStream> m_cache_write_stream; static std::unique_ptr<ByteStream> s_cache_write_stream;
static bool m_game_list_loaded = false; static bool m_game_list_loaded = false;
@ -276,12 +276,12 @@ bool GameList::PopulateEntryFromPath(const std::string& path, Entry* entry)
bool GameList::GetGameListEntryFromCache(const std::string& path, Entry* entry) bool GameList::GetGameListEntryFromCache(const std::string& path, Entry* entry)
{ {
auto iter = m_cache_map.find(path); auto iter = UnorderedStringMapFind(s_cache_map, path);
if (iter == m_cache_map.end()) if (iter == s_cache_map.end())
return false; return false;
*entry = std::move(iter->second); *entry = std::move(iter->second);
m_cache_map.erase(iter); s_cache_map.erase(iter);
return true; return true;
} }
@ -324,11 +324,11 @@ bool GameList::LoadEntriesFromCache(ByteStream* stream)
ge.type = static_cast<EntryType>(type); ge.type = static_cast<EntryType>(type);
ge.compatibility = static_cast<GameDatabase::CompatibilityRating>(compatibility_rating); ge.compatibility = static_cast<GameDatabase::CompatibilityRating>(compatibility_rating);
auto iter = m_cache_map.find(ge.path); auto iter = UnorderedStringMapFind(s_cache_map, ge.path);
if (iter != m_cache_map.end()) if (iter != s_cache_map.end())
iter->second = std::move(ge); iter->second = std::move(ge);
else else
m_cache_map.emplace(std::move(path), std::move(ge)); s_cache_map.emplace(std::move(path), std::move(ge));
} }
return true; return true;
@ -337,23 +337,23 @@ bool GameList::LoadEntriesFromCache(ByteStream* stream)
bool GameList::WriteEntryToCache(const Entry* entry) bool GameList::WriteEntryToCache(const Entry* entry)
{ {
bool result = true; bool result = true;
result &= m_cache_write_stream->WriteU8(static_cast<u8>(entry->type)); result &= s_cache_write_stream->WriteU8(static_cast<u8>(entry->type));
result &= m_cache_write_stream->WriteU8(static_cast<u8>(entry->region)); result &= s_cache_write_stream->WriteU8(static_cast<u8>(entry->region));
result &= m_cache_write_stream->WriteSizePrefixedString(entry->path); result &= s_cache_write_stream->WriteSizePrefixedString(entry->path);
result &= m_cache_write_stream->WriteSizePrefixedString(entry->serial); result &= s_cache_write_stream->WriteSizePrefixedString(entry->serial);
result &= m_cache_write_stream->WriteSizePrefixedString(entry->title); result &= s_cache_write_stream->WriteSizePrefixedString(entry->title);
result &= m_cache_write_stream->WriteSizePrefixedString(entry->genre); result &= s_cache_write_stream->WriteSizePrefixedString(entry->genre);
result &= m_cache_write_stream->WriteSizePrefixedString(entry->publisher); result &= s_cache_write_stream->WriteSizePrefixedString(entry->publisher);
result &= m_cache_write_stream->WriteSizePrefixedString(entry->developer); result &= s_cache_write_stream->WriteSizePrefixedString(entry->developer);
result &= m_cache_write_stream->WriteU64(entry->total_size); result &= s_cache_write_stream->WriteU64(entry->total_size);
result &= m_cache_write_stream->WriteU64(entry->last_modified_time); result &= s_cache_write_stream->WriteU64(entry->last_modified_time);
result &= m_cache_write_stream->WriteU64(entry->release_date); result &= s_cache_write_stream->WriteU64(entry->release_date);
result &= m_cache_write_stream->WriteU32(entry->supported_controllers); result &= s_cache_write_stream->WriteU32(entry->supported_controllers);
result &= m_cache_write_stream->WriteU8(entry->min_players); result &= s_cache_write_stream->WriteU8(entry->min_players);
result &= m_cache_write_stream->WriteU8(entry->max_players); result &= s_cache_write_stream->WriteU8(entry->max_players);
result &= m_cache_write_stream->WriteU8(entry->min_blocks); result &= s_cache_write_stream->WriteU8(entry->min_blocks);
result &= m_cache_write_stream->WriteU8(entry->max_blocks); result &= s_cache_write_stream->WriteU8(entry->max_blocks);
result &= m_cache_write_stream->WriteU8(static_cast<u8>(entry->compatibility)); result &= s_cache_write_stream->WriteU8(static_cast<u8>(entry->compatibility));
return result; return result;
} }
@ -374,7 +374,7 @@ void GameList::LoadCache()
{ {
Log_WarningPrintf("Deleting corrupted cache file '%s'", filename.c_str()); Log_WarningPrintf("Deleting corrupted cache file '%s'", filename.c_str());
stream.reset(); stream.reset();
m_cache_map.clear(); s_cache_map.clear();
DeleteCacheFile(); DeleteCacheFile();
return; return;
} }
@ -383,37 +383,37 @@ void GameList::LoadCache()
bool GameList::OpenCacheForWriting() bool GameList::OpenCacheForWriting()
{ {
const std::string cache_filename(GetCacheFilename()); const std::string cache_filename(GetCacheFilename());
Assert(!m_cache_write_stream); Assert(!s_cache_write_stream);
m_cache_write_stream = ByteStream::OpenFile(cache_filename.c_str(), s_cache_write_stream = ByteStream::OpenFile(cache_filename.c_str(),
BYTESTREAM_OPEN_READ | BYTESTREAM_OPEN_WRITE | BYTESTREAM_OPEN_SEEKABLE); BYTESTREAM_OPEN_READ | BYTESTREAM_OPEN_WRITE | BYTESTREAM_OPEN_SEEKABLE);
if (m_cache_write_stream) if (s_cache_write_stream)
{ {
// check the header // check the header
u32 signature, version; u32 signature, version;
if (m_cache_write_stream->ReadU32(&signature) && signature == GAME_LIST_CACHE_SIGNATURE && if (s_cache_write_stream->ReadU32(&signature) && signature == GAME_LIST_CACHE_SIGNATURE &&
m_cache_write_stream->ReadU32(&version) && version == GAME_LIST_CACHE_VERSION && s_cache_write_stream->ReadU32(&version) && version == GAME_LIST_CACHE_VERSION &&
m_cache_write_stream->SeekToEnd()) s_cache_write_stream->SeekToEnd())
{ {
return true; return true;
} }
m_cache_write_stream.reset(); s_cache_write_stream.reset();
} }
Log_InfoPrintf("Creating new game list cache file: '%s'", cache_filename.c_str()); Log_InfoPrintf("Creating new game list cache file: '%s'", cache_filename.c_str());
m_cache_write_stream = ByteStream::OpenFile( s_cache_write_stream = ByteStream::OpenFile(
cache_filename.c_str(), BYTESTREAM_OPEN_CREATE | BYTESTREAM_OPEN_TRUNCATE | BYTESTREAM_OPEN_WRITE); cache_filename.c_str(), BYTESTREAM_OPEN_CREATE | BYTESTREAM_OPEN_TRUNCATE | BYTESTREAM_OPEN_WRITE);
if (!m_cache_write_stream) if (!s_cache_write_stream)
return false; return false;
// new cache file, write header // new cache file, write header
if (!m_cache_write_stream->WriteU32(GAME_LIST_CACHE_SIGNATURE) || if (!s_cache_write_stream->WriteU32(GAME_LIST_CACHE_SIGNATURE) ||
!m_cache_write_stream->WriteU32(GAME_LIST_CACHE_VERSION)) !s_cache_write_stream->WriteU32(GAME_LIST_CACHE_VERSION))
{ {
Log_ErrorPrintf("Failed to write game list cache header"); Log_ErrorPrintf("Failed to write game list cache header");
m_cache_write_stream.reset(); s_cache_write_stream.reset();
FileSystem::DeleteFile(cache_filename.c_str()); FileSystem::DeleteFile(cache_filename.c_str());
return false; return false;
} }
@ -423,16 +423,16 @@ bool GameList::OpenCacheForWriting()
void GameList::CloseCacheFileStream() void GameList::CloseCacheFileStream()
{ {
if (!m_cache_write_stream) if (!s_cache_write_stream)
return; return;
m_cache_write_stream->Commit(); s_cache_write_stream->Commit();
m_cache_write_stream.reset(); s_cache_write_stream.reset();
} }
void GameList::DeleteCacheFile() void GameList::DeleteCacheFile()
{ {
Assert(!m_cache_write_stream); Assert(!s_cache_write_stream);
const std::string filename(GetCacheFilename()); const std::string filename(GetCacheFilename());
if (!FileSystem::FileExists(filename.c_str())) if (!FileSystem::FileExists(filename.c_str()))
@ -528,7 +528,7 @@ bool GameList::ScanFile(std::string path, std::time_t timestamp, std::unique_loc
entry.path = std::move(path); entry.path = std::move(path);
entry.last_modified_time = timestamp; entry.last_modified_time = timestamp;
if (m_cache_write_stream || OpenCacheForWriting()) if (s_cache_write_stream || OpenCacheForWriting())
{ {
if (!WriteEntryToCache(&entry)) if (!WriteEntryToCache(&entry))
Log_WarningPrintf("Failed to write entry '%s' to cache", entry.path.c_str()); Log_WarningPrintf("Failed to write entry '%s' to cache", entry.path.c_str());
@ -606,9 +606,9 @@ void GameList::Refresh(bool invalidate_cache, bool only_cache, ProgressCallback*
old_entries.swap(s_entries); old_entries.swap(s_entries);
} }
const std::vector<std::string> excluded_paths(Host::GetStringListSetting("GameList", "ExcludedPaths")); const std::vector<std::string> excluded_paths(Host::GetBaseStringListSetting("GameList", "ExcludedPaths"));
const std::vector<std::string> dirs(Host::GetStringListSetting("GameList", "Paths")); const std::vector<std::string> dirs(Host::GetBaseStringListSetting("GameList", "Paths"));
const std::vector<std::string> recursive_dirs(Host::GetStringListSetting("GameList", "RecursivePaths")); const std::vector<std::string> recursive_dirs(Host::GetBaseStringListSetting("GameList", "RecursivePaths"));
const PlayedTimeMap played_time(LoadPlayedTimeMap(GetPlayedTimeFile())); const PlayedTimeMap played_time(LoadPlayedTimeMap(GetPlayedTimeFile()));
if (!dirs.empty() || !recursive_dirs.empty()) if (!dirs.empty() || !recursive_dirs.empty())
@ -638,7 +638,7 @@ void GameList::Refresh(bool invalidate_cache, bool only_cache, ProgressCallback*
// don't need unused cache entries // don't need unused cache entries
CloseCacheFileStream(); CloseCacheFileStream();
m_cache_map.clear(); s_cache_map.clear();
} }
std::string GameList::GetCoverImagePathForEntry(const Entry* entry) std::string GameList::GetCoverImagePathForEntry(const Entry* entry)
@ -774,7 +774,8 @@ GameList::PlayedTimeMap GameList::LoadPlayedTimeMap(const std::string& path)
{ {
PlayedTimeMap ret; PlayedTimeMap ret;
auto fp = FileSystem::OpenManagedCFile(path.c_str(), "rb"); // Use write mode here, even though we're not writing, so we can lock the file from other updates.
auto fp = FileSystem::OpenManagedCFile(path.c_str(), "r+b");
#ifdef _WIN32 #ifdef _WIN32
// On Windows, the file is implicitly locked. // On Windows, the file is implicitly locked.
@ -902,6 +903,21 @@ void GameList::AddPlayedTimeForSerial(const std::string& serial, std::time_t las
} }
} }
std::time_t GameList::GetCachedPlayedTimeForSerial(const std::string& serial)
{
if (serial.empty())
return 0;
std::unique_lock<std::recursive_mutex> lock(s_mutex);
for (GameList::Entry& entry : s_entries)
{
if (entry.serial == serial)
return entry.total_played_time;
}
return 0;
}
TinyString GameList::FormatTimestamp(std::time_t timestamp) TinyString GameList::FormatTimestamp(std::time_t timestamp)
{ {
TinyString ret; TinyString ret;
@ -943,23 +959,33 @@ TinyString GameList::FormatTimestamp(std::time_t timestamp)
return ret; return ret;
} }
TinyString GameList::FormatTimespan(std::time_t timespan) TinyString GameList::FormatTimespan(std::time_t timespan, bool long_format)
{ {
const u32 hours = static_cast<u32>(timespan / 3600); const u32 hours = static_cast<u32>(timespan / 3600);
const u32 minutes = static_cast<u32>((timespan % 3600) / 60); const u32 minutes = static_cast<u32>((timespan % 3600) / 60);
const u32 seconds = static_cast<u32>((timespan % 3600) % 60); const u32 seconds = static_cast<u32>((timespan % 3600) % 60);
TinyString ret; TinyString ret;
if (hours >= 100) if (!long_format)
ret.Fmt(Host::TranslateString("GameList", "{}h {}m").GetCharArray(), hours, minutes); {
else if (hours > 0) if (hours >= 100)
ret.Fmt(Host::TranslateString("GameList", "{}h {}m {}s").GetCharArray(), hours, minutes, seconds); ret.Fmt(Host::TranslateString("GameList", "{}h {}m").GetCharArray(), hours, minutes);
else if (minutes > 0) else if (hours > 0)
ret.Fmt(Host::TranslateString("GameList", "{}m {}s").GetCharArray(), minutes, seconds); ret.Fmt(Host::TranslateString("GameList", "{}h {}m {}s").GetCharArray(), hours, minutes, seconds);
else if (seconds > 0) else if (minutes > 0)
ret.Fmt(Host::TranslateString("GameList", "{}s").GetCharArray(), seconds); ret.Fmt(Host::TranslateString("GameList", "{}m {}s").GetCharArray(), minutes, seconds);
else if (seconds > 0)
ret.Fmt(Host::TranslateString("GameList", "{}s").GetCharArray(), seconds);
else
ret = Host::TranslateString("GameList", "None");
}
else else
ret = Host::TranslateString("GameList", "None"); {
if (hours > 0)
ret = fmt::format(Host::TranslateString("GameList", "{} hours").GetCharArray(), hours);
else
ret = fmt::format(Host::TranslateString("GameList", "{} minutes").GetCharArray(), minutes);
}
return ret; return ret;
} }

View file

@ -79,11 +79,14 @@ void Refresh(bool invalidate_cache, bool only_cache = false, ProgressCallback* p
/// Add played time for the specified serial. /// Add played time for the specified serial.
void AddPlayedTimeForSerial(const std::string& serial, std::time_t last_time, std::time_t add_time); void AddPlayedTimeForSerial(const std::string& serial, std::time_t last_time, std::time_t add_time);
/// Returns the total time played for a game. Requires the game to be scanned in the list.
std::time_t GetCachedPlayedTimeForSerial(const std::string& serial);
/// Formats a timestamp to something human readable (e.g. Today, Yesterday, 10/11/12). /// Formats a timestamp to something human readable (e.g. Today, Yesterday, 10/11/12).
TinyString FormatTimestamp(std::time_t timestamp); TinyString FormatTimestamp(std::time_t timestamp);
/// Formats a timespan to something human readable (e.g. 1h2m3s). /// Formats a timespan to something human readable (e.g. 1h2m3s or 1 hour).
TinyString FormatTimespan(std::time_t timespan); TinyString FormatTimespan(std::time_t timespan, bool long_format = false);
std::string GetCoverImagePathForEntry(const Entry* entry); std::string GetCoverImagePathForEntry(const Entry* entry);
std::string GetCoverImagePath(const std::string& path, const std::string& serial, const std::string& title); std::string GetCoverImagePath(const std::string& path, const std::string& serial, const std::string& title);

View file

@ -440,7 +440,31 @@ void ImGuiFullscreen::BeginLayout()
// we evict from the texture cache at the start of the frame, in case we go over mid-frame, // we evict from the texture cache at the start of the frame, in case we go over mid-frame,
// we need to keep all those textures alive until the end of the frame // we need to keep all those textures alive until the end of the frame
s_texture_cache.ManualEvict(); s_texture_cache.ManualEvict();
PushResetLayout();
}
void ImGuiFullscreen::EndLayout()
{
DrawFileSelector();
DrawChoiceDialog();
DrawInputDialog();
DrawMessageDialog();
const float notification_margin = LayoutScale(10.0f);
const float spacing = LayoutScale(10.0f);
const float notification_vertical_pos = GetNotificationVerticalPosition();
ImVec2 position(notification_margin,
notification_vertical_pos * ImGui::GetIO().DisplaySize.y +
((notification_vertical_pos >= 0.5f) ? -notification_margin : notification_margin));
DrawBackgroundProgressDialogs(position, spacing);
DrawNotifications(position, spacing);
DrawToast();
PopResetLayout();
}
void ImGuiFullscreen::PushResetLayout()
{
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f); ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f); ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, LayoutScale(8.0f, 8.0f)); ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, LayoutScale(8.0f, 8.0f));
@ -465,23 +489,8 @@ void ImGuiFullscreen::BeginLayout()
ImGui::PushStyleColor(ImGuiCol_ScrollbarGrabActive, UIPrimaryDarkColor); ImGui::PushStyleColor(ImGuiCol_ScrollbarGrabActive, UIPrimaryDarkColor);
} }
void ImGuiFullscreen::EndLayout() void ImGuiFullscreen::PopResetLayout()
{ {
DrawFileSelector();
DrawChoiceDialog();
DrawInputDialog();
DrawMessageDialog();
const float notification_margin = LayoutScale(10.0f);
const float spacing = LayoutScale(10.0f);
const float notification_vertical_pos = GetNotificationVerticalPosition();
ImVec2 position(notification_margin,
notification_vertical_pos * ImGui::GetIO().DisplaySize.y +
((notification_vertical_pos >= 0.5f) ? -notification_margin : notification_margin));
DrawBackgroundProgressDialogs(position, spacing);
DrawNotifications(position, spacing);
DrawToast();
ImGui::PopStyleColor(10); ImGui::PopStyleColor(10);
ImGui::PopStyleVar(12); ImGui::PopStyleVar(12);
} }
@ -665,7 +674,7 @@ void ImGuiFullscreen::BeginMenuButtons(u32 num_items, float y_align, float x_pad
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, LayoutScale(x_padding, y_padding)); ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, LayoutScale(x_padding, y_padding));
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 0.0f); ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 0.0f);
ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 1.0f); ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, LayoutScale(1.0f));
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0.0f, 0.0f)); ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0.0f, 0.0f));
if (y_align != 0.0f) if (y_align != 0.0f)
@ -993,7 +1002,8 @@ bool ImGuiFullscreen::MenuImageButton(const char* title, const char* summary, Im
} }
bool ImGuiFullscreen::FloatingButton(const char* text, float x, float y, float width, float height, float anchor_x, bool ImGuiFullscreen::FloatingButton(const char* text, float x, float y, float width, float height, float anchor_x,
float anchor_y, bool enabled, ImFont* font, ImVec2* out_position) float anchor_y, bool enabled, ImFont* font, ImVec2* out_position,
bool repeat_button)
{ {
const ImVec2 text_size(font->CalcTextSizeA(font->FontSize, std::numeric_limits<float>::max(), 0.0f, text)); const ImVec2 text_size(font->CalcTextSizeA(font->FontSize, std::numeric_limits<float>::max(), 0.0f, text));
const ImVec2& padding(ImGui::GetStyle().FramePadding); const ImVec2& padding(ImGui::GetStyle().FramePadding);
@ -1047,11 +1057,14 @@ bool ImGuiFullscreen::FloatingButton(const char* text, float x, float y, float w
bool pressed; bool pressed;
if (enabled) if (enabled)
{ {
pressed = ImGui::ButtonBehavior(bb, id, &hovered, &held, 0); pressed = ImGui::ButtonBehavior(bb, id, &hovered, &held, repeat_button ? ImGuiButtonFlags_Repeat : 0);
if (hovered) if (hovered)
{ {
const float t = std::min(static_cast<float>(std::abs(std::sin(ImGui::GetTime() * 0.75) * 1.1)), 1.0f);
const ImU32 col = ImGui::GetColorU32(held ? ImGuiCol_ButtonActive : ImGuiCol_ButtonHovered, 1.0f); const ImU32 col = ImGui::GetColorU32(held ? ImGuiCol_ButtonActive : ImGuiCol_ButtonHovered, 1.0f);
ImGui::PushStyleColor(ImGuiCol_Border, ImGui::GetColorU32(ImGuiCol_Border, t));
ImGui::RenderFrame(bb.Min, bb.Max, col, true, 0.0f); ImGui::RenderFrame(bb.Min, bb.Max, col, true, 0.0f);
ImGui::PopStyleColor();
} }
} }
else else
@ -1456,6 +1469,13 @@ bool ImGuiFullscreen::EnumChoiceButtonImpl(const char* title, const char* summar
return changed; return changed;
} }
void ImGuiFullscreen::DrawShadowedText(ImDrawList* dl, ImFont* font, const ImVec2& pos, u32 col, const char* text,
const char* text_end /*= nullptr*/, float wrap_width /*= 0.0f*/)
{
dl->AddText(font, font->FontSize, pos + LayoutScale(1.0f, 1.0f), IM_COL32(0, 0, 0, 100), text, text_end, wrap_width);
dl->AddText(font, font->FontSize, pos, col, text, text_end, wrap_width);
}
void ImGuiFullscreen::BeginNavBar(float x_padding /*= LAYOUT_MENU_BUTTON_X_PADDING*/, void ImGuiFullscreen::BeginNavBar(float x_padding /*= LAYOUT_MENU_BUTTON_X_PADDING*/,
float y_padding /*= LAYOUT_MENU_BUTTON_Y_PADDING*/) float y_padding /*= LAYOUT_MENU_BUTTON_Y_PADDING*/)
{ {
@ -1711,6 +1731,7 @@ void ImGuiFullscreen::DrawFileSelector()
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, LayoutScale(10.0f)); ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, LayoutScale(10.0f));
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImGui::PushStyleVar(ImGuiStyleVar_FramePadding,
LayoutScale(LAYOUT_MENU_BUTTON_X_PADDING, LAYOUT_MENU_BUTTON_Y_PADDING)); LayoutScale(LAYOUT_MENU_BUTTON_X_PADDING, LAYOUT_MENU_BUTTON_Y_PADDING));
ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 0.0f);
ImGui::PushStyleColor(ImGuiCol_Text, UIPrimaryTextColor); ImGui::PushStyleColor(ImGuiCol_Text, UIPrimaryTextColor);
ImGui::PushStyleColor(ImGuiCol_TitleBg, UIPrimaryDarkColor); ImGui::PushStyleColor(ImGuiCol_TitleBg, UIPrimaryDarkColor);
ImGui::PushStyleColor(ImGuiCol_TitleBgActive, UIPrimaryColor); ImGui::PushStyleColor(ImGuiCol_TitleBgActive, UIPrimaryColor);
@ -1755,7 +1776,7 @@ void ImGuiFullscreen::DrawFileSelector()
} }
ImGui::PopStyleColor(4); ImGui::PopStyleColor(4);
ImGui::PopStyleVar(2); ImGui::PopStyleVar(3);
ImGui::PopFont(); ImGui::PopFont();
if (selected) if (selected)
@ -1821,6 +1842,7 @@ void ImGuiFullscreen::DrawChoiceDialog()
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, LayoutScale(10.0f)); ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, LayoutScale(10.0f));
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImGui::PushStyleVar(ImGuiStyleVar_FramePadding,
LayoutScale(LAYOUT_MENU_BUTTON_X_PADDING, LAYOUT_MENU_BUTTON_Y_PADDING)); LayoutScale(LAYOUT_MENU_BUTTON_X_PADDING, LAYOUT_MENU_BUTTON_Y_PADDING));
ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 0.0f);
ImGui::PushStyleColor(ImGuiCol_Text, UIPrimaryTextColor); ImGui::PushStyleColor(ImGuiCol_Text, UIPrimaryTextColor);
ImGui::PushStyleColor(ImGuiCol_TitleBg, UIPrimaryDarkColor); ImGui::PushStyleColor(ImGuiCol_TitleBg, UIPrimaryDarkColor);
ImGui::PushStyleColor(ImGuiCol_TitleBgActive, UIPrimaryColor); ImGui::PushStyleColor(ImGuiCol_TitleBgActive, UIPrimaryColor);
@ -1893,7 +1915,7 @@ void ImGuiFullscreen::DrawChoiceDialog()
} }
ImGui::PopStyleColor(4); ImGui::PopStyleColor(4);
ImGui::PopStyleVar(2); ImGui::PopStyleVar(3);
ImGui::PopFont(); ImGui::PopFont();
if (choice >= 0) if (choice >= 0)
@ -1938,6 +1960,7 @@ void ImGuiFullscreen::DrawInputDialog()
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, LayoutScale(10.0f)); ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, LayoutScale(10.0f));
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImGui::PushStyleVar(ImGuiStyleVar_FramePadding,
LayoutScale(LAYOUT_MENU_BUTTON_X_PADDING, LAYOUT_MENU_BUTTON_Y_PADDING)); LayoutScale(LAYOUT_MENU_BUTTON_X_PADDING, LAYOUT_MENU_BUTTON_Y_PADDING));
ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 0.0f);
ImGui::PushStyleColor(ImGuiCol_Text, UIPrimaryTextColor); ImGui::PushStyleColor(ImGuiCol_Text, UIPrimaryTextColor);
ImGui::PushStyleColor(ImGuiCol_TitleBg, UIPrimaryDarkColor); ImGui::PushStyleColor(ImGuiCol_TitleBg, UIPrimaryDarkColor);
ImGui::PushStyleColor(ImGuiCol_TitleBgActive, UIPrimaryColor); ImGui::PushStyleColor(ImGuiCol_TitleBgActive, UIPrimaryColor);
@ -1995,7 +2018,7 @@ void ImGuiFullscreen::DrawInputDialog()
CloseInputDialog(); CloseInputDialog();
ImGui::PopStyleColor(4); ImGui::PopStyleColor(4);
ImGui::PopStyleVar(2); ImGui::PopStyleVar(3);
ImGui::PopFont(); ImGui::PopFont();
} }
@ -2087,6 +2110,7 @@ void ImGuiFullscreen::DrawMessageDialog()
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, LayoutScale(10.0f)); ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, LayoutScale(10.0f));
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImGui::PushStyleVar(ImGuiStyleVar_FramePadding,
LayoutScale(LAYOUT_MENU_BUTTON_X_PADDING, LAYOUT_MENU_BUTTON_Y_PADDING)); LayoutScale(LAYOUT_MENU_BUTTON_X_PADDING, LAYOUT_MENU_BUTTON_Y_PADDING));
ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 0.0f);
ImGui::PushStyleColor(ImGuiCol_Text, UIPrimaryTextColor); ImGui::PushStyleColor(ImGuiCol_Text, UIPrimaryTextColor);
ImGui::PushStyleColor(ImGuiCol_TitleBg, UIPrimaryDarkColor); ImGui::PushStyleColor(ImGuiCol_TitleBg, UIPrimaryDarkColor);
ImGui::PushStyleColor(ImGuiCol_TitleBgActive, UIPrimaryColor); ImGui::PushStyleColor(ImGuiCol_TitleBgActive, UIPrimaryColor);
@ -2120,7 +2144,7 @@ void ImGuiFullscreen::DrawMessageDialog()
} }
ImGui::PopStyleColor(4); ImGui::PopStyleColor(4);
ImGui::PopStyleVar(3); ImGui::PopStyleVar(4);
ImGui::PopFont(); ImGui::PopFont();
if (!is_open || result.has_value()) if (!is_open || result.has_value())

View file

@ -106,6 +106,12 @@ static ALWAYS_INLINE ImVec4 MulAlpha(const ImVec4& v, float a)
return ImVec4(v.x, v.y, v.z, v.w * a); return ImVec4(v.x, v.y, v.z, v.w * a);
} }
static ALWAYS_INLINE std::string_view RemoveHash(const std::string_view& s)
{
const std::string_view::size_type pos = s.find('#');
return (pos != std::string_view::npos) ? s.substr(0, pos) : s;
}
/// Centers an image within the specified bounds, scaling up or down as needed. /// Centers an image within the specified bounds, scaling up or down as needed.
ImRect CenterImage(const ImVec2& fit_size, const ImVec2& image_size); ImRect CenterImage(const ImVec2& fit_size, const ImVec2& image_size);
ImRect CenterImage(const ImRect& fit_rect, const ImVec2& image_size); ImRect CenterImage(const ImRect& fit_rect, const ImVec2& image_size);
@ -131,6 +137,9 @@ void UploadAsyncTextures();
void BeginLayout(); void BeginLayout();
void EndLayout(); void EndLayout();
void PushResetLayout();
void PopResetLayout();
void QueueResetFocus(); void QueueResetFocus();
bool ResetFocusHere(); bool ResetFocusHere();
bool WantsToCloseMenu(); bool WantsToCloseMenu();
@ -181,7 +190,8 @@ bool MenuImageButton(const char* title, const char* summary, ImTextureID user_te
ImFont* font = g_large_font, ImFont* summary_font = g_medium_font); ImFont* font = g_large_font, ImFont* summary_font = g_medium_font);
bool FloatingButton(const char* text, float x, float y, float width = -1.0f, bool FloatingButton(const char* text, float x, float y, float width = -1.0f,
float height = LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY, float anchor_x = 0.0f, float anchor_y = 0.0f, float height = LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY, float anchor_x = 0.0f, float anchor_y = 0.0f,
bool enabled = true, ImFont* font = g_large_font, ImVec2* out_position = nullptr); bool enabled = true, ImFont* font = g_large_font, ImVec2* out_position = nullptr,
bool repeat_button = false);
bool ToggleButton(const char* title, const char* summary, bool* v, bool enabled = true, bool ToggleButton(const char* title, const char* summary, bool* v, bool enabled = true,
float height = LAYOUT_MENU_BUTTON_HEIGHT, ImFont* font = g_large_font, float height = LAYOUT_MENU_BUTTON_HEIGHT, ImFont* font = g_large_font,
ImFont* summary_font = g_medium_font); ImFont* summary_font = g_medium_font);
@ -221,6 +231,9 @@ ALWAYS_INLINE static bool EnumChoiceButton(const char* title, const char* summar
} }
} }
void DrawShadowedText(ImDrawList* dl, ImFont* font, const ImVec2& pos, u32 col, const char* text,
const char* text_end = nullptr, float wrap_width = 0.0f);
void BeginNavBar(float x_padding = LAYOUT_MENU_BUTTON_X_PADDING, float y_padding = LAYOUT_MENU_BUTTON_Y_PADDING); void BeginNavBar(float x_padding = LAYOUT_MENU_BUTTON_X_PADDING, float y_padding = LAYOUT_MENU_BUTTON_Y_PADDING);
void EndNavBar(); void EndNavBar();
void NavTitle(const char* title, float height = LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY, ImFont* font = g_large_font); void NavTitle(const char* title, float height = LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY, ImFont* font = g_large_font);

View file

@ -240,7 +240,8 @@ void ImGuiManager::DrawPerformanceOverlay()
} }
} }
} }
else if (g_settings.display_show_status_indicators && state == System::State::Paused) else if (g_settings.display_show_status_indicators && state == System::State::Paused &&
!FullscreenUI::HasActiveWindow())
{ {
text.Assign(ICON_FA_PAUSE); text.Assign(ICON_FA_PAUSE);
DRAW_LINE(standard_font, text, IM_COL32(255, 255, 255, 255)); DRAW_LINE(standard_font, text, IM_COL32(255, 255, 255, 255));

View file

@ -1390,6 +1390,19 @@ void InputManager::ReloadBindings(SettingsInterface& si, SettingsInterface& bind
// Source Management // Source Management
// ------------------------------------------------------------------------ // ------------------------------------------------------------------------
bool InputManager::ReloadDevices()
{
bool changed = false;
for (u32 i = FIRST_EXTERNAL_INPUT_SOURCE; i < LAST_EXTERNAL_INPUT_SOURCE; i++)
{
if (s_input_sources[i])
changed |= s_input_sources[i]->ReloadDevices();
}
return changed;
}
void InputManager::CloseSources() void InputManager::CloseSources()
{ {
for (u32 i = FIRST_EXTERNAL_INPUT_SOURCE; i < LAST_EXTERNAL_INPUT_SOURCE; i++) for (u32 i = FIRST_EXTERNAL_INPUT_SOURCE; i < LAST_EXTERNAL_INPUT_SOURCE; i++)

View file

@ -220,6 +220,10 @@ void ReloadBindings(SettingsInterface& si, SettingsInterface& binding_si);
/// Re-parses the sources part of the config and initializes any backends. /// Re-parses the sources part of the config and initializes any backends.
void ReloadSources(SettingsInterface& si, std::unique_lock<std::mutex>& settings_lock); void ReloadSources(SettingsInterface& si, std::unique_lock<std::mutex>& settings_lock);
/// Called when a device change is triggered by the system (DBT_DEVNODES_CHANGED on Windows).
/// Returns true if any device changes are detected.
bool ReloadDevices();
/// Shuts down any enabled input sources. /// Shuts down any enabled input sources.
void CloseSources(); void CloseSources();

View file

@ -20,6 +20,7 @@ public:
virtual bool Initialize(SettingsInterface& si, std::unique_lock<std::mutex>& settings_lock) = 0; virtual bool Initialize(SettingsInterface& si, std::unique_lock<std::mutex>& settings_lock) = 0;
virtual void UpdateSettings(SettingsInterface& si, std::unique_lock<std::mutex>& settings_lock) = 0; virtual void UpdateSettings(SettingsInterface& si, std::unique_lock<std::mutex>& settings_lock) = 0;
virtual bool ReloadDevices() = 0;
virtual void Shutdown() = 0; virtual void Shutdown() = 0;
virtual void PollEvents() = 0; virtual void PollEvents() = 0;

View file

@ -130,6 +130,13 @@ void SDLInputSource::LoadSettings(SettingsInterface& si)
m_controller_enhanced_mode = si.GetBoolValue("InputSources", "SDLControllerEnhancedMode", false); m_controller_enhanced_mode = si.GetBoolValue("InputSources", "SDLControllerEnhancedMode", false);
} }
bool SDLInputSource::ReloadDevices()
{
// We'll get a GC added/removed event here.
PollEvents();
return false;
}
void SDLInputSource::SetHints() void SDLInputSource::SetHints()
{ {
SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_PS4_RUMBLE, m_controller_enhanced_mode ? "1" : "0"); SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_PS4_RUMBLE, m_controller_enhanced_mode ? "1" : "0");

View file

@ -16,6 +16,7 @@ public:
bool Initialize(SettingsInterface& si, std::unique_lock<std::mutex>& settings_lock) override; bool Initialize(SettingsInterface& si, std::unique_lock<std::mutex>& settings_lock) override;
void UpdateSettings(SettingsInterface& si, std::unique_lock<std::mutex>& settings_lock) override; void UpdateSettings(SettingsInterface& si, std::unique_lock<std::mutex>& settings_lock) override;
bool ReloadDevices() override;
void Shutdown() override; void Shutdown() override;
void PollEvents() override; void PollEvents() override;

View file

@ -548,7 +548,14 @@ void VulkanHostDisplay::DestroyResources()
bool VulkanHostDisplay::CreateImGuiContext() bool VulkanHostDisplay::CreateImGuiContext()
{ {
return ImGui_ImplVulkan_Init(m_swap_chain->GetClearRenderPass()); const VkRenderPass render_pass =
m_swap_chain ? m_swap_chain->GetClearRenderPass() :
g_vulkan_context->GetRenderPass(VK_FORMAT_R8G8B8A8_UNORM, VK_FORMAT_UNDEFINED, VK_SAMPLE_COUNT_1_BIT,
VK_ATTACHMENT_LOAD_OP_CLEAR);
if (render_pass == VK_NULL_HANDLE)
return false;
return ImGui_ImplVulkan_Init(render_pass);
} }
void VulkanHostDisplay::DestroyImGuiContext() void VulkanHostDisplay::DestroyImGuiContext()

View file

@ -48,6 +48,11 @@ bool Win32RawInputSource::Initialize(SettingsInterface& si, std::unique_lock<std
void Win32RawInputSource::UpdateSettings(SettingsInterface& si, std::unique_lock<std::mutex>& settings_lock) {} void Win32RawInputSource::UpdateSettings(SettingsInterface& si, std::unique_lock<std::mutex>& settings_lock) {}
bool Win32RawInputSource::ReloadDevices()
{
return false;
}
void Win32RawInputSource::Shutdown() void Win32RawInputSource::Shutdown()
{ {
CloseDevices(); CloseDevices();

View file

@ -16,6 +16,7 @@ public:
bool Initialize(SettingsInterface& si, std::unique_lock<std::mutex>& settings_lock) override; bool Initialize(SettingsInterface& si, std::unique_lock<std::mutex>& settings_lock) override;
void UpdateSettings(SettingsInterface& si, std::unique_lock<std::mutex>& settings_lock) override; void UpdateSettings(SettingsInterface& si, std::unique_lock<std::mutex>& settings_lock) override;
bool ReloadDevices() override;
void Shutdown() override; void Shutdown() override;
void PollEvents() override; void PollEvents() override;

View file

@ -126,6 +126,35 @@ bool XInputSource::Initialize(SettingsInterface& si, std::unique_lock<std::mutex
void XInputSource::UpdateSettings(SettingsInterface& si, std::unique_lock<std::mutex>& settings_lock) {} void XInputSource::UpdateSettings(SettingsInterface& si, std::unique_lock<std::mutex>& settings_lock) {}
bool XInputSource::ReloadDevices()
{
bool changed = false;
for (u32 i = 0; i < NUM_CONTROLLERS; i++)
{
XINPUT_STATE new_state;
DWORD result = m_xinput_get_state(i, &new_state);
if (result == ERROR_SUCCESS)
{
if (m_controllers[i].connected)
continue;
HandleControllerConnection(i);
changed = true;
}
else if (result == ERROR_DEVICE_NOT_CONNECTED)
{
if (!m_controllers[i].connected)
continue;
HandleControllerDisconnection(i);
changed = true;
}
}
return changed;
}
void XInputSource::Shutdown() void XInputSource::Shutdown()
{ {
for (u32 i = 0; i < NUM_CONTROLLERS; i++) for (u32 i = 0; i < NUM_CONTROLLERS; i++)
@ -152,6 +181,8 @@ void XInputSource::PollEvents()
for (u32 i = 0; i < NUM_CONTROLLERS; i++) for (u32 i = 0; i < NUM_CONTROLLERS; i++)
{ {
const bool was_connected = m_controllers[i].connected; const bool was_connected = m_controllers[i].connected;
if (!was_connected)
continue;
XINPUT_STATE new_state; XINPUT_STATE new_state;
DWORD result = m_xinput_get_state(i, &new_state); DWORD result = m_xinput_get_state(i, &new_state);

View file

@ -17,6 +17,7 @@ public:
bool Initialize(SettingsInterface& si, std::unique_lock<std::mutex>& settings_lock) override; bool Initialize(SettingsInterface& si, std::unique_lock<std::mutex>& settings_lock) override;
void UpdateSettings(SettingsInterface& si, std::unique_lock<std::mutex>& settings_lock) override; void UpdateSettings(SettingsInterface& si, std::unique_lock<std::mutex>& settings_lock) override;
bool ReloadDevices() override;
void Shutdown() override; void Shutdown() override;
void PollEvents() override; void PollEvents() override;