From ef3ad91ad0969013bc29ad1276c13aa2842ecc2b Mon Sep 17 00:00:00 2001 From: Connor McLaughlin Date: Sat, 17 Sep 2022 15:51:05 +1000 Subject: [PATCH] FullscreenUI: Various improvements --- src/common/lru_cache.h | 8 +- src/common/string_util.cpp | 25 + src/common/string_util.h | 1 + src/duckstation-nogui/nogui_host.cpp | 79 -- src/duckstation-qt/audiosettingswidget.cpp | 3 + src/duckstation-qt/autoupdaterdialog.cpp | 1 + src/duckstation-qt/consolesettingswidget.cpp | 1 + .../controllerbindingwidgets.cpp | 1 + .../controllersettingsdialog.cpp | 1 + .../controllersettingwidgetbinder.h | 5 +- src/duckstation-qt/coverdownloaddialog.cpp | 13 +- src/duckstation-qt/coverdownloaddialog.h | 3 +- src/duckstation-qt/coverdownloaddialog.ui | 72 +- src/duckstation-qt/displaywidget.cpp | 5 +- .../gamelistsearchdirectoriesmodel.cpp | 2 + src/duckstation-qt/gamelistsettingswidget.cpp | 4 +- src/duckstation-qt/gamelistwidget.cpp | 8 + src/duckstation-qt/inputbindingdialog.cpp | 1 + src/duckstation-qt/inputbindingwidgets.cpp | 4 + src/duckstation-qt/mainwindow.cpp | 18 +- src/duckstation-qt/qthost.cpp | 77 -- .../resources/icons/black/svg/image-fill.svg | 1 + .../resources/icons/white/svg/image-fill.svg | 1 + src/duckstation-qt/resources/resources.qrc | 2 + src/duckstation-qt/settingsdialog.cpp | 5 + src/duckstation-qt/settingwidgetbinder.h | 9 + src/frontend-common/fullscreen_ui.cpp | 1008 +++++++++++++---- src/frontend-common/fullscreen_ui.h | 2 + src/frontend-common/game_list.cpp | 39 +- src/frontend-common/game_list.h | 13 +- src/frontend-common/host_settings.cpp | 49 + src/frontend-common/imgui_fullscreen.cpp | 361 ++++-- src/frontend-common/imgui_fullscreen.h | 29 +- src/frontend-common/imgui_manager.cpp | 13 +- 34 files changed, 1350 insertions(+), 514 deletions(-) create mode 100644 src/duckstation-qt/resources/icons/black/svg/image-fill.svg create mode 100644 src/duckstation-qt/resources/icons/white/svg/image-fill.svg diff --git a/src/common/lru_cache.h b/src/common/lru_cache.h index aafae1837..63fa6a9b2 100644 --- a/src/common/lru_cache.h +++ b/src/common/lru_cache.h @@ -69,7 +69,7 @@ public: void Evict(std::size_t count = 1) { - while (m_items.size() >= count) + while (!m_items.empty() && count > 0) { typename MapType::iterator lowest = m_items.end(); for (auto iter = m_items.begin(); iter != m_items.end(); ++iter) @@ -78,6 +78,7 @@ public: lowest = iter; } m_items.erase(lowest); + count--; } } @@ -87,23 +88,20 @@ public: auto iter = m_items.find(key); if (iter == m_items.end()) return false; - m_items.erase(iter); return true; } - void SetManualEvict(bool block) { m_manual_evict = block; if (!m_manual_evict) ManualEvict(); } - void ManualEvict() { // evict if we went over while (m_items.size() > m_max_capacity) - Evict(m_items.size() - (m_max_capacity - 1)); + Evict(m_items.size() - m_max_capacity); } private: diff --git a/src/common/string_util.cpp b/src/common/string_util.cpp index b43aabe3d..6d1749ea8 100644 --- a/src/common/string_util.cpp +++ b/src/common/string_util.cpp @@ -255,6 +255,31 @@ std::vector StringUtil::SplitString(const std::string_view& st return res; } +std::vector StringUtil::SplitNewString(const std::string_view& str, char delimiter, + bool skip_empty /*= true*/) +{ + std::vector res; + std::string_view::size_type last_pos = 0; + std::string_view::size_type pos; + while (last_pos < str.size() && (pos = str.find(delimiter, last_pos)) != std::string_view::npos) + { + std::string_view part(StripWhitespace(str.substr(last_pos, pos - last_pos))); + if (!skip_empty || !part.empty()) + res.emplace_back(part); + + last_pos = pos + 1; + } + + if (last_pos < str.size()) + { + std::string_view part(StripWhitespace(str.substr(last_pos))); + if (!skip_empty || !part.empty()) + res.emplace_back(part); + } + + return res; +} + std::string StringUtil::ReplaceAll(const std::string_view& subject, const std::string_view& search, const std::string_view& replacement) { diff --git a/src/common/string_util.h b/src/common/string_util.h index f2e6741c5..39903efa3 100644 --- a/src/common/string_util.h +++ b/src/common/string_util.h @@ -149,6 +149,7 @@ void StripWhitespace(std::string* str); /// Splits a string based on a single character delimiter. std::vector SplitString(const std::string_view& str, char delimiter, bool skip_empty = true); +std::vector SplitNewString(const std::string_view& str, char delimiter, bool skip_empty = true); /// Joins a string together using the specified delimiter. template diff --git a/src/duckstation-nogui/nogui_host.cpp b/src/duckstation-nogui/nogui_host.cpp index c8acb63de..b14408f1a 100644 --- a/src/duckstation-nogui/nogui_host.cpp +++ b/src/duckstation-nogui/nogui_host.cpp @@ -376,68 +376,6 @@ void Host::CheckForSettingsChanges(const Settings& old_settings) CommonHost::CheckForSettingsChanges(old_settings); } -void Host::SetBaseBoolSettingValue(const char* section, const char* key, bool value) -{ - auto lock = Host::GetSettingsLock(); - s_base_settings_interface->SetBoolValue(section, key, value); - NoGUIHost::SaveSettings(); -} - -void Host::SetBaseIntSettingValue(const char* section, const char* key, int value) -{ - auto lock = Host::GetSettingsLock(); - s_base_settings_interface->SetIntValue(section, key, value); - NoGUIHost::SaveSettings(); -} - -void Host::SetBaseFloatSettingValue(const char* section, const char* key, float value) -{ - auto lock = Host::GetSettingsLock(); - s_base_settings_interface->SetFloatValue(section, key, value); - NoGUIHost::SaveSettings(); -} - -void Host::SetBaseStringSettingValue(const char* section, const char* key, const char* value) -{ - auto lock = Host::GetSettingsLock(); - s_base_settings_interface->SetStringValue(section, key, value); - NoGUIHost::SaveSettings(); -} - -void Host::SetBaseStringListSettingValue(const char* section, const char* key, const std::vector& values) -{ - auto lock = Host::GetSettingsLock(); - s_base_settings_interface->SetStringList(section, key, values); - NoGUIHost::SaveSettings(); -} - -bool Host::AddValueToBaseStringListSetting(const char* section, const char* key, const char* value) -{ - auto lock = Host::GetSettingsLock(); - if (!s_base_settings_interface->AddToStringList(section, key, value)) - return false; - - NoGUIHost::SaveSettings(); - return true; -} - -bool Host::RemoveValueFromBaseStringListSetting(const char* section, const char* key, const char* value) -{ - auto lock = Host::GetSettingsLock(); - if (!s_base_settings_interface->RemoveFromStringList(section, key, value)) - return false; - - NoGUIHost::SaveSettings(); - return true; -} - -void Host::DeleteBaseSettingValue(const char* section, const char* key) -{ - auto lock = Host::GetSettingsLock(); - s_base_settings_interface->DeleteValue(section, key); - NoGUIHost::SaveSettings(); -} - void Host::CommitBaseSettingChanges() { NoGUIHost::SaveSettings(); @@ -1008,23 +946,6 @@ void Host::CancelGameListRefresh() NoGUIHost::CancelAsyncOp(); } -void Host::DownloadCoversAsync(std::vector url_templates) -{ - NoGUIHost::StartAsyncOp([url_templates = std::move(url_templates)](ProgressCallback* progress) { - GameList::DownloadCovers(url_templates, progress); - }); -} - -void Host::CancelCoversDownload() -{ - NoGUIHost::CancelAsyncOp(); -} - -void Host::CoversChanged() -{ - Host::RunOnCPUThread([]() { FullscreenUI::InvalidateCoverCache(); }); -} - bool Host::IsFullscreen() { return s_is_fullscreen; diff --git a/src/duckstation-qt/audiosettingswidget.cpp b/src/duckstation-qt/audiosettingswidget.cpp index d4227159c..88f64ad55 100644 --- a/src/duckstation-qt/audiosettingswidget.cpp +++ b/src/duckstation-qt/audiosettingswidget.cpp @@ -162,6 +162,7 @@ void AudioSettingsWidget::onOutputVolumeChanged(int new_value) // only called for base settings DebugAssert(!m_dialog->isPerGameSettings()); Host::SetBaseIntSettingValue("Audio", "OutputVolume", new_value); + Host::CommitBaseSettingChanges(); g_emu_thread->setAudioOutputVolume(new_value, m_ui.fastForwardVolume->value()); updateVolumeLabel(); @@ -172,6 +173,7 @@ void AudioSettingsWidget::onFastForwardVolumeChanged(int new_value) // only called for base settings DebugAssert(!m_dialog->isPerGameSettings()); Host::SetBaseIntSettingValue("Audio", "FastForwardVolume", new_value); + Host::CommitBaseSettingChanges(); g_emu_thread->setAudioOutputVolume(m_ui.volume->value(), new_value); updateVolumeLabel(); @@ -184,5 +186,6 @@ void AudioSettingsWidget::onOutputMutedChanged(int new_state) const bool muted = (new_state != 0); Host::SetBaseBoolSettingValue("Audio", "OutputMuted", muted); + Host::CommitBaseSettingChanges(); g_emu_thread->setAudioOutputMuted(muted); } diff --git a/src/duckstation-qt/autoupdaterdialog.cpp b/src/duckstation-qt/autoupdaterdialog.cpp index 2a11f9671..249ca9980 100644 --- a/src/duckstation-qt/autoupdaterdialog.cpp +++ b/src/duckstation-qt/autoupdaterdialog.cpp @@ -436,6 +436,7 @@ bool AutoUpdaterDialog::updateNeeded() const void AutoUpdaterDialog::skipThisUpdateClicked() { Host::SetBaseStringSettingValue("AutoUpdater", "LastVersion", m_latest_sha.toUtf8().constData()); + Host::CommitBaseSettingChanges(); done(0); } diff --git a/src/duckstation-qt/consolesettingswidget.cpp b/src/duckstation-qt/consolesettingswidget.cpp index 6daa9f99a..a252ca33a 100644 --- a/src/duckstation-qt/consolesettingswidget.cpp +++ b/src/duckstation-qt/consolesettingswidget.cpp @@ -156,6 +156,7 @@ void ConsoleSettingsWidget::onEnableCPUClockSpeedControlChecked(int state) } Host::SetBaseBoolSettingValue("UI", "CPUOverclockingWarningShown", true); + Host::CommitBaseSettingChanges(); } m_ui.cpuClockSpeed->setEnabled(m_dialog->getEffectiveBoolValue("CPU", "OverclockEnable", false)); diff --git a/src/duckstation-qt/controllerbindingwidgets.cpp b/src/duckstation-qt/controllerbindingwidgets.cpp index c1d571166..6bb7a8459 100644 --- a/src/duckstation-qt/controllerbindingwidgets.cpp +++ b/src/duckstation-qt/controllerbindingwidgets.cpp @@ -177,6 +177,7 @@ void ControllerBindingWidget::onTypeChanged() { Host::SetBaseStringSettingValue(m_config_section.c_str(), "Type", Settings::GetControllerTypeName(m_controller_type)); + Host::CommitBaseSettingChanges(); g_emu_thread->applySettings(); } diff --git a/src/duckstation-qt/controllersettingsdialog.cpp b/src/duckstation-qt/controllersettingsdialog.cpp index 5d4e862d5..7eed44da9 100644 --- a/src/duckstation-qt/controllersettingsdialog.cpp +++ b/src/duckstation-qt/controllersettingsdialog.cpp @@ -308,6 +308,7 @@ void ControllerSettingsDialog::setStringValue(const char* section, const char* k else { Host::SetBaseStringSettingValue(section, key, value); + Host::CommitBaseSettingChanges(); g_emu_thread->applySettings(); } } diff --git a/src/duckstation-qt/controllersettingwidgetbinder.h b/src/duckstation-qt/controllersettingwidgetbinder.h index d6b048543..a47d78c23 100644 --- a/src/duckstation-qt/controllersettingwidgetbinder.h +++ b/src/duckstation-qt/controllersettingwidgetbinder.h @@ -46,6 +46,7 @@ static void BindWidgetToInputProfileBool(SettingsInterface* sif, WidgetType* wid Accessor::connectValueChanged(widget, [widget, section = std::move(section), key = std::move(key)]() { const bool new_value = Accessor::getBoolValue(widget); Host::SetBaseBoolSettingValue(section.c_str(), key.c_str(), new_value); + Host::CommitBaseSettingChanges(); g_emu_thread->applySettings(); }); } @@ -78,6 +79,7 @@ static void BindWidgetToInputProfileFloat(SettingsInterface* sif, WidgetType* wi Accessor::connectValueChanged(widget, [widget, section = std::move(section), key = std::move(key)]() { const float new_value = Accessor::getFloatValue(widget); Host::SetBaseFloatSettingValue(section.c_str(), key.c_str(), new_value); + Host::CommitBaseSettingChanges(); g_emu_thread->applySettings(); }); } @@ -110,6 +112,7 @@ static void BindWidgetToInputProfileNormalized(SettingsInterface* sif, WidgetTyp Accessor::connectValueChanged(widget, [widget, section = std::move(section), key = std::move(key), range]() { const float new_value = (static_cast(Accessor::getIntValue(widget)) / range); Host::SetBaseFloatSettingValue(section.c_str(), key.c_str(), new_value); + Host::CommitBaseSettingChanges(); g_emu_thread->applySettings(); }); } @@ -153,7 +156,7 @@ static void BindWidgetToInputProfileString(SettingsInterface* sif, WidgetType* w Host::SetBaseStringSettingValue(section.c_str(), key.c_str(), new_value.toUtf8().constData()); else Host::DeleteBaseSettingValue(section.c_str(), key.c_str()); - + Host::CommitBaseSettingChanges(); g_emu_thread->applySettings(); }); } diff --git a/src/duckstation-qt/coverdownloaddialog.cpp b/src/duckstation-qt/coverdownloaddialog.cpp index 88f3bcf96..6cd5cc0af 100644 --- a/src/duckstation-qt/coverdownloaddialog.cpp +++ b/src/duckstation-qt/coverdownloaddialog.cpp @@ -5,6 +5,7 @@ CoverDownloadDialog::CoverDownloadDialog(QWidget* parent /*= nullptr*/) : QDialog(parent) { m_ui.setupUi(this); + m_ui.coverIcon->setPixmap(QIcon::fromTheme("image-fill").pixmap(32)); updateEnabled(); connect(m_ui.start, &QPushButton::clicked, this, &CoverDownloadDialog::onStartClicked); @@ -29,7 +30,8 @@ void CoverDownloadDialog::onDownloadStatus(const QString& text) void CoverDownloadDialog::onDownloadProgress(int value, int range) { - // limit to once every five seconds + // Limit to once every five seconds, otherwise it's way too flickery. + // Ideally in the future we'd have some way to invalidate only a single cover. if (m_last_refresh_time.GetTimeSeconds() >= 5.0f) { emit coverRefreshRequested(); @@ -83,7 +85,8 @@ void CoverDownloadDialog::updateEnabled() void CoverDownloadDialog::startThread() { - m_thread = std::make_unique(this, m_ui.urls->toPlainText()); + m_thread = std::make_unique(this, m_ui.urls->toPlainText(), m_ui.useSerialFileNames->isChecked()); + m_last_refresh_time.Reset(); connect(m_thread.get(), &CoverDownloadThread::statusUpdated, this, &CoverDownloadDialog::onDownloadStatus); connect(m_thread.get(), &CoverDownloadThread::progressUpdated, this, &CoverDownloadDialog::onDownloadProgress); connect(m_thread.get(), &CoverDownloadThread::threadFinished, this, &CoverDownloadDialog::onDownloadComplete); @@ -101,8 +104,8 @@ void CoverDownloadDialog::cancelThread() m_thread.reset(); } -CoverDownloadDialog::CoverDownloadThread::CoverDownloadThread(QWidget* parent, const QString& urls) - : QtAsyncProgressThread(parent) +CoverDownloadDialog::CoverDownloadThread::CoverDownloadThread(QWidget* parent, const QString& urls, bool use_serials) + : QtAsyncProgressThread(parent), m_use_serials(use_serials) { for (const QString& str : urls.split(QChar('\n'))) m_urls.push_back(str.toStdString()); @@ -112,5 +115,5 @@ CoverDownloadDialog::CoverDownloadThread::~CoverDownloadThread() = default; void CoverDownloadDialog::CoverDownloadThread::runAsync() { - GameList::DownloadCovers(m_urls, this); + GameList::DownloadCovers(m_urls, m_use_serials, this); } \ No newline at end of file diff --git a/src/duckstation-qt/coverdownloaddialog.h b/src/duckstation-qt/coverdownloaddialog.h index 573b21d1f..ee3d05457 100644 --- a/src/duckstation-qt/coverdownloaddialog.h +++ b/src/duckstation-qt/coverdownloaddialog.h @@ -34,7 +34,7 @@ private: class CoverDownloadThread : public QtAsyncProgressThread { public: - CoverDownloadThread(QWidget* parent, const QString& urls); + CoverDownloadThread(QWidget* parent, const QString& urls, bool use_serials); ~CoverDownloadThread(); protected: @@ -42,6 +42,7 @@ private: private: std::vector m_urls; + bool m_use_serials; }; void startThread(); diff --git a/src/duckstation-qt/coverdownloaddialog.ui b/src/duckstation-qt/coverdownloaddialog.ui index a2faaa2c8..fde2f9e2b 100644 --- a/src/duckstation-qt/coverdownloaddialog.ui +++ b/src/duckstation-qt/coverdownloaddialog.ui @@ -6,8 +6,8 @@ 0 0 - 656 - 343 + 720 + 380 @@ -15,9 +15,39 @@ - + + + 10 + + + + + + + + :/icons/black/svg/image-fill.svg + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + + + + + + DuckStation can automatically download covers for games which do not currently have a cover set. We do not host any cover images, the user must provide their own source for images. + + + true + + + + + + + - <html><head/><body><p>DuckStation can automatically download covers for games which do not currently have a cover set. We do not host any cover images, the user must provide their own source for images.</p><p>In the form below, specify the URLs to download covers from, with one template URL per line. The following variables are available:</p><p><span style=" font-style:italic;">${title}:</span> Title of the game.<br/><span style=" font-style:italic;">${filetitle}:</span> Name component of the game's filename.<br/><span style=" font-style:italic;">${serial}:</span> Serial of the game.</p><p><span style=" font-weight:700;">Example:</span> https://www.example-not-a-real-domain.com/covers/${serial}.jpg</p></body></html> + <html><head/><body><p>In the box below, specify the URLs to download covers from, with one template URL per line. The following variables are available:</p><p><span style=" font-style:italic;">${title}:</span> Title of the game.<br/><span style=" font-style:italic;">${filetitle}:</span> Name component of the game's filename.<br/><span style=" font-style:italic;">${serial}:</span> Serial of the game.</p><p><span style=" font-weight:700;">Example:</span> https://www.example-not-a-real-domain.com/covers/${serial}.jpg</p></body></html> true @@ -25,20 +55,24 @@ - - - Qt::Vertical - - - - 20 - 40 - - - + - + + + By default, the downloaded covers will be saved with the game's title. If this is not desired, you can check the "Use Serial File Names" box below. Using serials instead of game titles will prevent conflicts when multiple regions of the same game are used. + + + true + + + + + + + Use Serial File Names + + @@ -76,6 +110,8 @@ - + + + - + \ No newline at end of file diff --git a/src/duckstation-qt/displaywidget.cpp b/src/duckstation-qt/displaywidget.cpp index abc8478cb..32628a9de 100644 --- a/src/duckstation-qt/displaywidget.cpp +++ b/src/duckstation-qt/displaywidget.cpp @@ -225,7 +225,10 @@ bool DisplayWidget::event(QEvent* event) if (ImGuiManager::WantsTextInput() && key_event->type() == QEvent::KeyPress) { - const QString text(key_event->text()); + // Don't forward backspace characters. We send the backspace as a normal key event, + // so if we send the character too, it double-deletes. + QString text(key_event->text()); + text.remove(QChar('\b')); if (!text.isEmpty()) emit windowTextEntered(text); } diff --git a/src/duckstation-qt/gamelistsearchdirectoriesmodel.cpp b/src/duckstation-qt/gamelistsearchdirectoriesmodel.cpp index 460fa3e8a..ae4bfcd09 100644 --- a/src/duckstation-qt/gamelistsearchdirectoriesmodel.cpp +++ b/src/duckstation-qt/gamelistsearchdirectoriesmodel.cpp @@ -176,4 +176,6 @@ void GameListSearchDirectoriesModel::saveToSettings() Host::DeleteBaseSettingValue("GameList", "RecursivePaths"); else Host::SetBaseStringListSettingValue("GameList", "RecursivePaths", recursive_paths); + + Host::CommitBaseSettingChanges(); } diff --git a/src/duckstation-qt/gamelistsettingswidget.cpp b/src/duckstation-qt/gamelistsettingswidget.cpp index f8f228590..ed3b81423 100644 --- a/src/duckstation-qt/gamelistsettingswidget.cpp +++ b/src/duckstation-qt/gamelistsettingswidget.cpp @@ -55,6 +55,7 @@ bool GameListSettingsWidget::addExcludedPath(const std::string& path) if (!Host::AddValueToBaseStringListSetting("GameList", "ExcludedPaths", path.c_str())) return false; + Host::CommitBaseSettingChanges(); m_ui.excludedPaths->addItem(QString::fromStdString(path)); g_main_window->refreshGameList(false); return true; @@ -158,7 +159,8 @@ void GameListSettingsWidget::onRemoveExcludedPathButtonClicked() if (!item) return; - Host::RemoveValueFromBaseStringListSetting("GameList", "ExcludedPaths", item->text().toUtf8().constData()); + if (Host::RemoveValueFromBaseStringListSetting("GameList", "ExcludedPaths", item->text().toUtf8().constData())) + Host::CommitBaseSettingChanges(); delete item; g_main_window->refreshGameList(false); diff --git a/src/duckstation-qt/gamelistwidget.cpp b/src/duckstation-qt/gamelistwidget.cpp index 92a8e1a80..5ad3e23a4 100644 --- a/src/duckstation-qt/gamelistwidget.cpp +++ b/src/duckstation-qt/gamelistwidget.cpp @@ -322,6 +322,7 @@ void GameListWidget::listZoom(float delta) { const float new_scale = std::clamp(m_model->getCoverScale() + delta, MIN_SCALE, MAX_SCALE); Host::SetBaseFloatSettingValue("UI", "GameListCoverArtScale", new_scale); + Host::CommitBaseSettingChanges(); m_model->setCoverScale(new_scale); m_model->updateCacheSize(width(), height()); updateListFont(); @@ -345,6 +346,7 @@ void GameListWidget::gridIntScale(int int_scale) const float new_scale = std::clamp(static_cast(int_scale) / 100.0f, MIN_SCALE, MAX_SCALE); Host::SetBaseFloatSettingValue("UI", "GameListCoverArtScale", new_scale); + Host::CommitBaseSettingChanges(); m_model->setCoverScale(new_scale); m_model->updateCacheSize(width(), height()); updateListFont(); @@ -367,6 +369,7 @@ void GameListWidget::showGameList() } Host::SetBaseBoolSettingValue("UI", "GameListGridView", false); + Host::CommitBaseSettingChanges(); m_ui.stack->setCurrentIndex(0); resizeTableViewColumnsToFit(); updateToolbar(); @@ -382,6 +385,7 @@ void GameListWidget::showGameGrid() } Host::SetBaseBoolSettingValue("UI", "GameListGridView", true); + Host::CommitBaseSettingChanges(); m_ui.stack->setCurrentIndex(1); updateToolbar(); emit layoutChange(); @@ -396,6 +400,7 @@ void GameListWidget::setShowCoverTitles(bool enabled) } Host::SetBaseBoolSettingValue("UI", "GameListShowCoverTitles", enabled); + Host::CommitBaseSettingChanges(); m_model->setShowCoverTitles(enabled); if (isShowingGameGrid()) m_model->refresh(); @@ -499,6 +504,7 @@ void GameListWidget::saveTableViewColumnVisibilitySettings() { const bool visible = !m_table_view->isColumnHidden(column); Host::SetBaseBoolSettingValue("GameListTableView", getColumnVisibilitySettingsKeyName(column), visible); + Host::CommitBaseSettingChanges(); } } @@ -506,6 +512,7 @@ void GameListWidget::saveTableViewColumnVisibilitySettings(int column) { const bool visible = !m_table_view->isColumnHidden(column); Host::SetBaseBoolSettingValue("GameListTableView", getColumnVisibilitySettingsKeyName(column), visible); + Host::CommitBaseSettingChanges(); } void GameListWidget::loadTableViewColumnSortSettings() @@ -533,6 +540,7 @@ void GameListWidget::saveTableViewColumnSortSettings() } Host::SetBaseBoolSettingValue("GameListTableView", "SortDescending", sort_descending); + Host::CommitBaseSettingChanges(); } const GameList::Entry* GameListWidget::getSelectedEntry() const diff --git a/src/duckstation-qt/inputbindingdialog.cpp b/src/duckstation-qt/inputbindingdialog.cpp index b6b6fe4a8..8e07f0945 100644 --- a/src/duckstation-qt/inputbindingdialog.cpp +++ b/src/duckstation-qt/inputbindingdialog.cpp @@ -238,6 +238,7 @@ void InputBindingDialog::saveListToSettings() Host::SetBaseStringListSettingValue(m_section_name.c_str(), m_key_name.c_str(), m_bindings); else Host::DeleteBaseSettingValue(m_section_name.c_str(), m_key_name.c_str()); + Host::CommitBaseSettingChanges(); g_emu_thread->reloadInputBindings(); } } diff --git a/src/duckstation-qt/inputbindingwidgets.cpp b/src/duckstation-qt/inputbindingwidgets.cpp index 7d31e81e2..a5cf4cd9d 100644 --- a/src/duckstation-qt/inputbindingwidgets.cpp +++ b/src/duckstation-qt/inputbindingwidgets.cpp @@ -215,6 +215,7 @@ void InputBindingWidget::setNewBinding() else { Host::SetBaseStringSettingValue(m_section_name.c_str(), m_key_name.c_str(), new_binding.c_str()); + Host::CommitBaseSettingChanges(); g_emu_thread->reloadInputBindings(); } } @@ -235,6 +236,7 @@ void InputBindingWidget::clearBinding() else { Host::DeleteBaseSettingValue(m_section_name.c_str(), m_key_name.c_str()); + Host::CommitBaseSettingChanges(); g_emu_thread->reloadInputBindings(); } reloadBinding(); @@ -391,6 +393,7 @@ void InputVibrationBindingWidget::clearBinding() { m_binding = {}; Host::DeleteBaseSettingValue(m_section_name.c_str(), m_key_name.c_str()); + Host::CommitBaseSettingChanges(); g_emu_thread->reloadInputBindings(); setText(QString()); } @@ -428,6 +431,7 @@ void InputVibrationBindingWidget::onClicked() const QString new_value(input_dialog.textValue()); m_binding = new_value.toStdString(); Host::SetBaseStringSettingValue(m_section_name.c_str(), m_key_name.c_str(), m_binding.c_str()); + Host::CommitBaseSettingChanges(); setText(new_value); } diff --git a/src/duckstation-qt/mainwindow.cpp b/src/duckstation-qt/mainwindow.cpp index 5d818a454..96c2378b9 100644 --- a/src/duckstation-qt/mainwindow.cpp +++ b/src/duckstation-qt/mainwindow.cpp @@ -1119,18 +1119,21 @@ void MainWindow::onRemoveDiscActionTriggered() void MainWindow::onViewToolbarActionToggled(bool checked) { Host::SetBaseBoolSettingValue("UI", "ShowToolbar", checked); + Host::CommitBaseSettingChanges(); m_ui.toolBar->setVisible(checked); } void MainWindow::onViewLockToolbarActionToggled(bool checked) { Host::SetBaseBoolSettingValue("UI", "LockToolbar", checked); + Host::CommitBaseSettingChanges(); m_ui.toolBar->setMovable(!checked); } void MainWindow::onViewStatusBarActionToggled(bool checked) { Host::SetBaseBoolSettingValue("UI", "ShowStatusBar", checked); + Host::CommitBaseSettingChanges(); m_ui.statusBar->setVisible(checked); } @@ -1343,7 +1346,7 @@ void MainWindow::setGameListEntryCoverImage(const GameList::Entry* entry) } QString new_filename = - QString::fromStdString(GameList::GetNewCoverImagePathForEntry(entry, filename.toStdString().c_str())); + QString::fromStdString(GameList::GetNewCoverImagePathForEntry(entry, filename.toStdString().c_str(), false)); if (new_filename.isEmpty()) return; @@ -1427,6 +1430,7 @@ void MainWindow::setupAdditionalUi() action->setCheckable(true); connect(action, &QAction::triggered, [this, mode]() { Host::SetBaseStringSettingValue("CPU", "ExecutionMode", Settings::GetCPUExecutionModeName(mode)); + Host::CommitBaseSettingChanges(); g_emu_thread->applySettings(); updateDebugMenuCPUExecutionMode(); }); @@ -1441,6 +1445,7 @@ void MainWindow::setupAdditionalUi() action->setCheckable(true); connect(action, &QAction::triggered, [this, renderer]() { Host::SetBaseStringSettingValue("GPU", "Renderer", Settings::GetRendererName(renderer)); + Host::CommitBaseSettingChanges(); g_emu_thread->applySettings(); updateDebugMenuGPURenderer(); }); @@ -1455,6 +1460,7 @@ void MainWindow::setupAdditionalUi() action->setCheckable(true); connect(action, &QAction::triggered, [this, crop_mode]() { Host::SetBaseStringSettingValue("Display", "CropMode", Settings::GetDisplayCropModeName(crop_mode)); + Host::CommitBaseSettingChanges(); g_emu_thread->applySettings(); updateDebugMenuCropMode(); }); @@ -1484,6 +1490,7 @@ void MainWindow::setupAdditionalUi() connect(action, &QAction::triggered, [this, action]() { const QString new_language = action->data().toString(); Host::SetBaseStringSettingValue("Main", "Language", new_language.toUtf8().constData()); + Host::CommitBaseSettingChanges(); QtHost::InstallTranslator(); recreate(); }); @@ -1950,6 +1957,7 @@ void MainWindow::addThemeToMenu(const QString& name, const QString& key) void MainWindow::setTheme(const QString& theme) { Host::SetBaseStringSettingValue("UI", "Theme", theme.toUtf8().constData()); + Host::CommitBaseSettingChanges(); updateApplicationTheme(); updateMenuSelectedTheme(); m_game_list_widget->reloadCommonImages(); @@ -2097,7 +2105,10 @@ void MainWindow::saveGeometryToConfig() const QByteArray geometry_b64 = geometry.toBase64(); const std::string old_geometry_b64 = Host::GetBaseStringSettingValue("UI", "MainWindowGeometry"); if (old_geometry_b64 != geometry_b64.constData()) + { Host::SetBaseStringSettingValue("UI", "MainWindowGeometry", geometry_b64.constData()); + Host::CommitBaseSettingChanges(); + } } void MainWindow::restoreGeometryFromConfig() @@ -2114,7 +2125,10 @@ void MainWindow::saveDisplayWindowGeometryToConfig() const QByteArray geometry_b64 = geometry.toBase64(); const std::string old_geometry_b64 = Host::GetBaseStringSettingValue("UI", "DisplayWindowGeometry"); if (old_geometry_b64 != geometry_b64.constData()) + { Host::SetBaseStringSettingValue("UI", "DisplayWindowGeometry", geometry_b64.constData()); + Host::CommitBaseSettingChanges(); + } } void MainWindow::restoreDisplayWindowGeometryFromConfig() @@ -2443,6 +2457,7 @@ void MainWindow::onCheckForUpdatesActionTriggered() { // Wipe out the last version, that way it displays the update if we've previously skipped it. Host::DeleteBaseSettingValue("AutoUpdater", "LastVersion"); + Host::CommitBaseSettingChanges(); checkForUpdates(true); } @@ -2553,6 +2568,7 @@ void MainWindow::onToolsCheatManagerTriggered() connect(cb, &QCheckBox::stateChanged, [](int state) { Host::SetBaseBoolSettingValue("UI", "DisplayCheatWarning", (state != Qt::CheckState::Checked)); + Host::CommitBaseSettingChanges(); }); if (mb.exec() == QMessageBox::No) diff --git a/src/duckstation-qt/qthost.cpp b/src/duckstation-qt/qthost.cpp index 19a04cfa3..d3196be44 100644 --- a/src/duckstation-qt/qthost.cpp +++ b/src/duckstation-qt/qthost.cpp @@ -1078,21 +1078,6 @@ void Host::CancelGameListRefresh() QMetaObject::invokeMethod(g_main_window, "cancelGameListRefresh", Qt::BlockingQueuedConnection); } -void Host::DownloadCoversAsync(std::vector url_templates) -{ - // -} - -void Host::CancelCoversDownload() -{ - // -} - -void Host::CoversChanged() -{ - // -} - void EmuThread::loadState(const QString& filename) { if (!isOnThread()) @@ -1579,68 +1564,6 @@ std::optional Host::GetResourceFileTimestamp(const char* filename) return sd.ModificationTime; } -void Host::SetBaseBoolSettingValue(const char* section, const char* key, bool value) -{ - auto lock = Host::GetSettingsLock(); - s_base_settings_interface->SetBoolValue(section, key, value); - QtHost::QueueSettingsSave(); -} - -void Host::SetBaseIntSettingValue(const char* section, const char* key, int value) -{ - auto lock = Host::GetSettingsLock(); - s_base_settings_interface->SetIntValue(section, key, value); - QtHost::QueueSettingsSave(); -} - -void Host::SetBaseFloatSettingValue(const char* section, const char* key, float value) -{ - auto lock = Host::GetSettingsLock(); - s_base_settings_interface->SetFloatValue(section, key, value); - QtHost::QueueSettingsSave(); -} - -void Host::SetBaseStringSettingValue(const char* section, const char* key, const char* value) -{ - auto lock = Host::GetSettingsLock(); - s_base_settings_interface->SetStringValue(section, key, value); - QtHost::QueueSettingsSave(); -} - -void Host::SetBaseStringListSettingValue(const char* section, const char* key, const std::vector& values) -{ - auto lock = Host::GetSettingsLock(); - s_base_settings_interface->SetStringList(section, key, values); - QtHost::QueueSettingsSave(); -} - -bool Host::AddValueToBaseStringListSetting(const char* section, const char* key, const char* value) -{ - auto lock = Host::GetSettingsLock(); - if (!s_base_settings_interface->AddToStringList(section, key, value)) - return false; - - QtHost::QueueSettingsSave(); - return true; -} - -bool Host::RemoveValueFromBaseStringListSetting(const char* section, const char* key, const char* value) -{ - auto lock = Host::GetSettingsLock(); - if (!s_base_settings_interface->RemoveFromStringList(section, key, value)) - return false; - - QtHost::QueueSettingsSave(); - return true; -} - -void Host::DeleteBaseSettingValue(const char* section, const char* key) -{ - auto lock = Host::GetSettingsLock(); - s_base_settings_interface->DeleteValue(section, key); - QtHost::QueueSettingsSave(); -} - void Host::CommitBaseSettingChanges() { if (g_emu_thread->isOnThread()) diff --git a/src/duckstation-qt/resources/icons/black/svg/image-fill.svg b/src/duckstation-qt/resources/icons/black/svg/image-fill.svg new file mode 100644 index 000000000..25cbfc9c4 --- /dev/null +++ b/src/duckstation-qt/resources/icons/black/svg/image-fill.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/duckstation-qt/resources/icons/white/svg/image-fill.svg b/src/duckstation-qt/resources/icons/white/svg/image-fill.svg new file mode 100644 index 000000000..ca68e0ef2 --- /dev/null +++ b/src/duckstation-qt/resources/icons/white/svg/image-fill.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/duckstation-qt/resources/resources.qrc b/src/duckstation-qt/resources/resources.qrc index 1fb62e951..7fad1efe8 100644 --- a/src/duckstation-qt/resources/resources.qrc +++ b/src/duckstation-qt/resources/resources.qrc @@ -178,6 +178,7 @@ icons/black/svg/gamepad-line.svg icons/black/svg/global-line.svg icons/black/svg/hard-drive-2-line.svg + icons/black/svg/image-fill.svg icons/black/svg/keyboard-line.svg icons/black/svg/layout-grid-line.svg icons/black/svg/list-check.svg @@ -508,6 +509,7 @@ icons/white/svg/gamepad-line.svg icons/white/svg/global-line.svg icons/white/svg/hard-drive-2-line.svg + icons/white/svg/image-fill.svg icons/white/svg/keyboard-line.svg icons/white/svg/layout-grid-line.svg icons/white/svg/list-check.svg diff --git a/src/duckstation-qt/settingsdialog.cpp b/src/duckstation-qt/settingsdialog.cpp index ba4af1049..b941d1574 100644 --- a/src/duckstation-qt/settingsdialog.cpp +++ b/src/duckstation-qt/settingsdialog.cpp @@ -407,6 +407,7 @@ void SettingsDialog::setBoolSettingValue(const char* section, const char* key, s { value.has_value() ? Host::SetBaseBoolSettingValue(section, key, value.value()) : Host::DeleteBaseSettingValue(section, key); + Host::CommitBaseSettingChanges(); g_emu_thread->applySettings(); } } @@ -423,6 +424,7 @@ void SettingsDialog::setIntSettingValue(const char* section, const char* key, st { value.has_value() ? Host::SetBaseIntSettingValue(section, key, value.value()) : Host::DeleteBaseSettingValue(section, key); + Host::CommitBaseSettingChanges(); g_emu_thread->applySettings(); } } @@ -439,6 +441,7 @@ void SettingsDialog::setFloatSettingValue(const char* section, const char* key, { value.has_value() ? Host::SetBaseFloatSettingValue(section, key, value.value()) : Host::DeleteBaseSettingValue(section, key); + Host::CommitBaseSettingChanges(); g_emu_thread->applySettings(); } } @@ -455,6 +458,7 @@ void SettingsDialog::setStringSettingValue(const char* section, const char* key, { value.has_value() ? Host::SetBaseStringSettingValue(section, key, value.value()) : Host::DeleteBaseSettingValue(section, key); + Host::CommitBaseSettingChanges(); g_emu_thread->applySettings(); } } @@ -470,6 +474,7 @@ void SettingsDialog::removeSettingValue(const char* section, const char* key) else { Host::DeleteBaseSettingValue(section, key); + Host::CommitBaseSettingChanges(); g_emu_thread->applySettings(); } } diff --git a/src/duckstation-qt/settingwidgetbinder.h b/src/duckstation-qt/settingwidgetbinder.h index 18441d3ea..907bfe07d 100644 --- a/src/duckstation-qt/settingwidgetbinder.h +++ b/src/duckstation-qt/settingwidgetbinder.h @@ -651,6 +651,7 @@ static void BindWidgetToBoolSetting(SettingsInterface* sif, WidgetType* widget, Accessor::connectValueChanged(widget, [widget, section = std::move(section), key = std::move(key)]() { const bool new_value = Accessor::getBoolValue(widget); Host::SetBaseBoolSettingValue(section.c_str(), key.c_str(), new_value); + Host::CommitBaseSettingChanges(); g_emu_thread->applySettings(); }); } @@ -694,6 +695,7 @@ static void BindWidgetToIntSetting(SettingsInterface* sif, WidgetType* widget, s widget, [widget, section = std::move(section), key = std::move(key), option_offset]() { const int new_value = Accessor::getIntValue(widget); Host::SetBaseIntSettingValue(section.c_str(), key.c_str(), new_value + option_offset); + Host::CommitBaseSettingChanges(); g_emu_thread->applySettings(); }); } @@ -734,6 +736,7 @@ static void BindWidgetToFloatSetting(SettingsInterface* sif, WidgetType* widget, Accessor::connectValueChanged(widget, [widget, section = std::move(section), key = std::move(key)]() { const float new_value = Accessor::getFloatValue(widget); Host::SetBaseFloatSettingValue(section.c_str(), key.c_str(), new_value); + Host::CommitBaseSettingChanges(); g_emu_thread->applySettings(); }); } @@ -774,6 +777,7 @@ static void BindWidgetToNormalizedSetting(SettingsInterface* sif, WidgetType* wi Accessor::connectValueChanged(widget, [widget, section = std::move(section), key = std::move(key), range]() { const float new_value = (static_cast(Accessor::getIntValue(widget)) / range); Host::SetBaseFloatSettingValue(section.c_str(), key.c_str(), new_value); + Host::CommitBaseSettingChanges(); g_emu_thread->applySettings(); }); } @@ -819,6 +823,7 @@ static void BindWidgetToStringSetting(SettingsInterface* sif, WidgetType* widget else Host::DeleteBaseSettingValue(section.c_str(), key.c_str()); + Host::CommitBaseSettingChanges(); g_emu_thread->applySettings(); }); } @@ -884,6 +889,7 @@ static void BindWidgetToEnumSetting(SettingsInterface* sif, WidgetType* widget, const DataType value = static_cast(static_cast(Accessor::getIntValue(widget))); const char* string_value = to_string_function(value); Host::SetBaseStringSettingValue(section.c_str(), key.c_str(), string_value); + Host::CommitBaseSettingChanges(); g_emu_thread->applySettings(); }); } @@ -946,6 +952,7 @@ static void BindWidgetToEnumSetting(SettingsInterface* sif, WidgetType* widget, Accessor::connectValueChanged(widget, [widget, section = std::move(section), key = std::move(key), enum_names]() { const UnderlyingType value = static_cast(Accessor::getIntValue(widget)); Host::SetBaseStringSettingValue(section.c_str(), key.c_str(), enum_names[value]); + Host::CommitBaseSettingChanges(); g_emu_thread->applySettings(); }); } @@ -1010,6 +1017,7 @@ static void BindWidgetToEnumSetting(SettingsInterface* sif, WidgetType* widget, Accessor::connectValueChanged(widget, [widget, section = std::move(section), key = std::move(key), enum_values]() { const int value = Accessor::getIntValue(widget); Host::SetBaseStringSettingValue(section.c_str(), key.c_str(), enum_values[value]); + Host::CommitBaseSettingChanges(); g_emu_thread->applySettings(); }); } @@ -1054,6 +1062,7 @@ static void BindWidgetToFolderSetting(SettingsInterface* sif, WidgetType* widget Host::DeleteBaseSettingValue(section.c_str(), key.c_str()); } + Host::CommitBaseSettingChanges(); g_emu_thread->updateEmuFolders(); }); diff --git a/src/frontend-common/fullscreen_ui.cpp b/src/frontend-common/fullscreen_ui.cpp index b66d1ddd2..ce53e69f3 100644 --- a/src/frontend-common/fullscreen_ui.cpp +++ b/src/frontend-common/fullscreen_ui.cpp @@ -9,6 +9,7 @@ #include "common/path.h" #include "common/string.h" #include "common/string_util.h" +#include "common/threading.h" #include "common_host.h" #include "core/bios.h" #include "core/cheats.h" @@ -99,10 +100,13 @@ using ImGuiFullscreen::LayoutScale; using ImGuiFullscreen::LoadTexture; using ImGuiFullscreen::MenuButton; using ImGuiFullscreen::MenuButtonFrame; +using ImGuiFullscreen::MenuButtonWithoutSummary; using ImGuiFullscreen::MenuButtonWithValue; using ImGuiFullscreen::MenuHeading; using ImGuiFullscreen::MenuHeadingButton; using ImGuiFullscreen::MenuImageButton; +using ImGuiFullscreen::ModAlpha; +using ImGuiFullscreen::MulAlpha; using ImGuiFullscreen::NavButton; using ImGuiFullscreen::NavTitle; using ImGuiFullscreen::OpenChoiceDialog; @@ -148,7 +152,6 @@ enum class SettingsPage { Summary, Interface, - GameList, Console, Emulation, BIOS, @@ -162,14 +165,28 @@ enum class SettingsPage Count }; +enum class GameListPage +{ + Grid, + List, + Settings, + Count +}; + ////////////////////////////////////////////////////////////////////////// // Utility ////////////////////////////////////////////////////////////////////////// static std::string TimeToPrintableString(time_t t); +static void StartAsyncOp(std::function callback, std::string name); +static void AsyncOpThreadEntryPoint(std::function callback, + FullscreenUI::ProgressCallback* progress); +static void CancelAsyncOpWithName(const std::string_view& name); +static void CancelAsyncOps(); ////////////////////////////////////////////////////////////////////////// // Main ////////////////////////////////////////////////////////////////////////// +static void ToggleTheme(); static void PauseForMenuOpen(); static void ClosePauseMenu(); static void OpenPauseSubMenu(PauseSubMenu submenu); @@ -190,6 +207,11 @@ static bool s_pause_menu_was_open = false; static bool s_was_paused_on_quick_menu_open = false; static bool s_about_window_open = false; +// async operations (e.g. cover downloads) +using AsyncOpEntry = std::pair>; +static std::mutex s_async_op_mutex; +static std::deque s_async_ops; + ////////////////////////////////////////////////////////////////////////// // Resources ////////////////////////////////////////////////////////////////////////// @@ -240,7 +262,6 @@ static void SwitchToGameSettingsForSerial(const std::string_view& serial); static void DrawSettingsWindow(); static void DrawSummarySettingsPage(); static void DrawInterfaceSettingsPage(); -static void DrawGameListSettingsPage(); static void DrawBIOSSettingsPage(); static void DrawConsoleSettingsPage(); static void DrawEmulationSettingsPage(); @@ -321,7 +342,6 @@ static void DrawFolderSetting(SettingsInterface* bsi, const char* title, const c static void PopulateGraphicsAdapterList(); static void PopulateGameListDirectoryCache(SettingsInterface* si); -static ImGuiFullscreen::ChoiceDialogOptions GetGameListDirectoryOptions(bool recursive_as_checked); static void BeginInputBinding(SettingsInterface* bsi, Controller::ControllerBindingType type, const std::string_view& section, const std::string_view& key, const std::string_view& display_name); @@ -356,6 +376,7 @@ struct SaveStateListEntry std::string summary; std::string path; std::unique_ptr preview_texture; + time_t timestamp; s32 slot; bool global; }; @@ -371,6 +392,8 @@ static bool OpenLoadStateSelectorForGame(const std::string& game_path); static bool OpenSaveStateSelector(bool is_loading); static void CloseSaveStateSelector(); static void DrawSaveStateSelector(bool is_loading, bool fullscreen); +static bool OpenLoadStateSelectorForGameResume(const GameList::Entry* entry); +static void DrawResumeStateSelector(); static void DoLoadState(std::string path); static void DoSaveState(s32 slot, bool global); @@ -378,11 +401,18 @@ static std::vector s_save_state_selector_slots; static std::string s_save_state_selector_game_path; static bool s_save_state_selector_open = false; static bool s_save_state_selector_loading = true; +static bool s_save_state_selector_resuming = false; ////////////////////////////////////////////////////////////////////////// // Game List ////////////////////////////////////////////////////////////////////////// static void DrawGameListWindow(); +static void DrawCoverDownloaderWindow(); +static void DrawGameList(const ImVec2& heading_size); +static void DrawGameGrid(const ImVec2& heading_size); +static void HandleGameListActivate(const GameList::Entry* entry); +static void HandleGameListOptions(const GameList::Entry* entry); +static void DrawGameListSettingsPage(const ImVec2& heading_size); static void SwitchToGameList(); static void PopulateGameListEntryList(); static HostDisplayTexture* GetTextureForGameListEntryType(GameList::EntryType type); @@ -392,6 +422,7 @@ static HostDisplayTexture* GetCoverForCurrentGame(); // Lazily populated cover images. static std::unordered_map s_cover_image_map; static std::vector s_game_list_sorted_entries; +static GameListPage s_game_list_page = GameListPage::Grid; #ifdef WITH_CHEEVOS ////////////////////////////////////////////////////////////////////////// @@ -426,6 +457,77 @@ std::string FullscreenUI::TimeToPrintableString(time_t t) return std::string(buf); } +void FullscreenUI::StartAsyncOp(std::function callback, std::string name) +{ + CancelAsyncOpWithName(name); + + std::unique_lock lock(s_async_op_mutex); + std::unique_ptr progress( + std::make_unique(std::move(name))); + std::thread thread(AsyncOpThreadEntryPoint, std::move(callback), progress.get()); + s_async_ops.emplace_back(std::move(thread), std::move(progress)); +} + +void FullscreenUI::CancelAsyncOpWithName(const std::string_view& name) +{ + std::unique_lock lock(s_async_op_mutex); + for (auto iter = s_async_ops.begin(); iter != s_async_ops.end(); ++iter) + { + if (name != iter->second->GetName()) + continue; + + // move the thread out so it doesn't detach itself, then join + std::unique_ptr progress(std::move(iter->second)); + std::thread thread(std::move(iter->first)); + progress->SetCancelled(); + s_async_ops.erase(iter); + lock.unlock(); + if (thread.joinable()) + thread.join(); + lock.lock(); + break; + } +} + +void FullscreenUI::CancelAsyncOps() +{ + std::unique_lock lock(s_async_op_mutex); + while (!s_async_ops.empty()) + { + auto iter = s_async_ops.begin(); + + // move the thread out so it doesn't detach itself, then join + std::unique_ptr progress(std::move(iter->second)); + std::thread thread(std::move(iter->first)); + progress->SetCancelled(); + s_async_ops.erase(iter); + lock.unlock(); + if (thread.joinable()) + thread.join(); + lock.lock(); + } +} + +void FullscreenUI::AsyncOpThreadEntryPoint(std::function callback, + FullscreenUI::ProgressCallback* progress) +{ + Threading::SetNameOfCurrentThread(fmt::format("{} Async Op", progress->GetName()).c_str()); + + callback(progress); + + // if we were removed from the list, it means we got cancelled, and the main thread is blocking + std::unique_lock lock(s_async_op_mutex); + for (auto iter = s_async_ops.begin(); iter != s_async_ops.end(); ++iter) + { + if (iter->second.get() == progress) + { + iter->first.detach(); + s_async_ops.erase(iter); + break; + } + } +} + ////////////////////////////////////////////////////////////////////////// // Main ////////////////////////////////////////////////////////////////////////// @@ -438,7 +540,7 @@ bool FullscreenUI::Initialize() if (s_tried_to_initialize) return false; - ImGuiFullscreen::SetTheme(); + ImGuiFullscreen::SetTheme(Host::GetBaseBoolSettingValue("Main", "UseLightFullscreenUITheme", false)); ImGuiFullscreen::UpdateLayoutScale(); if (!ImGuiManager::AddFullscreenFontsIfMissing() || !ImGuiFullscreen::Initialize("images/placeholder.png") || @@ -519,6 +621,14 @@ void FullscreenUI::OnRunningGameChanged() s_current_game_subtitle = {}; } +void FullscreenUI::ToggleTheme() +{ + const bool new_light = !Host::GetBaseBoolSettingValue("Main", "UseLightFullscreenUITheme", false); + Host::SetBaseBoolSettingValue("Main", "UseLightFullscreenUITheme", new_light); + Host::CommitBaseSettingChanges(); + ImGuiFullscreen::SetTheme(new_light); +} + void FullscreenUI::PauseForMenuOpen() { s_was_paused_on_quick_menu_open = (System::GetState() == System::State::Paused); @@ -573,6 +683,7 @@ void FullscreenUI::OpenPauseSubMenu(PauseSubMenu submenu) void FullscreenUI::Shutdown() { + CancelAsyncOps(); CloseSaveStateSelector(); s_cover_image_map.clear(); s_game_list_sorted_entries = {}; @@ -626,7 +737,12 @@ void FullscreenUI::Render() } if (s_save_state_selector_open) - DrawSaveStateSelector(s_save_state_selector_loading, false); + { + if (s_save_state_selector_resuming) + DrawResumeStateSelector(); + else + DrawSaveStateSelector(s_save_state_selector_loading, false); + } if (s_about_window_open) DrawAboutWindow(); @@ -678,7 +794,7 @@ bool FullscreenUI::LoadResources() s_fallback_disc_texture = LoadTexture("fullscreenui/media-cdrom.png"); s_fallback_exe_texture = LoadTexture("fullscreenui/applications-system.png"); - s_fallback_exe_texture = LoadTexture("fullscreenui/multimedia-player.png"); + s_fallback_psf_texture = LoadTexture("fullscreenui/multimedia-player.png"); s_fallback_playlist_texture = LoadTexture("fullscreenui/address-book-new.png"); for (u32 i = 0; i < static_cast(GameDatabase::CompatibilityRating::Count); i++) @@ -934,9 +1050,9 @@ void FullscreenUI::SwitchToLanding() void FullscreenUI::DrawLandingWindow() { - BeginFullscreenColumns(); + BeginFullscreenColumns(nullptr, 0.0f, true); - if (BeginFullscreenColumnWindow(0.0f, 570.0f, "logo", UIPrimaryDarkColor)) + if (BeginFullscreenColumnWindow(0.0f, -710.0f, "logo", UIPrimaryDarkColor)) { const float image_size = LayoutScale(380.f); ImGui::SetCursorPos(ImVec2((ImGui::GetWindowWidth() * 0.5f) - (image_size * 0.5f), @@ -945,12 +1061,17 @@ void FullscreenUI::DrawLandingWindow() } EndFullscreenColumnWindow(); - if (BeginFullscreenColumnWindow(570.0f, LAYOUT_SCREEN_WIDTH, "menu", UIBackgroundColor)) + if (BeginFullscreenColumnWindow(-710.0f, 0.0f, "menu", UIBackgroundColor)) { ResetFocusHere(); BeginMenuButtons(7, 0.5f); + if (MenuButton(ICON_FA_LIST " Game List", "Launch a game from images scanned from your game directories.")) + { + SwitchToGameList(); + } + if (MenuButton(ICON_FA_PLAY_CIRCLE " Resume", "Starts the console from where it was before it was last closed.")) { System::GetMostRecentResumeSaveStatePath(); @@ -972,11 +1093,6 @@ void FullscreenUI::DrawLandingWindow() OpenSaveStateSelector(true); } - if (MenuButton(ICON_FA_LIST " Open Game List", "Launch a game from images scanned from your game directories.")) - { - SwitchToGameList(); - } - if (MenuButton(ICON_FA_SLIDERS_H " Settings", "Change settings for the emulator.")) SwitchToSettings(); @@ -999,17 +1115,26 @@ void FullscreenUI::DrawLandingWindow() DoToggleFullscreen(); } - if (FloatingButton(ICON_FA_QUESTION_CIRCLE, fullscreen_pos.x, 0.0f, -1.0f, -1.0f, -1.0f, 0.0f)) + if (FloatingButton(ICON_FA_QUESTION_CIRCLE, fullscreen_pos.x, 0.0f, -1.0f, -1.0f, -1.0f, 0.0f, true, g_large_font, + &fullscreen_pos)) + { OpenAboutWindow(); + } + + if (FloatingButton(ICON_FA_LIGHTBULB, fullscreen_pos.x, 0.0f, -1.0f, -1.0f, -1.0f, 0.0f, true, g_large_font, + &fullscreen_pos)) + { + ToggleTheme(); + } } EndMenuButtons(); - const ImVec2 text_size(g_medium_font->CalcTextSizeA(g_medium_font->FontSize, FLT_MAX, 0.0f, g_scm_tag_str)); - ImGui::SetCursorPos(ImVec2(ImGui::GetWindowWidth() - text_size.x - LayoutScale(15.0f), - ImGui::GetWindowHeight() - text_size.y - LayoutScale(15.0f))); + const ImVec2 rev_size(g_medium_font->CalcTextSizeA(g_medium_font->FontSize, FLT_MAX, 0.0f, g_scm_tag_str)); + ImGui::SetCursorPos(ImVec2(ImGui::GetWindowWidth() - rev_size.x - LayoutScale(20.0f), + ImGui::GetWindowHeight() - rev_size.y - LayoutScale(20.0f))); ImGui::PushFont(g_medium_font); - ImGui::TextUnformatted(g_scm_tag_str); + ImGui::Text(g_scm_tag_str); ImGui::PopFont(); } @@ -1313,6 +1438,13 @@ void FullscreenUI::DrawIntListSetting(SettingsInterface* bsi, const char* title, float height, ImFont* font, ImFont* summary_font) { const bool game_settings = IsEditingGameSettings(bsi); + + if (options && option_count == 0) + { + while (options[option_count] != nullptr) + option_count++; + } + const std::optional value = bsi->GetOptionalIntValue(section, key, game_settings ? std::nullopt : std::optional(default_value)); const int index = value.has_value() ? (value.value() - option_offset) : std::numeric_limits::min(); @@ -1368,7 +1500,8 @@ void FullscreenUI::DrawIntRangeSetting(SettingsInterface* bsi, const char* title if (MenuButtonWithValue(title, summary, value_text.c_str(), enabled, height, font, summary_font)) ImGui::OpenPopup(title); - ImGui::SetNextWindowSize(LayoutScale(500.0f, 180.0f)); + 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)); @@ -1376,10 +1509,14 @@ void FullscreenUI::DrawIntRangeSetting(SettingsInterface* bsi, const char* title ImGuiFullscreen::LAYOUT_MENU_BUTTON_Y_PADDING)); ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, LayoutScale(20.0f, 20.0f)); - if (ImGui::BeginPopupModal(title, nullptr, + bool is_open = true; + if (ImGui::BeginPopupModal(title, &is_open, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove)) { - ImGui::SetNextItemWidth(LayoutScale(450.0f)); + BeginMenuButtons(); + + const float end = ImGui::GetCurrentWindow()->WorkRect.GetWidth(); + ImGui::SetNextItemWidth(end); s32 dlg_value = static_cast(value.value_or(default_value)); if (ImGui::SliderInt("##value", &dlg_value, min_value, max_value, format, ImGuiSliderFlags_NoInput)) { @@ -1391,9 +1528,11 @@ void FullscreenUI::DrawIntRangeSetting(SettingsInterface* bsi, const char* title SetSettingsChanged(bsi); } - BeginMenuButtons(); - if (MenuButton("OK", nullptr, true, LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY)) + ImGui::SetCursorPosY(ImGui::GetCursorPosY() + LayoutScale(10.0f)); + if (MenuButtonWithoutSummary("OK", true, LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY, g_large_font, ImVec2(0.5f, 0.0f))) + { ImGui::CloseCurrentPopup(); + } EndMenuButtons(); ImGui::EndPopup(); @@ -1417,7 +1556,8 @@ void FullscreenUI::DrawFloatRangeSetting(SettingsInterface* bsi, const char* tit if (MenuButtonWithValue(title, summary, value_text.c_str(), enabled, height, font, summary_font)) ImGui::OpenPopup(title); - ImGui::SetNextWindowSize(LayoutScale(500.0f, 180.0f)); + 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)); @@ -1425,10 +1565,14 @@ void FullscreenUI::DrawFloatRangeSetting(SettingsInterface* bsi, const char* tit ImGuiFullscreen::LAYOUT_MENU_BUTTON_Y_PADDING)); ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, LayoutScale(20.0f, 20.0f)); - if (ImGui::BeginPopupModal(title, nullptr, + bool is_open = true; + if (ImGui::BeginPopupModal(title, &is_open, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove)) { - ImGui::SetNextItemWidth(LayoutScale(450.0f)); + BeginMenuButtons(); + + const float end = ImGui::GetCurrentWindow()->WorkRect.GetWidth(); + ImGui::SetNextItemWidth(end); float dlg_value = value.value_or(default_value) * multiplier; if (ImGui::SliderFloat("##value", &dlg_value, min_value, max_value, format, ImGuiSliderFlags_NoInput)) { @@ -1442,9 +1586,11 @@ void FullscreenUI::DrawFloatRangeSetting(SettingsInterface* bsi, const char* tit SetSettingsChanged(bsi); } - BeginMenuButtons(); - if (MenuButton("OK", nullptr, true, LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY)) + ImGui::SetCursorPosY(ImGui::GetCursorPosY() + LayoutScale(10.0f)); + if (MenuButtonWithoutSummary("OK", true, LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY, g_large_font, ImVec2(0.5f, 0.0f))) + { ImGui::CloseCurrentPopup(); + } EndMenuButtons(); ImGui::EndPopup(); @@ -1479,7 +1625,8 @@ void FullscreenUI::DrawIntRectSetting(SettingsInterface* bsi, const char* title, if (MenuButtonWithValue(title, summary, value_text.c_str(), enabled, height, font, summary_font)) ImGui::OpenPopup(title); - ImGui::SetNextWindowSize(LayoutScale(500.0f, 320.0f)); + ImGui::SetNextWindowSize(LayoutScale(500.0f, 370.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)); @@ -1487,7 +1634,8 @@ void FullscreenUI::DrawIntRectSetting(SettingsInterface* bsi, const char* title, ImGuiFullscreen::LAYOUT_MENU_BUTTON_Y_PADDING)); ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, LayoutScale(20.0f, 20.0f)); - if (ImGui::BeginPopupModal(title, nullptr, + bool is_open = true; + if (ImGui::BeginPopupModal(title, &is_open, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove)) { s32 dlg_left_value = static_cast(left_value.value_or(default_left)); @@ -1495,22 +1643,34 @@ void FullscreenUI::DrawIntRectSetting(SettingsInterface* bsi, const char* title, s32 dlg_right_value = static_cast(right_value.value_or(default_right)); s32 dlg_bottom_value = static_cast(bottom_value.value_or(default_bottom)); + BeginMenuButtons(); + + const float midpoint = LayoutScale(150.0f); + const float end = (ImGui::GetCurrentWindow()->WorkRect.GetWidth() - midpoint) + ImGui::GetStyle().WindowPadding.x; ImGui::TextUnformatted("Left: "); - ImGui::SameLine(); + ImGui::SameLine(midpoint); + ImGui::SetNextItemWidth(end); const bool left_modified = ImGui::SliderInt("##left", &dlg_left_value, min_value, max_value, format, ImGuiSliderFlags_NoInput); + ImGui::SetCursorPosY(ImGui::GetCursorPosY() + LayoutScale(10.0f)); ImGui::TextUnformatted("Top: "); - ImGui::SameLine(); + ImGui::SameLine(midpoint); + ImGui::SetNextItemWidth(end); const bool top_modified = ImGui::SliderInt("##top", &dlg_top_value, min_value, max_value, format, ImGuiSliderFlags_NoInput); + ImGui::SetCursorPosY(ImGui::GetCursorPosY() + LayoutScale(10.0f)); ImGui::TextUnformatted("Right: "); - ImGui::SameLine(); + ImGui::SameLine(midpoint); + ImGui::SetNextItemWidth(end); const bool right_modified = ImGui::SliderInt("##right", &dlg_right_value, min_value, max_value, format, ImGuiSliderFlags_NoInput); + ImGui::SetCursorPosY(ImGui::GetCursorPosY() + LayoutScale(10.0f)); ImGui::TextUnformatted("Bottom: "); - ImGui::SameLine(); + ImGui::SameLine(midpoint); + ImGui::SetNextItemWidth(end); const bool bottom_modified = ImGui::SliderInt("##bottom", &dlg_bottom_value, min_value, max_value, format, ImGuiSliderFlags_NoInput); + ImGui::SetCursorPosY(ImGui::GetCursorPosY() + LayoutScale(10.0f)); if (left_modified) { if (IsEditingGameSettings(bsi) && dlg_left_value == default_left) @@ -1543,9 +1703,10 @@ void FullscreenUI::DrawIntRectSetting(SettingsInterface* bsi, const char* title, if (left_modified || top_modified || right_modified || bottom_modified) SetSettingsChanged(bsi); - BeginMenuButtons(); - if (MenuButton("OK", nullptr, true, LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY)) + if (MenuButtonWithoutSummary("OK", true, LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY, g_large_font, ImVec2(0.5f, 0.0f))) + { ImGui::CloseCurrentPopup(); + } EndMenuButtons(); ImGui::EndPopup(); @@ -1793,8 +1954,10 @@ void FullscreenUI::StartAutomaticBinding(u32 port) const std::string& name = names[index]; auto lock = Host::GetSettingsLock(); - const bool result = InputManager::MapController(*GetEditingSettingsInterface(), port, - InputManager::GetGenericBindingMapping(name)); + SettingsInterface* bsi = GetEditingSettingsInterface(); + const bool result = + InputManager::MapController(*bsi, port, InputManager::GetGenericBindingMapping(name)); + SetSettingsChanged(bsi); // and the toast needs to happen on the UI thread. ShowToast({}, result ? fmt::format("Automatic mapping completed for {}.", name) : @@ -1808,11 +1971,6 @@ void FullscreenUI::SwitchToSettings() s_game_settings_entry.reset(); s_game_settings_interface.reset(); - // populate the cache with all settings from ini - auto lock = Host::GetSettingsLock(); - SettingsInterface* bsi = Host::Internal::GetBaseSettingsLayer(); - - PopulateGameListDirectoryCache(bsi); PopulateGraphicsAdapterList(); s_current_main_window = MainWindowType::Settings; @@ -1876,14 +2034,6 @@ void FullscreenUI::PopulateGameListDirectoryCache(SettingsInterface* si) s_game_list_directories_cache.emplace_back(std::move(dir), true); } -ImGuiFullscreen::ChoiceDialogOptions FullscreenUI::GetGameListDirectoryOptions(bool recursive_as_checked) -{ - ImGuiFullscreen::ChoiceDialogOptions options; - for (const auto& it : s_game_list_directories_cache) - options.emplace_back(it.first, it.second && recursive_as_checked); - return options; -} - void FullscreenUI::DoCopyGameSettings() { if (!s_game_settings_interface) @@ -1921,28 +2071,29 @@ void FullscreenUI::DrawSettingsWindow() const float bg_alpha = System::IsValid() ? 0.90f : 1.0f; - if (BeginFullscreenWindow(ImVec2(0.0f, 0.0f), heading_size, "settings_category", UIPrimaryColor)) + if (BeginFullscreenWindow(ImVec2(0.0f, 0.0f), heading_size, "settings_category", + ImVec4(UIPrimaryColor.x, UIPrimaryColor.y, UIPrimaryColor.z, bg_alpha))) { static constexpr float ITEM_WIDTH = 25.0f; static constexpr const char* global_icons[] = { - ICON_FA_WINDOW_MAXIMIZE, ICON_FA_LIST, ICON_FA_HDD, ICON_FA_SLIDERS_H, - ICON_FA_MICROCHIP, ICON_FA_MAGIC, ICON_FA_HEADPHONES, ICON_FA_GAMEPAD, - ICON_FA_KEYBOARD, ICON_FA_SD_CARD, ICON_FA_TROPHY, ICON_FA_EXCLAMATION_TRIANGLE}; + ICON_FA_WINDOW_MAXIMIZE, ICON_FA_HDD, ICON_FA_SLIDERS_H, ICON_FA_MICROCHIP, ICON_FA_MAGIC, + ICON_FA_HEADPHONES, ICON_FA_GAMEPAD, ICON_FA_KEYBOARD, ICON_FA_SD_CARD, ICON_FA_TROPHY, + ICON_FA_EXCLAMATION_TRIANGLE}; static constexpr const char* per_game_icons[] = { ICON_FA_PARAGRAPH, ICON_FA_HDD, ICON_FA_SLIDERS_H, ICON_FA_MAGIC, ICON_FA_HEADPHONES, ICON_FA_GAMEPAD, ICON_FA_SD_CARD, ICON_FA_TROPHY, ICON_FA_EXCLAMATION_TRIANGLE}; static constexpr SettingsPage global_pages[] = { - SettingsPage::Interface, SettingsPage::GameList, SettingsPage::Console, SettingsPage::Emulation, - SettingsPage::BIOS, SettingsPage::Display, SettingsPage::Audio, SettingsPage::Controller, - SettingsPage::Hotkey, SettingsPage::MemoryCards, SettingsPage::Achievements, SettingsPage::Advanced}; + SettingsPage::Interface, SettingsPage::Console, SettingsPage::Emulation, SettingsPage::BIOS, + SettingsPage::Display, SettingsPage::Audio, SettingsPage::Controller, SettingsPage::Hotkey, + SettingsPage::MemoryCards, SettingsPage::Achievements, SettingsPage::Advanced}; static constexpr SettingsPage per_game_pages[] = { SettingsPage::Summary, SettingsPage::Console, SettingsPage::Emulation, SettingsPage::Display, SettingsPage::Audio, SettingsPage::Controller, SettingsPage::MemoryCards, SettingsPage::Achievements, SettingsPage::Advanced}; static constexpr std::array(SettingsPage::Count)> titles = { - {"Summary", "Interface Settings", "Game List Settings", "Console Settings", "Emulation Settings", "BIOS Settings", + {"Summary", "Interface Settings", "Console Settings", "Emulation Settings", "BIOS Settings", "Controller Settings", "Hotkey Settings", "Memory Card Settings", "Display Settings", "Audio Settings", "Achievements Settings", "Advanced Settings"}}; @@ -1963,15 +2114,18 @@ void FullscreenUI::DrawSettingsWindow() BeginNavBar(); - if (ImGui::IsNavInputTest(ImGuiNavInput_FocusPrev, ImGuiNavReadMode_Pressed)) + if (!ImGui::IsPopupOpen(0u, ImGuiPopupFlags_AnyPopup)) { - index = (index == 0) ? (count - 1) : (index - 1); - s_settings_page = pages[index]; - } - else if (ImGui::IsNavInputTest(ImGuiNavInput_FocusNext, ImGuiNavReadMode_Pressed)) - { - index = (index + 1) % count; - s_settings_page = pages[index]; + if (ImGui::IsNavInputTest(ImGuiNavInput_FocusPrev, ImGuiNavReadMode_Pressed)) + { + index = (index == 0) ? (count - 1) : (index - 1); + s_settings_page = pages[index]; + } + else if (ImGui::IsNavInputTest(ImGuiNavInput_FocusNext, ImGuiNavReadMode_Pressed)) + { + index = (index + 1) % count; + s_settings_page = pages[index]; + } } if (NavButton(ICON_FA_BACKWARD, true, true)) @@ -2021,10 +2175,6 @@ void FullscreenUI::DrawSettingsWindow() DrawInterfaceSettingsPage(); break; - case SettingsPage::GameList: - DrawGameListSettingsPage(); - break; - case SettingsPage::BIOS: DrawBIOSSettingsPage(); break; @@ -2166,6 +2316,12 @@ void FullscreenUI::DrawInterfaceSettingsPage() "Main", "ApplyGameSettings", true); DrawToggleSetting(bsi, ICON_FA_FROWN " Automatically Load Cheats", "Automatically loads and applies cheats on game start.", "Main", "AutoLoadCheats", true); + if (DrawToggleSetting(bsi, ICON_FA_PAINT_BRUSH " Use Light Theme", + "Uses a light coloured theme instead of the default dark theme.", "Main", + "UseLightFullscreenUITheme", false)) + { + ImGuiFullscreen::SetTheme(bsi->GetBoolValue("Main", "UseLightFullscreenUITheme", false)); + } #ifdef WITH_DISCORD_PRESENCE MenuHeading("Integration"); @@ -2202,88 +2358,6 @@ void FullscreenUI::DrawInterfaceSettingsPage() EndMenuButtons(); } -void FullscreenUI::DrawGameListSettingsPage() -{ - BeginMenuButtons(); - - MenuHeading("Game List"); - - if (MenuButton(ICON_FA_FOLDER_PLUS " Add Search Directory", "Adds a new directory to the game search list.")) - { - OpenFileSelector(ICON_FA_FOLDER_PLUS " Add Search Directory", true, [](const std::string& dir) { - if (!dir.empty()) - { - auto lock = Host::GetSettingsLock(); - SettingsInterface* bsi = Host::Internal::GetBaseSettingsLayer(); - - bsi->AddToStringList("GameList", "RecursivePaths", dir.c_str()); - bsi->RemoveFromStringList("GameList", "Paths", dir.c_str()); - bsi->Save(); - PopulateGameListDirectoryCache(bsi); - Host::RefreshGameListAsync(false); - } - - CloseFileSelector(); - }); - } - - if (MenuButton(ICON_FA_FOLDER_OPEN " Change Recursive Directories", - "Sets whether subdirectories are searched for each game directory")) - { - OpenChoiceDialog(ICON_FA_FOLDER_OPEN " Change Recursive Directories", true, GetGameListDirectoryOptions(true), - [](s32 index, const std::string& title, bool checked) { - if (index < 0) - return; - - auto lock = Host::GetSettingsLock(); - SettingsInterface* bsi = Host::Internal::GetBaseSettingsLayer(); - if (checked) - { - bsi->RemoveFromStringList("GameList", "Paths", title.c_str()); - bsi->AddToStringList("GameList", "RecursivePaths", title.c_str()); - } - else - { - bsi->RemoveFromStringList("GameList", "RecursivePaths", title.c_str()); - bsi->AddToStringList("GameList", "Paths", title.c_str()); - } - - bsi->Save(); - PopulateGameListDirectoryCache(bsi); - Host::RefreshGameListAsync(false); - }); - } - - if (MenuButton(ICON_FA_FOLDER_MINUS " Remove Search Directory", "Removes a directory from the game search list.")) - { - OpenChoiceDialog(ICON_FA_FOLDER_MINUS " Remove Search Directory", false, GetGameListDirectoryOptions(false), - [](s32 index, const std::string& title, bool checked) { - if (index < 0) - return; - - auto lock = Host::GetSettingsLock(); - SettingsInterface* bsi = Host::Internal::GetBaseSettingsLayer(); - bsi->RemoveFromStringList("GameList", "Paths", title.c_str()); - bsi->RemoveFromStringList("GameList", "RecursivePaths", title.c_str()); - bsi->Save(); - PopulateGameListDirectoryCache(bsi); - Host::RefreshGameListAsync(false); - CloseChoiceDialog(); - }); - } - - 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_PLUS " Rescan All Games", "Forces a full rescan of all games previously identified.")) - Host::RefreshGameListAsync(true); - - MenuHeading("Search Directories"); - for (const auto& it : s_game_list_directories_cache) - MenuButton(it.first.c_str(), it.second ? "Scanning Subdirectories" : "Not Scanning Subdirectories", false); - - EndMenuButtons(); -} - void FullscreenUI::DrawBIOSSettingsPage() { static constexpr auto config_keys = make_array("", "PathNTSCJ", "PathNTSCU", "PathPAL"); @@ -3737,7 +3811,7 @@ void FullscreenUI::DrawPauseMenu(MainWindowType type) SmallString subtitle; if (!code.empty()) subtitle.Format("%s - ", code.c_str()); - subtitle.AppendString(Path::GetFileTitle(System::GetRunningPath())); + subtitle.AppendString(Path::GetFileName(System::GetRunningPath())); const ImVec2 title_size( g_large_font->CalcTextSizeA(g_large_font->FontSize, std::numeric_limits::max(), -1.0f, title.c_str())); @@ -3943,6 +4017,7 @@ void FullscreenUI::InitializePlaceholderSaveStateListEntry(SaveStateListEntry* l li->title = fmt::format("{0} {1} Slot {2}##{1}_slot_{2}", title, global ? "Global" : "Game", slot); li->summary = "No Save State"; li->path = {}; + li->timestamp = 0; li->slot = slot; li->preview_texture = {}; } @@ -3969,6 +4044,7 @@ bool FullscreenUI::InitializeSaveStateListEntry(SaveStateListEntry* li, const st } li->summary = fmt::format("{} - Saved {:%c}", ssi->game_code.c_str(), fmt::localtime(ssi->timestamp)); + li->timestamp = ssi->timestamp; li->slot = slot; li->path = std::move(filename); @@ -4053,6 +4129,7 @@ bool FullscreenUI::OpenLoadStateSelectorForGame(const std::string& game_path) if (PopulateSaveStateListEntries(entry->title.c_str(), entry->serial.c_str()) > 0) { s_save_state_selector_open = true; + s_save_state_selector_resuming = false; s_save_state_selector_game_path = game_path; return true; } @@ -4066,6 +4143,7 @@ bool FullscreenUI::OpenSaveStateSelector(bool is_loading) { s_save_state_selector_game_path = {}; s_save_state_selector_loading = is_loading; + s_save_state_selector_resuming = false; if (PopulateSaveStateListEntries(System::GetRunningTitle().c_str(), System::GetRunningCode().c_str()) > 0) { s_save_state_selector_open = true; @@ -4081,8 +4159,10 @@ void FullscreenUI::CloseSaveStateSelector() ClearSaveStateEntryList(); s_save_state_selector_open = false; s_save_state_selector_loading = false; + s_save_state_selector_resuming = false; s_save_state_selector_game_path = {}; - ReturnToMainWindow(); + if (s_current_main_window != MainWindowType::GameList) + ReturnToMainWindow(); } void FullscreenUI::DrawSaveStateSelector(bool is_loading, bool fullscreen) @@ -4128,10 +4208,10 @@ void FullscreenUI::DrawSaveStateSelector(bool is_loading, bool fullscreen) BeginMenuButtons(); - constexpr float padding = 10.0f; - constexpr float button_height = 96.0f; - constexpr float max_image_width = 96.0f; - constexpr float max_image_height = 96.0f; + static constexpr float padding = 10.0f; + 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) { @@ -4208,6 +4288,106 @@ void FullscreenUI::DrawSaveStateSelector(bool is_loading, bool fullscreen) } } +bool FullscreenUI::OpenLoadStateSelectorForGameResume(const GameList::Entry* entry) +{ + SaveStateListEntry slentry; + if (!InitializeSaveStateListEntry(&slentry, entry->title, entry->serial, -1, false)) + return false; + + CloseSaveStateSelector(); + s_save_state_selector_slots.push_back(std::move(slentry)); + s_save_state_selector_game_path = entry->path; + s_save_state_selector_loading = true; + s_save_state_selector_open = true; + s_save_state_selector_resuming = true; + return true; +} + +void FullscreenUI::DrawResumeStateSelector() +{ + ImGui::SetNextWindowSize(LayoutScale(800.0f, 600.0f)); + ImGui::SetNextWindowPos(ImGui::GetIO().DisplaySize * 0.5f, ImGuiCond_Always, ImVec2(0.5f, 0.5f)); + ImGui::OpenPopup("Load Resume State"); + + ImGui::PushFont(g_large_font); + ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, LayoutScale(10.0f)); + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, LayoutScale(20.0f, 20.0f)); + + bool is_open = true; + if (ImGui::BeginPopupModal("Load Resume State", &is_open, ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize)) + { + static constexpr float max_image_width = 96.0f; + static constexpr float max_image_height = 96.0f; + + const SaveStateListEntry& entry = s_save_state_selector_slots.front(); + ImGui::TextWrapped("A resume save state created at %s was found.\n\nDo you want to load this save and continue?", + TimeToPrintableString(entry.timestamp).c_str()); + + const HostDisplayTexture* image = + entry.preview_texture ? entry.preview_texture.get() : GetPlaceholderTexture().get(); + const float image_height = LayoutScale(250.0f); + const float image_width = + image_height * (static_cast(image->GetWidth()) / static_cast(image->GetHeight())); + const ImVec2 pos(ImGui::GetCursorScreenPos() + + ImVec2((ImGui::GetCurrentWindow()->WorkRect.GetWidth() - image_width) * 0.5f, LayoutScale(20.0f))); + const ImRect image_bb(pos, pos + ImVec2(image_width, image_height)); + ImGui::GetWindowDrawList()->AddImage(static_cast(entry.preview_texture ? + entry.preview_texture->GetHandle() : + GetPlaceholderTexture()->GetHandle()), + image_bb.Min, image_bb.Max); + + ImGui::SetCursorPosY(ImGui::GetCursorPosY() + image_height + LayoutScale(40.0f)); + + BeginMenuButtons(); + + if (ActiveButton(ICON_FA_PLAY " Load State", false)) + { + DoStartPath(s_save_state_selector_game_path, std::move(entry.path)); + is_open = false; + } + + if (ActiveButton(ICON_FA_LIGHTBULB " Clean Boot", false)) + { + DoStartPath(s_save_state_selector_game_path); + is_open = false; + } + + if (ActiveButton(ICON_FA_FOLDER_MINUS " Delete State", false)) + { + if (FileSystem::DeleteFile(entry.path.c_str())) + { + DoStartPath(s_save_state_selector_game_path); + is_open = false; + } + else + { + ShowToast(std::string(), "Failed to delete save state."); + } + } + + if (ActiveButton(ICON_FA_WINDOW_CLOSE " Cancel", false)) + { + ImGui::CloseCurrentPopup(); + is_open = false; + } + EndMenuButtons(); + + ImGui::EndPopup(); + } + + ImGui::PopStyleVar(2); + ImGui::PopFont(); + + if (!is_open) + { + ClearSaveStateEntryList(); + s_save_state_selector_open = false; + s_save_state_selector_loading = false; + s_save_state_selector_resuming = false; + s_save_state_selector_game_path = {}; + } +} + void FullscreenUI::DoLoadState(std::string path) { Host::RunOnCPUThread([boot_path = s_save_state_selector_game_path, path = std::move(path)]() { @@ -4249,12 +4429,83 @@ void FullscreenUI::PopulateGameListEntryList() // TODO: Custom sort types std::sort(s_game_list_sorted_entries.begin(), s_game_list_sorted_entries.end(), - [](const GameList::Entry* lhs, const GameList::Entry* rhs) { return lhs->title < rhs->title; }); + [](const GameList::Entry* lhs, const GameList::Entry* rhs) { + return StringUtil::Strcasecmp(lhs->title.c_str(), rhs->title.c_str()) < 0; + }); } void FullscreenUI::DrawGameListWindow() { - if (!BeginFullscreenColumns()) + auto game_list_lock = GameList::GetLock(); + PopulateGameListEntryList(); + + ImGuiIO& io = ImGui::GetIO(); + ImVec2 heading_size = ImVec2( + io.DisplaySize.x, LayoutScale(LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY + LAYOUT_MENU_BUTTON_Y_PADDING * 2.0f + 2.0f)); + + const float bg_alpha = System::IsValid() ? 0.90f : 1.0f; + + if (BeginFullscreenWindow(ImVec2(0.0f, 0.0f), heading_size, "gamelist_view", MulAlpha(UIPrimaryColor, bg_alpha))) + { + static constexpr float ITEM_WIDTH = 25.0f; + static constexpr const char* icons[] = {ICON_FA_BORDER_ALL, ICON_FA_LIST, ICON_FA_COG}; + static constexpr const char* titles[] = {"Game Grid", "Game List", "Game List Settings"}; + static constexpr u32 count = static_cast(std::size(titles)); + + BeginNavBar(); + + if (!ImGui::IsPopupOpen(0u, ImGuiPopupFlags_AnyPopup)) + { + if (ImGui::IsNavInputTest(ImGuiNavInput_FocusPrev, ImGuiNavReadMode_Pressed)) + { + s_game_list_page = static_cast( + (s_game_list_page == static_cast(0)) ? (count - 1) : (static_cast(s_game_list_page) - 1)); + } + else if (ImGui::IsNavInputTest(ImGuiNavInput_FocusNext, ImGuiNavReadMode_Pressed)) + { + s_game_list_page = static_cast((static_cast(s_game_list_page) + 1) % count); + } + } + + if (NavButton(ICON_FA_BACKWARD, true, true)) + ReturnToMainWindow(); + + NavTitle(titles[static_cast(s_game_list_page)]); + RightAlignNavButtons(count, ITEM_WIDTH, LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY); + + for (u32 i = 0; i < count; i++) + { + if (NavButton(icons[i], static_cast(i) == s_game_list_page, true, ITEM_WIDTH, + LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY)) + { + s_game_list_page = static_cast(i); + } + } + + EndNavBar(); + } + + EndFullscreenWindow(); + + switch (s_game_list_page) + { + case GameListPage::Grid: + DrawGameGrid(heading_size); + break; + case GameListPage::List: + DrawGameList(heading_size); + break; + case GameListPage::Settings: + DrawGameListSettingsPage(heading_size); + break; + default: + break; + } +} + +void FullscreenUI::DrawGameList(const ImVec2& heading_size) +{ + if (!BeginFullscreenColumns(nullptr, heading_size.y, true)) { EndFullscreenColumns(); return; @@ -4264,15 +4515,14 @@ void FullscreenUI::DrawGameListWindow() const GameList::Entry* selected_entry = nullptr; PopulateGameListEntryList(); - if (BeginFullscreenColumnWindow(0.0f, 750.0f, "game_list_entries")) + if (BeginFullscreenColumnWindow(0.0f, -530.0f, "game_list_entries")) { - const ImVec2 image_size(LayoutScale(LAYOUT_MENU_BUTTON_HEIGHT * 0.68f, LAYOUT_MENU_BUTTON_HEIGHT)); + const ImVec2 image_size(LayoutScale(LAYOUT_MENU_BUTTON_HEIGHT, LAYOUT_MENU_BUTTON_HEIGHT)); ResetFocusHere(); BeginMenuButtons(); - // TODO: replace with something not heap alllocating SmallString summary; for (const GameList::Entry* entry : s_game_list_sorted_entries) @@ -4325,10 +4575,7 @@ void FullscreenUI::DrawGameListWindow() } if (pressed) - { - // launch game - DoStartPath(entry->path); - } + HandleGameListActivate(entry); if (hovered) selected_entry = entry; @@ -4336,41 +4583,7 @@ void FullscreenUI::DrawGameListWindow() if (selected_entry && (ImGui::IsItemClicked(ImGuiMouseButton_Right) || ImGui::IsNavInputTest(ImGuiNavInput_Input, ImGuiNavReadMode_Pressed))) { - ImGuiFullscreen::ChoiceDialogOptions options = { - {"Open Game Properties", false}, {"Resume Game", false}, {"Load State", false}, - {"Default Boot", false}, {"Fast Boot", false}, {"Slow Boot", false}, - {"Close Menu", false}, - }; - - OpenChoiceDialog(selected_entry->title.c_str(), false, std::move(options), - [entry_path = selected_entry->path, - entry_serial = selected_entry->serial](s32 index, const std::string& title, bool checked) { - switch (index) - { - case 0: // Open Game Properties - SwitchToGameSettingsForPath(entry_path); - break; - case 1: // Resume Game - DoStartPath(entry_path, System::GetGameSaveStateFileName(entry_serial, -1)); - break; - case 2: // Load State - OpenLoadStateSelectorForGame(entry_path); - break; - case 3: // Default Boot - DoStartPath(entry_path); - break; - case 4: // Fast Boot - DoStartPath(entry_path, {}, true); - break; - case 5: // Slow Boot - DoStartPath(entry_path, {}, false); - break; - default: - break; - } - - CloseChoiceDialog(); - }); + HandleGameListOptions(selected_entry); } } @@ -4378,17 +4591,17 @@ void FullscreenUI::DrawGameListWindow() } EndFullscreenColumnWindow(); - if (BeginFullscreenColumnWindow(750.0f, LAYOUT_SCREEN_WIDTH, "game_list_info", UIPrimaryDarkColor)) + if (BeginFullscreenColumnWindow(-530.0f, 0.0f, "game_list_info", UIPrimaryDarkColor)) { const HostDisplayTexture* cover_texture = selected_entry ? GetGameListCover(selected_entry) : GetTextureForGameListEntryType(GameList::EntryType::Count); if (cover_texture) { const ImRect image_rect( - CenterImage(LayoutScale(ImVec2(240.0f, 350.0f)), ImVec2(static_cast(cover_texture->GetWidth()), + CenterImage(LayoutScale(ImVec2(350.0f, 350.0f)), ImVec2(static_cast(cover_texture->GetWidth()), static_cast(cover_texture->GetHeight())))); - ImGui::SetCursorPos(LayoutScale(ImVec2(145.0f, 50.0f)) + image_rect.Min); + ImGui::SetCursorPos(LayoutScale(ImVec2(90.0f, 50.0f)) + image_rect.Min); ImGui::Image(selected_entry ? GetGameListCover(selected_entry)->GetHandle() : GetTextureForGameListEntryType(GameList::EntryType::Count)->GetHandle(), image_rect.GetSize()); @@ -4400,6 +4613,7 @@ void FullscreenUI::DrawGameListWindow() float text_y = 425.0f; float text_width; + PushPrimaryColor(); ImGui::SetCursorPos(LayoutScale(start_x, text_y)); ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0.0f, field_margin_y)); ImGui::BeginGroup(); @@ -4481,21 +4695,389 @@ void FullscreenUI::DrawGameListWindow() ImGui::EndGroup(); ImGui::PopStyleVar(); - - ImGui::SetCursorPosY(ImGui::GetWindowHeight() - LayoutScale(50.0f)); - BeginMenuButtons(); - if (ActiveButton(ICON_FA_BACKWARD " Back", false)) - ReturnToMainWindow(); - EndMenuButtons(); + PopPrimaryColor(); } EndFullscreenColumnWindow(); EndFullscreenColumns(); } +void FullscreenUI::DrawGameGrid(const ImVec2& heading_size) +{ + ImGuiIO& io = ImGui::GetIO(); + if (!BeginFullscreenWindow(ImVec2(0.0f, heading_size.y), ImVec2(io.DisplaySize.x, io.DisplaySize.y - heading_size.y), + "game_grid", UIBackgroundColor)) + { + EndFullscreenWindow(); + return; + } + + if (WantsToCloseMenu()) + { + if (ImGui::IsWindowFocused()) + ReturnToMainWindow(); + } + + ResetFocusHere(); + BeginMenuButtons(); + + const ImGuiStyle& style = ImGui::GetStyle(); + + const float title_spacing = LayoutScale(10.0f); + const float item_spacing = LayoutScale(20.0f); + const float item_width_with_spacing = std::floor(LayoutScale(LAYOUT_SCREEN_WIDTH / 5.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; + const ImVec2 image_size(image_width, image_height); + const float item_height = (style.FramePadding.y * 2.0f) + image_height + title_spacing + g_medium_font->FontSize; + const ImVec2 item_size(item_width, item_height); + const u32 grid_count_x = static_cast(std::floor(ImGui::GetWindowWidth() / item_width_with_spacing)); + const float start_x = + (static_cast(ImGui::GetWindowWidth()) - (item_width_with_spacing * static_cast(grid_count_x))) * 0.5f; + + SmallString draw_title; + + u32 grid_x = 0; + u32 grid_y = 0; + ImGui::SetCursorPos(ImVec2(start_x, 0.0f)); + for (const GameList::Entry* entry : s_game_list_sorted_entries) + { + ImGuiWindow* window = ImGui::GetCurrentWindow(); + if (window->SkipItems) + continue; + + const ImGuiID id = window->GetID(entry->path.c_str(), entry->path.c_str() + entry->path.length()); + 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); + + const float t = static_cast(std::min(std::abs(std::sin(ImGui::GetTime() * 0.75) * 1.1), 1.0)); + ImGui::PushStyleColor(ImGuiCol_Border, ImGui::GetColorU32(ImGuiCol_Border, t)); + + ImGui::RenderFrame(bb.Min, bb.Max, col, true, 0.0f); + + ImGui::PopStyleColor(); + } + + bb.Min += style.FramePadding; + bb.Max -= style.FramePadding; + + const HostDisplayTexture* const cover_texture = GetGameListCover(entry); + const ImRect image_rect( + CenterImage(ImRect(bb.Min, bb.Min + image_size), ImVec2(static_cast(cover_texture->GetWidth()), + static_cast(cover_texture->GetHeight())))); + + ImGui::GetWindowDrawList()->AddImage(cover_texture->GetHandle(), image_rect.Min, image_rect.Max, + ImVec2(0.0f, 0.0f), ImVec2(1.0f, 1.0f), IM_COL32(255, 255, 255, 255)); + + const ImRect title_bb(ImVec2(bb.Min.x, bb.Min.y + image_height + title_spacing), bb.Max); + const std::string_view title( + std::string_view(entry->title).substr(0, (entry->title.length() > 31) ? 31 : std::string_view::npos)); + draw_title.Fmt("{}{}", title, (title.length() == entry->title.length()) ? "" : "..."); + ImGui::PushFont(g_medium_font); + ImGui::RenderTextClipped(title_bb.Min, title_bb.Max, draw_title.GetCharArray(), + draw_title.GetCharArray() + draw_title.GetLength(), nullptr, ImVec2(0.5f, 0.0f), + &title_bb); + ImGui::PopFont(); + + if (pressed) + HandleGameListActivate(entry); + + if (hovered && (ImGui::IsItemClicked(ImGuiMouseButton_Right) || + ImGui::IsNavInputTest(ImGuiNavInput_Input, ImGuiNavReadMode_Pressed))) + { + HandleGameListOptions(entry); + } + } + + grid_x++; + if (grid_x == grid_count_x) + { + grid_x = 0; + grid_y++; + ImGui::SetCursorPosX(start_x); + ImGui::SetCursorPosY(ImGui::GetCursorPosY() + item_spacing); + } + else + { + ImGui::SameLine(start_x + static_cast(grid_x) * (item_width + item_spacing)); + } + } + + EndMenuButtons(); + EndFullscreenWindow(); +} + +void FullscreenUI::HandleGameListActivate(const GameList::Entry* entry) +{ + // launch game + if (!OpenLoadStateSelectorForGameResume(entry)) + DoStartPath(entry->path); +} + +void FullscreenUI::HandleGameListOptions(const GameList::Entry* entry) +{ + ImGuiFullscreen::ChoiceDialogOptions options = { + {ICON_FA_WRENCH " Game Properties", false}, {ICON_FA_PLAY " Resume Game", false}, + {ICON_FA_UNDO " Load State", false}, {ICON_FA_COMPACT_DISC " Default Boot", false}, + {ICON_FA_LIGHTBULB " Fast Boot", false}, {ICON_FA_MAGIC " Slow Boot", false}, + {ICON_FA_WINDOW_CLOSE " Close Menu", false}, + }; + + OpenChoiceDialog( + entry->title.c_str(), false, std::move(options), + [entry_path = entry->path, entry_serial = entry->serial](s32 index, const std::string& title, bool checked) { + switch (index) + { + case 0: // Open Game Properties + SwitchToGameSettingsForPath(entry_path); + break; + case 1: // Resume Game + DoStartPath(entry_path, System::GetGameSaveStateFileName(entry_serial, -1)); + break; + case 2: // Load State + OpenLoadStateSelectorForGame(entry_path); + break; + case 3: // Default Boot + DoStartPath(entry_path); + break; + case 4: // Fast Boot + DoStartPath(entry_path, {}, true); + break; + case 5: // Slow Boot + DoStartPath(entry_path, {}, false); + break; + default: + break; + } + + CloseChoiceDialog(); + }); +} + +void FullscreenUI::DrawGameListSettingsPage(const ImVec2& heading_size) +{ + const ImGuiIO& io = ImGui::GetIO(); + if (!BeginFullscreenWindow(ImVec2(0.0f, heading_size.y), ImVec2(io.DisplaySize.x, io.DisplaySize.y - heading_size.y), + "settings_parent", UIBackgroundColor)) + { + EndFullscreenWindow(); + return; + } + + if (WantsToCloseMenu()) + { + if (ImGui::IsWindowFocused()) + ReturnToMainWindow(); + } + + auto lock = Host::GetSettingsLock(); + SettingsInterface* bsi = GetEditingSettingsInterface(false); + + BeginMenuButtons(); + + MenuHeading("Search Directories"); + if (MenuButton(ICON_FA_FOLDER_PLUS " Add Search Directory", "Adds a new directory to the game search list.")) + { + OpenFileSelector(ICON_FA_FOLDER_PLUS " Add Search Directory", true, [](const std::string& dir) { + if (!dir.empty()) + { + auto lock = Host::GetSettingsLock(); + SettingsInterface* bsi = Host::Internal::GetBaseSettingsLayer(); + + bsi->AddToStringList("GameList", "RecursivePaths", dir.c_str()); + bsi->RemoveFromStringList("GameList", "Paths", dir.c_str()); + SetSettingsChanged(bsi); + PopulateGameListDirectoryCache(bsi); + Host::RefreshGameListAsync(false); + } + + CloseFileSelector(); + }); + } + + for (const auto& it : s_game_list_directories_cache) + { + if (MenuButton(SmallString::FromFmt(ICON_FA_FOLDER " {}", it.first), + it.second ? "Scanning Subdirectories" : "Not Scanning Subdirectories")) + { + ImGuiFullscreen::ChoiceDialogOptions options = { + {ICON_FA_FOLDER_OPEN " Open in File Browser", false}, + {it.second ? (ICON_FA_FOLDER_MINUS " Disable Subdirectory Scanning") : + (ICON_FA_FOLDER_PLUS " Enable Subdirectory Scanning"), + false}, + {ICON_FA_TIMES " Remove From List", false}, + {ICON_FA_WINDOW_CLOSE " Close Menu", false}, + }; + + OpenChoiceDialog(it.first.c_str(), false, std::move(options), + [dir = it.first, recursive = it.second](s32 index, const std::string& title, bool checked) { + if (index < 0) + return; + + if (index == 0) + { + // Open in file browser... todo + Host::ReportErrorAsync("Error", "Not implemented"); + } + else if (index == 1) + { + // toggle subdirectory scanning + { + auto lock = Host::GetSettingsLock(); + SettingsInterface* bsi = Host::Internal::GetBaseSettingsLayer(); + if (!recursive) + { + bsi->RemoveFromStringList("GameList", "Paths", dir.c_str()); + bsi->AddToStringList("GameList", "RecursivePaths", dir.c_str()); + } + else + { + bsi->RemoveFromStringList("GameList", "RecursivePaths", dir.c_str()); + bsi->AddToStringList("GameList", "Paths", dir.c_str()); + } + + SetSettingsChanged(bsi); + PopulateGameListDirectoryCache(bsi); + } + + Host::RefreshGameListAsync(false); + } + else if (index == 2) + { + // remove from list + auto lock = Host::GetSettingsLock(); + SettingsInterface* bsi = Host::Internal::GetBaseSettingsLayer(); + bsi->RemoveFromStringList("GameList", "Paths", dir.c_str()); + bsi->RemoveFromStringList("GameList", "RecursivePaths", dir.c_str()); + SetSettingsChanged(bsi); + PopulateGameListDirectoryCache(bsi); + Host::RefreshGameListAsync(false); + } + + CloseChoiceDialog(); + }); + } + } + + static constexpr const char* view_types[] = {"Game Grid", "Game List"}; + + 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.")) + ImGui::OpenPopup("Download Covers"); + 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)); + + 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_PLUS " Rescan All Games", "Forces a full rescan of all games previously identified.")) + Host::RefreshGameListAsync(true); + + EndMenuButtons(); + + DrawCoverDownloaderWindow(); + EndFullscreenWindow(); +} + +void FullscreenUI::DrawCoverDownloaderWindow() +{ + ImGui::SetNextWindowSize(LayoutScale(1000.0f, 0.0f)); + ImGui::SetNextWindowPos(ImGui::GetIO().DisplaySize * 0.5f, ImGuiCond_Always, ImVec2(0.5f, 0.5f)); + + ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, LayoutScale(10.0f)); + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, LayoutScale(20.0f, 20.0f)); + ImGui::PushFont(g_large_font); + + bool is_open = true; + if (ImGui::BeginPopupModal("Download Covers", &is_open, ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize)) + { + ImGui::TextWrapped( + "PCSX2 can automatically download covers for games which do not currently have a cover set. We do not host any " + "cover images, the user must provide their own source for images."); + ImGui::NewLine(); + ImGui::TextWrapped( + "In the form below, specify the URLs to download covers from, with one template URL per line. The following " + "variables are available:"); + ImGui::NewLine(); + ImGui::TextWrapped("${title}: Title of the game.\n${filetitle}: Name component of the game's filename.\n${serial}: " + "Serial of the game."); + ImGui::NewLine(); + ImGui::TextWrapped("Example: https://www.example-not-a-real-domain.com/covers/${serial}.jpg"); + ImGui::NewLine(); + + BeginMenuButtons(); + + static char template_urls[512]; + ImGui::InputTextMultiline("##templates", template_urls, sizeof(template_urls), + ImVec2(ImGui::GetCurrentWindow()->WorkRect.GetWidth(), LayoutScale(175.0f))); + + ImGui::SetCursorPosY(ImGui::GetCursorPosY() + LayoutScale(5.0f)); + + static bool use_serial_names; + ImGui::PushFont(g_medium_font); + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, LayoutScale(2.0f, 2.0f)); + ImGui::Checkbox("Use Serial File Names", &use_serial_names); + ImGui::PopStyleVar(1); + ImGui::PopFont(); + + ImGui::SetCursorPosY(ImGui::GetCursorPosY() + LayoutScale(10.0f)); + + const bool download_enabled = (std::strlen(template_urls) > 0); + + if (ActiveButton(ICON_FA_DOWNLOAD " Start Download", false, download_enabled)) + { + StartAsyncOp( + [urls = StringUtil::SplitNewString(template_urls, '\n'), + use_serial_names = use_serial_names](::ProgressCallback* progress) { + GameList::DownloadCovers(urls, use_serial_names, progress, + [](const GameList::Entry* entry, std::string save_path) { + // cache the cover path on our side once it's saved + Host::RunOnCPUThread([path = entry->path, save_path = std::move(save_path)]() { + s_cover_image_map[std::move(path)] = std::move(save_path); + }); + }); + }, + "Download Covers"); + std::memset(template_urls, 0, sizeof(template_urls)); + use_serial_names = false; + ImGui::CloseCurrentPopup(); + } + + if (ActiveButton(ICON_FA_TIMES " Cancel", false)) + { + std::memset(template_urls, 0, sizeof(template_urls)); + use_serial_names = false; + ImGui::CloseCurrentPopup(); + } + + EndMenuButtons(); + + ImGui::EndPopup(); + } + + ImGui::PopFont(); + ImGui::PopStyleVar(2); +} + void FullscreenUI::SwitchToGameList() { s_current_main_window = MainWindowType::GameList; + s_game_list_page = static_cast(Host::GetBaseIntSettingValue("Main", "DefaultFullscreenUIGameView", 0)); + { + auto lock = Host::GetSettingsLock(); + PopulateGameListDirectoryCache(Host::Internal::GetBaseSettingsLayer()); + } QueueResetFocus(); } diff --git a/src/frontend-common/fullscreen_ui.h b/src/frontend-common/fullscreen_ui.h index f052ce439..5297e5273 100644 --- a/src/frontend-common/fullscreen_ui.h +++ b/src/frontend-common/fullscreen_ui.h @@ -33,6 +33,8 @@ public: ProgressCallback(std::string name); ~ProgressCallback() override; + ALWAYS_INLINE const std::string& GetName() const { return m_name; } + void PushState() override; void PopState() override; diff --git a/src/frontend-common/game_list.cpp b/src/frontend-common/game_list.cpp index 266f7ba90..e344bc4ae 100644 --- a/src/frontend-common/game_list.cpp +++ b/src/frontend-common/game_list.cpp @@ -647,7 +647,7 @@ std::string GameList::GetCoverImagePath(const std::string& path, const std::stri return {}; } -std::string GameList::GetNewCoverImagePathForEntry(const Entry* entry, const char* new_filename) +std::string GameList::GetNewCoverImagePathForEntry(const Entry* entry, const char* new_filename, bool use_serial) { const char* extension = std::strrchr(new_filename, '.'); if (!extension) @@ -665,12 +665,12 @@ std::string GameList::GetNewCoverImagePathForEntry(const Entry* entry, const cha const std::string sanitized_name(Path::SanitizeFileName(entry->title)); std::string name; - if (sanitized_name != entry->title) + if (sanitized_name != entry->title || use_serial) name = fmt::format("{}{}", entry->serial, extension); else name = fmt::format("{}{}", entry->title, extension); - return Path::Combine(EmuFolders::Covers, name); + return Path::Combine(EmuFolders::Covers, Path::SanitizeFileName(name)); } size_t GameList::Entry::GetReleaseDateString(char* buffer, size_t buffer_size) const @@ -690,7 +690,8 @@ size_t GameList::Entry::GetReleaseDateString(char* buffer, size_t buffer_size) c return std::strftime(buffer, buffer_size, "%d %B %Y", &date_tm); } -bool GameList::DownloadCovers(const std::vector& url_templates, ProgressCallback* progress /*= nullptr*/) +bool GameList::DownloadCovers(const std::vector& url_templates, bool use_serial, + ProgressCallback* progress, std::function save_callback) { if (!progress) progress = ProgressCallback::NullProgressCallback; @@ -776,23 +777,25 @@ bool GameList::DownloadCovers(const std::vector& url_templates, Pro // we could actually do a few in parallel here... std::string filename(Common::HTTPDownloader::URLDecode(url)); - downloader->CreateRequest(std::move(url), [entry_path = std::move(entry_path), filename = std::move(filename)]( - s32 status_code, Common::HTTPDownloader::Request::Data data) { - if (status_code != Common::HTTPDownloader::HTTP_OK || data.empty()) - return; + downloader->CreateRequest( + std::move(url), [use_serial, &save_callback, entry_path = std::move(entry_path), + filename = std::move(filename)](s32 status_code, Common::HTTPDownloader::Request::Data data) { + if (status_code != Common::HTTPDownloader::HTTP_OK || data.empty()) + return; - std::unique_lock lock(s_mutex); - const GameList::Entry* entry = GetEntryForPath(entry_path.c_str()); - if (!entry || !GetCoverImagePathForEntry(entry).empty()) - return; + std::unique_lock lock(s_mutex); + const GameList::Entry* entry = GetEntryForPath(entry_path.c_str()); + if (!entry || !GetCoverImagePathForEntry(entry).empty()) + return; - std::string write_path(GetNewCoverImagePathForEntry(entry, filename.c_str())); - if (write_path.empty()) - return; + std::string write_path(GetNewCoverImagePathForEntry(entry, filename.c_str(), use_serial)); + if (write_path.empty()) + return; - FileSystem::WriteBinaryFile(write_path.c_str(), data.data(), data.size()); - Host::CoversChanged(); - }); + FileSystem::WriteBinaryFile(write_path.c_str(), data.data(), data.size()); + if (save_callback) + save_callback(entry, std::move(write_path)); + }); downloader->WaitForAllRequests(); progress->IncrementProgressValue(); } diff --git a/src/frontend-common/game_list.h b/src/frontend-common/game_list.h index 5dca2594e..b0d6edc25 100644 --- a/src/frontend-common/game_list.h +++ b/src/frontend-common/game_list.h @@ -3,6 +3,7 @@ #include "core/types.h" #include "util/cd_image.h" #include +#include #include #include @@ -74,9 +75,13 @@ void Refresh(bool invalidate_cache, bool only_cache = false, ProgressCallback* p std::string GetCoverImagePathForEntry(const Entry* entry); std::string GetCoverImagePath(const std::string& path, const std::string& serial, const std::string& title); -std::string GetNewCoverImagePathForEntry(const Entry* entry, const char* new_filename); +std::string GetNewCoverImagePathForEntry(const Entry* entry, const char* new_filename, bool use_serial); -bool DownloadCovers(const std::vector& url_templates, ProgressCallback* progress = nullptr); +/// Downloads covers using the specified URL templates. By default, covers are saved by title, but this can be changed +/// with the use_serial parameter. save_callback optionall takes the entry and the path the new cover is saved to. +bool DownloadCovers(const std::vector& url_templates, bool use_serial = false, + ProgressCallback* progress = nullptr, + std::function save_callback = {}); }; // namespace GameList namespace Host { @@ -85,8 +90,4 @@ void RefreshGameListAsync(bool invalidate_cache); /// Cancels game list refresh, if there is one in progress. void CancelGameListRefresh(); - -void DownloadCoversAsync(std::vector url_templates); -void CancelCoversDownload(); -void CoversChanged(); } // namespace Host diff --git a/src/frontend-common/host_settings.cpp b/src/frontend-common/host_settings.cpp index 4e3c967a3..5ca12cce4 100644 --- a/src/frontend-common/host_settings.cpp +++ b/src/frontend-common/host_settings.cpp @@ -106,6 +106,55 @@ std::vector Host::GetStringListSetting(const char* section, const c return s_layered_settings_interface.GetStringList(section, key); } +void Host::SetBaseBoolSettingValue(const char* section, const char* key, bool value) +{ + std::unique_lock lock(s_settings_mutex); + s_layered_settings_interface.GetLayer(LayeredSettingsInterface::LAYER_BASE)->SetBoolValue(section, key, value); +} + +void Host::SetBaseIntSettingValue(const char* section, const char* key, int value) +{ + std::unique_lock lock(s_settings_mutex); + s_layered_settings_interface.GetLayer(LayeredSettingsInterface::LAYER_BASE)->SetIntValue(section, key, value); +} + +void Host::SetBaseFloatSettingValue(const char* section, const char* key, float value) +{ + std::unique_lock lock(s_settings_mutex); + s_layered_settings_interface.GetLayer(LayeredSettingsInterface::LAYER_BASE)->SetFloatValue(section, key, value); +} + +void Host::SetBaseStringSettingValue(const char* section, const char* key, const char* value) +{ + std::unique_lock lock(s_settings_mutex); + s_layered_settings_interface.GetLayer(LayeredSettingsInterface::LAYER_BASE)->SetStringValue(section, key, value); +} + +void Host::SetBaseStringListSettingValue(const char* section, const char* key, const std::vector& values) +{ + std::unique_lock lock(s_settings_mutex); + s_layered_settings_interface.GetLayer(LayeredSettingsInterface::LAYER_BASE)->SetStringList(section, key, values); +} + +bool Host::AddValueToBaseStringListSetting(const char* section, const char* key, const char* value) +{ + std::unique_lock lock(s_settings_mutex); + return s_layered_settings_interface.GetLayer(LayeredSettingsInterface::LAYER_BASE)->AddToStringList(section, key, value); +} + +bool Host::RemoveValueFromBaseStringListSetting(const char* section, const char* key, const char* value) +{ + std::unique_lock lock(s_settings_mutex); + return s_layered_settings_interface.GetLayer(LayeredSettingsInterface::LAYER_BASE) + ->RemoveFromStringList(section, key, value); +} + +void Host::DeleteBaseSettingValue(const char* section, const char* key) +{ + std::unique_lock lock(s_settings_mutex); + s_layered_settings_interface.GetLayer(LayeredSettingsInterface::LAYER_BASE)->DeleteValue(section, key); +} + SettingsInterface* Host::Internal::GetBaseSettingsLayer() { return s_layered_settings_interface.GetLayer(LayeredSettingsInterface::LAYER_BASE); diff --git a/src/frontend-common/imgui_fullscreen.cpp b/src/frontend-common/imgui_fullscreen.cpp index 954639957..fe3755e45 100644 --- a/src/frontend-common/imgui_fullscreen.cpp +++ b/src/frontend-common/imgui_fullscreen.cpp @@ -17,15 +17,20 @@ #include "fmt/core.h" #include "imgui_internal.h" #include "imgui_stdlib.h" + +#include #include #include #include #include #include +#include Log_SetChannel(ImGuiFullscreen); namespace ImGuiFullscreen { +using MessageDialogCallbackVariant = std::variant; + static std::optional LoadTextureImage(const char* path); static std::shared_ptr UploadTexture(const char* path, const Common::RGBA8Image& image); static void TextureLoaderThread(); @@ -33,6 +38,7 @@ static void TextureLoaderThread(); static void DrawFileSelector(); static void DrawChoiceDialog(); static void DrawInputDialog(); +static void DrawMessageDialog(); static void DrawBackgroundProgressDialogs(ImVec2& position, float spacing); static void DrawNotifications(ImVec2& position, float spacing); static void DrawToast(); @@ -98,6 +104,12 @@ static std::string s_input_dialog_text; static std::string s_input_dialog_ok_text; static InputStringDialogCallback s_input_dialog_callback; +static bool s_message_dialog_open = false; +static std::string s_message_dialog_title; +static std::string s_message_dialog_message; +static std::array s_message_dialog_buttons; +static MessageDialogCallbackVariant s_message_dialog_callback; + struct FileSelectorItem { FileSelectorItem() = default; @@ -201,6 +213,7 @@ void ImGuiFullscreen::Shutdown() s_notifications.clear(); s_background_progress_dialogs.clear(); CloseInputDialog(); + CloseMessageDialog(); s_choice_dialog_open = false; s_choice_dialog_checkable = false; s_choice_dialog_title = {}; @@ -431,6 +444,16 @@ void ImGuiFullscreen::BeginLayout() ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f); ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f); + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, LayoutScale(8.0f, 8.0f)); + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, LayoutScale(4.0f, 3.0f)); + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, LayoutScale(8.0f, 4.0f)); + ImGui::PushStyleVar(ImGuiStyleVar_ItemInnerSpacing, LayoutScale(4.0f, 4.0f)); + ImGui::PushStyleVar(ImGuiStyleVar_CellPadding, LayoutScale(4.0f, 2.0f)); + ImGui::PushStyleVar(ImGuiStyleVar_IndentSpacing, LayoutScale(21.0f)); + ImGui::PushStyleVar(ImGuiStyleVar_ScrollbarSize, LayoutScale(14.0f)); + ImGui::PushStyleVar(ImGuiStyleVar_ScrollbarRounding, 0.0f); + ImGui::PushStyleVar(ImGuiStyleVar_GrabMinSize, LayoutScale(10.0f)); + ImGui::PushStyleVar(ImGuiStyleVar_TabRounding, LayoutScale(4.0f)); ImGui::PushStyleColor(ImGuiCol_Text, UISecondaryTextColor); ImGui::PushStyleColor(ImGuiCol_TextDisabled, UIDisabledColor); ImGui::PushStyleColor(ImGuiCol_Button, UISecondaryColor); @@ -448,6 +471,7 @@ void ImGuiFullscreen::EndLayout() DrawFileSelector(); DrawChoiceDialog(); DrawInputDialog(); + DrawMessageDialog(); const float notification_margin = LayoutScale(10.0f); const float spacing = LayoutScale(10.0f); @@ -460,7 +484,7 @@ void ImGuiFullscreen::EndLayout() DrawToast(); ImGui::PopStyleColor(10); - ImGui::PopStyleVar(2); + ImGui::PopStyleVar(12); } void ImGuiFullscreen::QueueResetFocus() @@ -536,10 +560,12 @@ void ImGuiFullscreen::PopSecondaryColor() ImGui::PopStyleColor(5); } -bool ImGuiFullscreen::BeginFullscreenColumns(const char* title) +bool ImGuiFullscreen::BeginFullscreenColumns(const char* title, float pos_y, bool expand_to_screen_width) { - ImGui::SetNextWindowPos(ImVec2(g_layout_padding_left, 0.0f)); - ImGui::SetNextWindowSize(ImVec2(LayoutScale(LAYOUT_SCREEN_WIDTH), ImGui::GetIO().DisplaySize.y)); + ImGui::SetNextWindowPos(ImVec2(expand_to_screen_width ? 0.0f : g_layout_padding_left, pos_y)); + ImGui::SetNextWindowSize( + ImVec2(expand_to_screen_width ? ImGui::GetIO().DisplaySize.x : LayoutScale(LAYOUT_SCREEN_WIDTH), + ImGui::GetIO().DisplaySize.y - pos_y)); ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f)); ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f); @@ -569,8 +595,16 @@ void ImGuiFullscreen::EndFullscreenColumns() bool ImGuiFullscreen::BeginFullscreenColumnWindow(float start, float end, const char* name, const ImVec4& background) { - const ImVec2 pos(LayoutScale(start), 0.0f); - const ImVec2 size(LayoutScale(end - start), ImGui::GetIO().DisplaySize.y); + start = LayoutScale(start); + end = LayoutScale(end); + + if (start < 0.0f) + start = ImGui::GetIO().DisplaySize.x + start; + if (end <= 0.0f) + end = ImGui::GetIO().DisplaySize.x + end; + + const ImVec2 pos(start, 0.0f); + const ImVec2 size(end - start, ImGui::GetCurrentWindow()->Size.y); ImGui::PushStyleColor(ImGuiCol_ChildBg, background); @@ -891,6 +925,33 @@ bool ImGuiFullscreen::MenuButton(const char* title, const char* summary, bool en return pressed; } +bool ImGuiFullscreen::MenuButtonWithoutSummary(const char* title, bool enabled, float height, ImFont* font, + const ImVec2& text_align) +{ + ImRect bb; + bool visible, hovered; + bool pressed = MenuButtonFrame(title, enabled, height, &visible, &hovered, &bb); + if (!visible) + return false; + + const float midpoint = bb.Min.y + font->FontSize + LayoutScale(4.0f); + const ImRect title_bb(bb.Min, ImVec2(bb.Max.x, midpoint)); + const ImRect summary_bb(ImVec2(bb.Min.x, midpoint), bb.Max); + + if (!enabled) + ImGui::PushStyleColor(ImGuiCol_Text, ImGui::GetColorU32(ImGuiCol_TextDisabled)); + + ImGui::PushFont(font); + ImGui::RenderTextClipped(title_bb.Min, title_bb.Max, title, nullptr, nullptr, text_align, &title_bb); + ImGui::PopFont(); + + if (!enabled) + ImGui::PopStyleColor(); + + s_menu_button_index++; + return pressed; +} + bool ImGuiFullscreen::MenuImageButton(const char* title, const char* summary, ImTextureID user_texture_id, const ImVec2& image_size, bool enabled, float height, const ImVec2& uv0, const ImVec2& uv1, ImFont* title_font, ImFont* summary_font) @@ -1215,6 +1276,7 @@ bool ImGuiFullscreen::RangeButton(const char* title, const char* summary, s32* v bool changed = false; ImGui::SetNextWindowSize(LayoutScale(500.0f, 180.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)); @@ -1286,6 +1348,7 @@ bool ImGuiFullscreen::RangeButton(const char* title, const char* summary, float* bool changed = false; ImGui::SetNextWindowSize(LayoutScale(500.0f, 180.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)); @@ -1531,7 +1594,7 @@ void ImGuiFullscreen::PopulateFileSelectorItems() { for (std::string& root_path : FileSystem::GetRootDirectoryList()) { - s_file_selector_items.emplace_back(StringUtil::StdStringFromFormat(ICON_FA_FOLDER " %s", root_path.c_str()), + s_file_selector_items.emplace_back(StringUtil::StdStringFromFormat(ICON_FA_FOLDER " %s", root_path.c_str()), std::move(root_path), false); } } @@ -1566,7 +1629,7 @@ void ImGuiFullscreen::PopulateFileSelectorItems() if (fd.Attributes & FILESYSTEM_FILE_ATTRIBUTE_DIRECTORY) { - std::string title(StringUtil::StdStringFromFormat(ICON_FA_FOLDER " %s", fd.FileName.c_str())); + std::string title(StringUtil::StdStringFromFormat(ICON_FA_FOLDER " %s", fd.FileName.c_str())); s_file_selector_items.emplace_back(std::move(title), std::move(full_path), false); } else @@ -1580,7 +1643,7 @@ void ImGuiFullscreen::PopulateFileSelectorItems() continue; } - std::string title(StringUtil::StdStringFromFormat(ICON_FA_FILE " %s", fd.FileName.c_str())); + std::string title(StringUtil::StdStringFromFormat(ICON_FA_FILE " %s", fd.FileName.c_str())); s_file_selector_items.emplace_back(std::move(title), std::move(full_path), true); } } @@ -1652,7 +1715,7 @@ void ImGuiFullscreen::DrawFileSelector() ImGui::PushStyleColor(ImGuiCol_Text, UIPrimaryTextColor); ImGui::PushStyleColor(ImGuiCol_TitleBg, UIPrimaryDarkColor); ImGui::PushStyleColor(ImGuiCol_TitleBgActive, UIPrimaryColor); - ImGui::PushStyleColor(ImGuiCol_PopupBg, UIBackgroundColor); + ImGui::PushStyleColor(ImGuiCol_PopupBg, MulAlpha(UIBackgroundColor, 0.95f)); bool is_open = !WantsToCloseMenu(); bool directory_selected = false; @@ -1665,13 +1728,13 @@ void ImGuiFullscreen::DrawFileSelector() if (!s_file_selector_current_directory.empty()) { - MenuButton(fmt::format(ICON_FA_FOLDER_OPEN " {}", s_file_selector_current_directory).c_str(), nullptr, false, + MenuButton(fmt::format(ICON_FA_FOLDER_OPEN " {}", s_file_selector_current_directory).c_str(), nullptr, false, LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY); } if (s_file_selector_directory && !s_file_selector_current_directory.empty()) { - if (MenuButton(ICON_FA_FOLDER_PLUS " ", nullptr, true, LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY)) + if (MenuButton(ICON_FA_FOLDER_PLUS " ", nullptr, true, LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY)) directory_selected = true; } @@ -1755,16 +1818,6 @@ void ImGuiFullscreen::DrawChoiceDialog() if (!s_choice_dialog_open) return; - const float width = 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 = - std::min(400.0f, title_height + (LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY + (LAYOUT_MENU_BUTTON_Y_PADDING * 2.0f)) * - static_cast(s_choice_dialog_options.size())); - ImGui::SetNextWindowSize(LayoutScale(width, height)); - ImGui::SetNextWindowPos(ImGui::GetIO().DisplaySize * 0.5f, ImGuiCond_Always, ImVec2(0.5f, 0.5f)); - ImGui::OpenPopup(s_choice_dialog_title.c_str()); - ImGui::PushFont(g_large_font); ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, LayoutScale(10.0f)); ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, @@ -1772,7 +1825,18 @@ void ImGuiFullscreen::DrawChoiceDialog() ImGui::PushStyleColor(ImGuiCol_Text, UIPrimaryTextColor); ImGui::PushStyleColor(ImGuiCol_TitleBg, UIPrimaryDarkColor); ImGui::PushStyleColor(ImGuiCol_TitleBgActive, UIPrimaryColor); - ImGui::PushStyleColor(ImGuiCol_PopupBg, UIBackgroundColor); + 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 = + std::min(LayoutScale(400.0f), + title_height + LayoutScale(LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY + (LAYOUT_MENU_BUTTON_Y_PADDING * 2.0f)) * + static_cast(s_choice_dialog_options.size())); + ImGui::SetNextWindowSize(ImVec2(width, height)); + ImGui::SetNextWindowPos(ImGui::GetIO().DisplaySize * 0.5f, ImGuiCond_Always, ImVec2(0.5f, 0.5f)); + ImGui::OpenPopup(s_choice_dialog_title.c_str()); bool is_open = !WantsToCloseMenu(); s32 choice = -1; @@ -1791,7 +1855,7 @@ void ImGuiFullscreen::DrawChoiceDialog() auto& option = s_choice_dialog_options[i]; const std::string title( - fmt::format("{0} {1}", option.second ? ICON_FA_CHECK_SQUARE : ICON_FA_SQUARE, option.first)); + fmt::format("{0} {1}", option.second ? ICON_FA_CHECK_SQUARE : ICON_FA_SQUARE, option.first)); if (MenuButton(title.c_str(), nullptr, true, LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY)) { choice = i; @@ -1806,7 +1870,7 @@ void ImGuiFullscreen::DrawChoiceDialog() auto& option = s_choice_dialog_options[i]; std::string title; if (option.second) - title += ICON_FA_CHECK " "; + title += ICON_FA_CHECK " "; title += option.first; if (ActiveButton(title.c_str(), option.second, true, LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY)) @@ -1871,9 +1935,14 @@ void ImGuiFullscreen::DrawInputDialog() ImGui::SetNextWindowPos(ImGui::GetIO().DisplaySize * 0.5f, ImGuiCond_Always, ImVec2(0.5f, 0.5f)); ImGui::OpenPopup(s_input_dialog_title.c_str()); - ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, LayoutScale(10.0f)); - ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, LayoutScale(20.0f, 20.0f)); 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::PushStyleColor(ImGuiCol_Text, UIPrimaryTextColor); + ImGui::PushStyleColor(ImGuiCol_TitleBg, UIPrimaryDarkColor); + ImGui::PushStyleColor(ImGuiCol_TitleBgActive, UIPrimaryColor); + ImGui::PushStyleColor(ImGuiCol_PopupBg, MulAlpha(UIBackgroundColor, 0.95f)); bool is_open = true; if (ImGui::BeginPopupModal(s_input_dialog_title.c_str(), &is_open, @@ -1881,16 +1950,25 @@ void ImGuiFullscreen::DrawInputDialog() ImGuiWindowFlags_NoMove)) { ImGui::TextWrapped("%s", s_input_dialog_message.c_str()); - ImGui::NewLine(); - - if (!s_input_dialog_caption.empty()) - ImGui::TextUnformatted(s_input_dialog_caption.c_str()); - ImGui::InputText("##input", &s_input_dialog_text); - - ImGui::NewLine(); BeginMenuButtons(); + ImGui::SetCursorPosY(ImGui::GetCursorPosY() + LayoutScale(10.0f)); + + if (!s_input_dialog_caption.empty()) + { + const float prev = ImGui::GetCursorPosX(); + ImGui::TextUnformatted(s_input_dialog_caption.c_str()); + ImGui::SetNextItemWidth(ImGui::GetCursorPosX() - prev); + } + else + { + ImGui::SetNextItemWidth(ImGui::GetCurrentWindow()->WorkRect.GetWidth()); + } + ImGui::InputText("##input", &s_input_dialog_text); + + ImGui::SetCursorPosY(ImGui::GetCursorPosY() + LayoutScale(10.0f)); + const bool ok_enabled = !s_input_dialog_text.empty(); if (ActiveButton(s_input_dialog_ok_text.c_str(), false, ok_enabled) && ok_enabled) @@ -1903,7 +1981,7 @@ void ImGuiFullscreen::DrawInputDialog() cb(std::move(text)); } - if (ActiveButton(ICON_FA_TIMES " Cancel", false)) + if (ActiveButton(ICON_FA_TIMES " Cancel", false)) { CloseInputDialog(); @@ -1917,8 +1995,9 @@ void ImGuiFullscreen::DrawInputDialog() if (!is_open) CloseInputDialog(); - ImGui::PopFont(); + ImGui::PopStyleColor(4); ImGui::PopStyleVar(2); + ImGui::PopFont(); } void ImGuiFullscreen::CloseInputDialog() @@ -1935,6 +2014,137 @@ void ImGuiFullscreen::CloseInputDialog() s_input_dialog_callback = {}; } +bool ImGuiFullscreen::IsMessageBoxDialogOpen() +{ + return s_message_dialog_open; +} + +void ImGuiFullscreen::OpenConfirmMessageDialog(std::string title, std::string message, + ConfirmMessageDialogCallback callback, std::string yes_button_text, + std::string no_button_text) +{ + CloseMessageDialog(); + + s_message_dialog_open = true; + s_message_dialog_title = std::move(title); + s_message_dialog_message = std::move(message); + s_message_dialog_callback = std::move(callback); + s_message_dialog_buttons[0] = std::move(yes_button_text); + s_message_dialog_buttons[1] = std::move(no_button_text); +} + +void ImGuiFullscreen::OpenInfoMessageDialog(std::string title, std::string message, InfoMessageDialogCallback callback, + std::string button_text) +{ + CloseMessageDialog(); + + s_message_dialog_open = true; + s_message_dialog_title = std::move(title); + s_message_dialog_message = std::move(message); + s_message_dialog_callback = std::move(callback); + s_message_dialog_buttons[0] = std::move(button_text); +} + +void ImGuiFullscreen::OpenMessageDialog(std::string title, std::string message, MessageDialogCallback callback, + std::string first_button_text, std::string second_button_text, + std::string third_button_text) +{ + CloseMessageDialog(); + + s_message_dialog_open = true; + s_message_dialog_title = std::move(title); + s_message_dialog_message = std::move(message); + s_message_dialog_callback = std::move(callback); + s_message_dialog_buttons[0] = std::move(first_button_text); + s_message_dialog_buttons[1] = std::move(second_button_text); + s_message_dialog_buttons[2] = std::move(third_button_text); +} + +void ImGuiFullscreen::CloseMessageDialog() +{ + if (!s_message_dialog_open) + return; + + s_message_dialog_open = false; + s_message_dialog_title = {}; + s_message_dialog_message = {}; + s_message_dialog_buttons = {}; + s_message_dialog_callback = {}; +} + +void ImGuiFullscreen::DrawMessageDialog() +{ + if (!s_message_dialog_open) + return; + + const char* win_id = s_message_dialog_title.empty() ? "##messagedialog" : s_message_dialog_title.c_str(); + + ImGui::SetNextWindowSize(LayoutScale(700.0f, 0.0f)); + ImGui::SetNextWindowPos(ImGui::GetIO().DisplaySize * 0.5f, ImGuiCond_Always, ImVec2(0.5f, 0.5f)); + ImGui::OpenPopup(win_id); + + ImGui::PushFont(g_large_font); + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, LayoutScale(20.0f, 20.0f)); + ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, LayoutScale(10.0f)); + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, + LayoutScale(LAYOUT_MENU_BUTTON_X_PADDING, LAYOUT_MENU_BUTTON_Y_PADDING)); + ImGui::PushStyleColor(ImGuiCol_Text, UIPrimaryTextColor); + ImGui::PushStyleColor(ImGuiCol_TitleBg, UIPrimaryDarkColor); + ImGui::PushStyleColor(ImGuiCol_TitleBgActive, UIPrimaryColor); + ImGui::PushStyleColor(ImGuiCol_PopupBg, MulAlpha(UIBackgroundColor, 0.95f)); + + bool is_open = true; + const u32 flags = ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | + (s_message_dialog_title.empty() ? ImGuiWindowFlags_NoTitleBar : 0); + std::optional result; + + if (ImGui::BeginPopupModal(win_id, &is_open, flags)) + { + BeginMenuButtons(); + + ImGui::TextWrapped("%s", s_message_dialog_message.c_str()); + ImGui::SetCursorPosY(ImGui::GetCursorPosY() + LayoutScale(20.0f)); + + for (s32 button_index = 0; button_index < static_cast(s_message_dialog_buttons.size()); button_index++) + { + if (!s_message_dialog_buttons[button_index].empty() && + ActiveButton(s_message_dialog_buttons[button_index].c_str(), false)) + { + result = button_index; + ImGui::CloseCurrentPopup(); + } + } + + EndMenuButtons(); + + ImGui::EndPopup(); + } + + ImGui::PopStyleColor(4); + ImGui::PopStyleVar(3); + ImGui::PopFont(); + + if (!is_open || result.has_value()) + { + // have to move out in case they open another dialog in the callback + auto cb = (std::move(s_message_dialog_callback)); + CloseMessageDialog(); + + if (std::holds_alternative(cb)) + { + const InfoMessageDialogCallback& func = std::get(cb); + if (func) + func(); + } + else if (std::holds_alternative(cb)) + { + const ConfirmMessageDialogCallback& func = std::get(cb); + if (func) + func(result.value_or(1) == 0); + } + } +} + static float s_notification_vertical_position = 0.3f; static float s_notification_vertical_direction = -1.0f; @@ -1965,10 +2175,12 @@ void ImGuiFullscreen::OpenBackgroundProgressDialog(const char* str_id, std::stri std::unique_lock lock(s_background_progress_lock); +#ifdef _DEBUG for (const BackgroundProgressDialogData& data : s_background_progress_dialogs) { DebugAssert(data.id != id); } +#endif BackgroundProgressDialogData data; data.id = id; @@ -2271,41 +2483,44 @@ void ImGuiFullscreen::DrawToast() } } -void ImGuiFullscreen::SetTheme() +void ImGuiFullscreen::SetTheme(bool light) { -#if 1 - // dark - UIBackgroundColor = HEX_TO_IMVEC4(0x212121, 0xff); - UIBackgroundTextColor = HEX_TO_IMVEC4(0xffffff, 0xff); - UIBackgroundLineColor = HEX_TO_IMVEC4(0xf0f0f0, 0xff); - UIBackgroundHighlightColor = HEX_TO_IMVEC4(0x4b4b4b, 0xff); - UIPrimaryColor = HEX_TO_IMVEC4(0x2e2e2e, 0xff); - UIPrimaryLightColor = HEX_TO_IMVEC4(0x484848, 0xff); - UIPrimaryDarkColor = HEX_TO_IMVEC4(0x000000, 0xff); - UIPrimaryTextColor = HEX_TO_IMVEC4(0xffffff, 0xff); - UIDisabledColor = HEX_TO_IMVEC4(0xaaaaaa, 0xff); - UITextHighlightColor = HEX_TO_IMVEC4(0x90caf9, 0xff); - UIPrimaryLineColor = HEX_TO_IMVEC4(0xffffff, 0xff); - UISecondaryColor = HEX_TO_IMVEC4(0x0d47a1, 0xff); - UISecondaryLightColor = HEX_TO_IMVEC4(0x63a4ff, 0xff); - UISecondaryDarkColor = HEX_TO_IMVEC4(0x002171, 0xff); - UISecondaryTextColor = HEX_TO_IMVEC4(0xffffff, 0xff); -#elif 1 - // light - UIBackgroundColor = HEX_TO_IMVEC4(0xf5f5f6, 0xff); - UIBackgroundTextColor = HEX_TO_IMVEC4(0x000000, 0xff); - UIBackgroundLineColor = HEX_TO_IMVEC4(0xe1e2e1, 0xff); - UIBackgroundHighlightColor = HEX_TO_IMVEC4(0xe1e2e1, 0xff); - UIPrimaryColor = HEX_TO_IMVEC4(0x0d47a1, 0xff); - UIPrimaryLightColor = HEX_TO_IMVEC4(0x5472d3, 0xff); - UIPrimaryDarkColor = HEX_TO_IMVEC4(0x002171, 0xff); - UIPrimaryTextColor = HEX_TO_IMVEC4(0xffffff, 0xff); - UIDisabledColor = HEX_TO_IMVEC4(0xaaaaaa, 0xff); - UITextHighlightColor = HEX_TO_IMVEC4(0x8e8e8e, 0xff); - UIPrimaryLineColor = HEX_TO_IMVEC4(0x000000, 0xff); - UISecondaryColor = HEX_TO_IMVEC4(0x3d5afe, 0xff); - UISecondaryLightColor = HEX_TO_IMVEC4(0xc0cfff, 0xff); - UISecondaryDarkColor = HEX_TO_IMVEC4(0x0031ca, 0xff); - UISecondaryTextColor = HEX_TO_IMVEC4(0x000000, 0xff); -#endif -} + if (!light) + { + // dark + UIBackgroundColor = HEX_TO_IMVEC4(0x212121, 0xff); + UIBackgroundTextColor = HEX_TO_IMVEC4(0xffffff, 0xff); + UIBackgroundLineColor = HEX_TO_IMVEC4(0xf0f0f0, 0xff); + UIBackgroundHighlightColor = HEX_TO_IMVEC4(0x4b4b4b, 0xff); + UIPrimaryColor = HEX_TO_IMVEC4(0x2e2e2e, 0xff); + UIPrimaryLightColor = HEX_TO_IMVEC4(0x484848, 0xff); + UIPrimaryDarkColor = HEX_TO_IMVEC4(0x000000, 0xff); + UIPrimaryTextColor = HEX_TO_IMVEC4(0xffffff, 0xff); + UIDisabledColor = HEX_TO_IMVEC4(0xaaaaaa, 0xff); + UITextHighlightColor = HEX_TO_IMVEC4(0x90caf9, 0xff); + UIPrimaryLineColor = HEX_TO_IMVEC4(0xffffff, 0xff); + UISecondaryColor = HEX_TO_IMVEC4(0x0d47a1, 0xff); + UISecondaryLightColor = HEX_TO_IMVEC4(0x63a4ff, 0xff); + UISecondaryDarkColor = HEX_TO_IMVEC4(0x002171, 0xff); + UISecondaryTextColor = HEX_TO_IMVEC4(0xffffff, 0xff); + } + else + { + // light + UIBackgroundColor = HEX_TO_IMVEC4(0xf5f5f6, 0xff); + UIBackgroundTextColor = HEX_TO_IMVEC4(0x000000, 0xff); + UIBackgroundLineColor = HEX_TO_IMVEC4(0xe1e2e1, 0xff); + UIBackgroundHighlightColor = HEX_TO_IMVEC4(0xe1e2e1, 0xff); + UIPrimaryColor = HEX_TO_IMVEC4(0x0d47a1, 0xff); + UIPrimaryLightColor = HEX_TO_IMVEC4(0x5472d3, 0xff); + UIPrimaryDarkColor = HEX_TO_IMVEC4(0x002171, 0xff); + UIPrimaryTextColor = HEX_TO_IMVEC4(0xffffff, 0xff); + UIDisabledColor = HEX_TO_IMVEC4(0xaaaaaa, 0xff); + UITextHighlightColor = HEX_TO_IMVEC4(0x8e8e8e, 0xff); + UIPrimaryLineColor = HEX_TO_IMVEC4(0x000000, 0xff); + UISecondaryColor = HEX_TO_IMVEC4(0x3d5afe, 0xff); + UISecondaryLightColor = HEX_TO_IMVEC4(0xc0cfff, 0xff); + UISecondaryDarkColor = HEX_TO_IMVEC4(0x0031ca, 0xff); + UISecondaryTextColor = HEX_TO_IMVEC4(0x000000, 0xff); + } +} \ No newline at end of file diff --git a/src/frontend-common/imgui_fullscreen.h b/src/frontend-common/imgui_fullscreen.h index 4df4241d7..3bb2bb1b6 100644 --- a/src/frontend-common/imgui_fullscreen.h +++ b/src/frontend-common/imgui_fullscreen.h @@ -1,4 +1,5 @@ #pragma once +#include "IconsFontAwesome5.h" #include "common/types.h" #include "imgui.h" #include "imgui_internal.h" @@ -100,6 +101,11 @@ static ALWAYS_INLINE ImVec4 ModAlpha(const ImVec4& v, float a) return ImVec4(v.x, v.y, v.z, a); } +static ALWAYS_INLINE ImVec4 MulAlpha(const ImVec4& v, float a) +{ + return ImVec4(v.x, v.y, v.z, v.w * a); +} + /// 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 ImRect& fit_rect, const ImVec2& image_size); @@ -107,7 +113,7 @@ ImRect CenterImage(const ImRect& fit_rect, const ImVec2& image_size); /// Initializes, setting up any state. bool Initialize(const char* placeholder_image_path); -void SetTheme(); +void SetTheme(bool light); void SetFonts(ImFont* standard_font, ImFont* medium_font, ImFont* large_font); bool UpdateLayoutScale(); @@ -137,7 +143,7 @@ void PopSecondaryColor(); void DrawWindowTitle(const char* title); -bool BeginFullscreenColumns(const char* title = nullptr); +bool BeginFullscreenColumns(const char* title = nullptr, float pos_y = 0.0f, bool expand_to_screen_width = false); void EndFullscreenColumns(); bool BeginFullscreenColumnWindow(float start, float end, const char* name, @@ -163,6 +169,9 @@ bool ActiveButton(const char* title, bool is_active, bool enabled = true, float height = LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY, ImFont* font = g_large_font); bool MenuButton(const char* title, const char* summary, bool enabled = true, float height = LAYOUT_MENU_BUTTON_HEIGHT, ImFont* font = g_large_font, ImFont* summary_font = g_medium_font); +bool MenuButtonWithoutSummary(const char* title, bool enabled = true, + float height = LAYOUT_MENU_BUTTON_HEIGHT_NO_SUMMARY, ImFont* font = g_large_font, + const ImVec2& text_align = ImVec2(0.0f, 0.0f)); bool MenuButtonWithValue(const char* title, const char* summary, const char* value, bool enabled = true, float height = LAYOUT_MENU_BUTTON_HEIGHT, ImFont* font = g_large_font, ImFont* summary_font = g_medium_font); @@ -236,9 +245,23 @@ void CloseChoiceDialog(); using InputStringDialogCallback = std::function; bool IsInputDialogOpen(); -void OpenInputStringDialog(std::string title, std::string message, std::string caption, std::string ok_button_text, InputStringDialogCallback callback); +void OpenInputStringDialog(std::string title, std::string message, std::string caption, std::string ok_button_text, + InputStringDialogCallback callback); void CloseInputDialog(); +using ConfirmMessageDialogCallback = std::function; +using InfoMessageDialogCallback = std::function; +using MessageDialogCallback = std::function; +bool IsMessageBoxDialogOpen(); +void OpenConfirmMessageDialog(std::string title, std::string message, ConfirmMessageDialogCallback callback, + std::string yes_button_text = ICON_FA_CHECK " Yes", + std::string no_button_text = ICON_FA_TIMES " No"); +void OpenInfoMessageDialog(std::string title, std::string message, InfoMessageDialogCallback callback = {}, + std::string button_text = ICON_FA_WINDOW_CLOSE " Close"); +void OpenMessageDialog(std::string title, std::string message, MessageDialogCallback callback, + std::string first_button_text, std::string second_button_text, std::string third_button_text); +void CloseMessageDialog(); + float GetNotificationVerticalPosition(); float GetNotificationVerticalDirection(); void SetNotificationVerticalPosition(float position, float direction); diff --git a/src/frontend-common/imgui_manager.cpp b/src/frontend-common/imgui_manager.cpp index 232430d20..3a5a78643 100644 --- a/src/frontend-common/imgui_manager.cpp +++ b/src/frontend-common/imgui_manager.cpp @@ -458,18 +458,7 @@ ImFont* ImGuiManager::AddFixedFont(float size) bool ImGuiManager::AddIconFonts(float size) { - static constexpr ImWchar range_fa[] = { - 0xf002, 0xf002, 0xf005, 0xf005, 0xf007, 0xf007, 0xf00c, 0xf00e, 0xf011, 0xf011, 0xf013, 0xf013, 0xf017, 0xf017, - 0xf019, 0xf019, 0xf021, 0xf021, 0xf025, 0xf025, 0xf027, 0xf028, 0xf02d, 0xf02e, 0xf030, 0xf030, 0xf03a, 0xf03a, - 0xf03d, 0xf03d, 0xf049, 0xf04c, 0xf050, 0xf050, 0xf059, 0xf059, 0xf05e, 0xf05e, 0xf065, 0xf065, 0xf067, 0xf067, - 0xf071, 0xf071, 0xf075, 0xf075, 0xf077, 0xf078, 0xf07b, 0xf07c, 0xf084, 0xf085, 0xf091, 0xf091, 0xf0a0, 0xf0a0, - 0xf0ac, 0xf0ad, 0xf0c5, 0xf0c5, 0xf0c7, 0xf0c8, 0xf0cb, 0xf0cb, 0xf0d0, 0xf0d0, 0xf0e2, 0xf0e2, 0xf0eb, 0xf0eb, - 0xf0f1, 0xf0f1, 0xf0f3, 0xf0f3, 0xf0fe, 0xf0fe, 0xf110, 0xf110, 0xf119, 0xf119, 0xf11b, 0xf11c, 0xf140, 0xf140, - 0xf144, 0xf144, 0xf14a, 0xf14a, 0xf15b, 0xf15b, 0xf188, 0xf188, 0xf191, 0xf192, 0xf1dd, 0xf1de, 0xf1e6, 0xf1e6, - 0xf1eb, 0xf1eb, 0xf1f8, 0xf1f8, 0xf242, 0xf242, 0xf245, 0xf245, 0xf26c, 0xf26c, 0xf279, 0xf279, 0xf2d0, 0xf2d0, - 0xf2db, 0xf2db, 0xf2f2, 0xf2f2, 0xf2f5, 0xf2f5, 0xf410, 0xf410, 0xf466, 0xf466, 0xf500, 0xf500, 0xf51f, 0xf51f, - 0xf545, 0xf545, 0xf548, 0xf548, 0xf552, 0xf552, 0xf57a, 0xf57a, 0xf5a2, 0xf5a2, 0xf5e7, 0xf5e7, 0xf65d, 0xf65e, - 0xf6a9, 0xf6a9, 0xf7c2, 0xf7c2, 0xf807, 0xf807, 0xf815, 0xf815, 0xf818, 0xf818, 0xf8cc, 0xf8cc, 0x0, 0x0}; + static constexpr ImWchar range_fa[] = { 0xf002,0xf002,0xf005,0xf005,0xf007,0xf007,0xf00c,0xf00e,0xf011,0xf011,0xf013,0xf013,0xf017,0xf017,0xf019,0xf019,0xf021,0xf021,0xf025,0xf025,0xf027,0xf028,0xf02d,0xf02e,0xf030,0xf030,0xf03a,0xf03a,0xf03d,0xf03d,0xf049,0xf04c,0xf050,0xf050,0xf059,0xf059,0xf05e,0xf05e,0xf065,0xf065,0xf067,0xf067,0xf071,0xf071,0xf075,0xf075,0xf077,0xf078,0xf07b,0xf07c,0xf084,0xf085,0xf091,0xf091,0xf0a0,0xf0a0,0xf0ac,0xf0ad,0xf0c5,0xf0c5,0xf0c7,0xf0c8,0xf0cb,0xf0cb,0xf0d0,0xf0d0,0xf0e2,0xf0e2,0xf0eb,0xf0eb,0xf0f1,0xf0f1,0xf0f3,0xf0f3,0xf0fe,0xf0fe,0xf110,0xf110,0xf119,0xf119,0xf11b,0xf11c,0xf140,0xf140,0xf144,0xf144,0xf14a,0xf14a,0xf15b,0xf15b,0xf188,0xf188,0xf191,0xf192,0xf1dd,0xf1de,0xf1e6,0xf1e6,0xf1eb,0xf1eb,0xf1f8,0xf1f8,0xf1fc,0xf1fc,0xf242,0xf242,0xf245,0xf245,0xf26c,0xf26c,0xf279,0xf279,0xf2d0,0xf2d0,0xf2db,0xf2db,0xf2f2,0xf2f2,0xf2f5,0xf2f5,0xf410,0xf410,0xf466,0xf466,0xf500,0xf500,0xf51f,0xf51f,0xf545,0xf545,0xf548,0xf548,0xf552,0xf552,0xf57a,0xf57a,0xf5a2,0xf5a2,0xf5e7,0xf5e7,0xf65d,0xf65e,0xf6a9,0xf6a9,0xf7c2,0xf7c2,0xf807,0xf807,0xf815,0xf815,0xf818,0xf818,0xf84c,0xf84c,0xf8cc,0xf8cc,0x0,0x0 }; ImFontConfig cfg; cfg.MergeMode = true;