Qt: Add setup wizard

This commit is contained in:
Stenzek 2023-09-02 17:27:34 +10:00
parent 5480e42cd1
commit 4fc984e082
17 changed files with 1475 additions and 53 deletions

View file

@ -131,7 +131,9 @@ std::optional<BIOS::Image> BIOS::LoadImageFromFile(const char* filename)
return std::nullopt;
}
Log_DevPrint(fmt::format("Hash for BIOS '{}': {}", FileSystem::GetDisplayNameFromPath(filename), GetImageHash(ret).ToString()).c_str());
Log_DevPrint(
fmt::format("Hash for BIOS '{}': {}", FileSystem::GetDisplayNameFromPath(filename), GetImageHash(ret).ToString())
.c_str());
return ret;
}
@ -162,7 +164,7 @@ const BIOS::ImageInfo* BIOS::GetInfoForImage(const Image& image, const Hash& has
bool BIOS::IsValidBIOSForRegion(ConsoleRegion console_region, ConsoleRegion bios_region)
{
return (bios_region == ConsoleRegion::Auto || bios_region == console_region);
return (console_region == ConsoleRegion::Auto || bios_region == ConsoleRegion::Auto || bios_region == console_region);
}
void BIOS::PatchBIOS(u8* image, u32 image_size, u32 address, u32 value, u32 mask /*= UINT32_C(0xFFFFFFFF)*/)
@ -293,8 +295,8 @@ std::optional<std::vector<u8>> BIOS::GetBIOSImage(ConsoleRegion region)
std::optional<Image> image = LoadImageFromFile(Path::Combine(EmuFolders::Bios, bios_name).c_str());
if (!image.has_value())
{
Host::ReportFormattedErrorAsync(
"Error", TRANSLATE("HostInterface", "Failed to load configured BIOS file '%s'"), bios_name.c_str());
Host::ReportFormattedErrorAsync("Error", TRANSLATE("HostInterface", "Failed to load configured BIOS file '%s'"),
bios_name.c_str());
return std::nullopt;
}
@ -348,8 +350,7 @@ std::optional<std::vector<u8>> BIOS::FindBIOSImageInDirectory(ConsoleRegion regi
if (!fallback_image.has_value())
{
Host::ReportFormattedErrorAsync("Error",
TRANSLATE("HostInterface", "No BIOS image found for %s region"),
Host::ReportFormattedErrorAsync("Error", TRANSLATE("HostInterface", "No BIOS image found for %s region"),
Settings::GetConsoleRegionDisplayName(region));
return std::nullopt;
}

View file

@ -129,6 +129,9 @@ set(SRCS
settingsdialog.h
settingsdialog.ui
settingwidgetbinder.h
setupwizarddialog.cpp
setupwizarddialog.h
setupwizarddialog.ui
)
if(ENABLE_CHEEVOS)

View file

@ -1,12 +1,15 @@
// SPDX-FileCopyrightText: 2019-2022 Connor McLaughlin <stenzek@gmail.com>
// SPDX-FileCopyrightText: 2019-2023 Connor McLaughlin <stenzek@gmail.com>
// SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0)
#include "biossettingswidget.h"
#include "core/bios.h"
#include "qthost.h"
#include "qtutils.h"
#include "settingsdialog.h"
#include "settingwidgetbinder.h"
#include "core/bios.h"
#include "core/settings.h"
#include <QtWidgets/QFileDialog>
#include <algorithm>
@ -22,9 +25,8 @@ BIOSSettingsWidget::BIOSSettingsWidget(SettingsDialog* dialog, QWidget* parent)
dialog->registerWidgetHelp(m_ui.fastBoot, tr("Fast Boot"), tr("Unchecked"),
tr("Patches the BIOS to skip the console's boot animation. Does not work with all games, "
"but usually safe to enable."));
dialog->registerWidgetHelp(
m_ui.enableTTYLogging, tr("Enable TTY Logging"), tr("Unchecked"),
tr("Logs BIOS calls to printf(). Not all games contain debugging messages."));
dialog->registerWidgetHelp(m_ui.enableTTYLogging, tr("Enable TTY Logging"), tr("Unchecked"),
tr("Logs BIOS calls to printf(). Not all games contain debugging messages."));
connect(m_ui.imageNTSCJ, QOverload<int>::of(&QComboBox::currentIndexChanged), [this](int index) {
if (m_dialog->isPerGameSettings() && index == 0)
@ -72,25 +74,34 @@ BIOSSettingsWidget::BIOSSettingsWidget(SettingsDialog* dialog, QWidget* parent)
BIOSSettingsWidget::~BIOSSettingsWidget() = default;
std::vector<std::pair<std::string, const BIOS::ImageInfo*>> BIOSSettingsWidget::getList(const char* directory)
{
return BIOS::FindBIOSImagesInDirectory(directory);
}
void BIOSSettingsWidget::refreshList()
{
auto images = BIOS::FindBIOSImagesInDirectory(m_ui.searchDirectory->text().toUtf8().constData());
populateDropDownForRegion(ConsoleRegion::NTSC_J, m_ui.imageNTSCJ, images);
populateDropDownForRegion(ConsoleRegion::NTSC_U, m_ui.imageNTSCU, images);
populateDropDownForRegion(ConsoleRegion::PAL, m_ui.imagePAL, images);
auto images = getList(m_ui.searchDirectory->text().toUtf8().constData());
populateDropDownForRegion(ConsoleRegion::NTSC_J, m_ui.imageNTSCJ, images, m_dialog->isPerGameSettings());
populateDropDownForRegion(ConsoleRegion::NTSC_U, m_ui.imageNTSCU, images, m_dialog->isPerGameSettings());
populateDropDownForRegion(ConsoleRegion::PAL, m_ui.imagePAL, images, m_dialog->isPerGameSettings());
setDropDownValue(m_ui.imageNTSCJ, m_dialog->getStringValue("BIOS", "PathNTSCJ", std::nullopt));
setDropDownValue(m_ui.imageNTSCU, m_dialog->getStringValue("BIOS", "PathNTSCU", std::nullopt));
setDropDownValue(m_ui.imagePAL, m_dialog->getStringValue("BIOS", "PathPAL", std::nullopt));
setDropDownValue(m_ui.imageNTSCJ, m_dialog->getStringValue("BIOS", "PathNTSCJ", std::nullopt),
m_dialog->isPerGameSettings());
setDropDownValue(m_ui.imageNTSCU, m_dialog->getStringValue("BIOS", "PathNTSCU", std::nullopt),
m_dialog->isPerGameSettings());
setDropDownValue(m_ui.imagePAL, m_dialog->getStringValue("BIOS", "PathPAL", std::nullopt),
m_dialog->isPerGameSettings());
}
void BIOSSettingsWidget::populateDropDownForRegion(ConsoleRegion region, QComboBox* cb,
std::vector<std::pair<std::string, const BIOS::ImageInfo*>>& images)
std::vector<std::pair<std::string, const BIOS::ImageInfo*>>& images,
bool per_game)
{
QSignalBlocker sb(cb);
cb->clear();
if (m_dialog->isPerGameSettings())
if (per_game)
cb->addItem(QIcon(QStringLiteral(":/icons/system-search.png")), tr("Use Global Setting"));
cb->addItem(QIcon(QStringLiteral(":/icons/system-search.png")), tr("Auto-Detect"));
@ -117,13 +128,13 @@ void BIOSSettingsWidget::populateDropDownForRegion(ConsoleRegion region, QComboB
}
}
void BIOSSettingsWidget::setDropDownValue(QComboBox* cb, const std::optional<std::string>& name)
void BIOSSettingsWidget::setDropDownValue(QComboBox* cb, const std::optional<std::string>& name, bool per_game)
{
QSignalBlocker sb(cb);
if (!name.has_value() || name->empty())
{
cb->setCurrentIndex((m_dialog->isPerGameSettings() && name.has_value()) ? 1 : 0);
cb->setCurrentIndex((per_game && name.has_value()) ? 1 : 0);
return;
}

View file

@ -22,14 +22,16 @@ public:
explicit BIOSSettingsWidget(SettingsDialog* dialog, QWidget* parent);
~BIOSSettingsWidget();
static void populateDropDownForRegion(ConsoleRegion region, QComboBox* cb,
std::vector<std::pair<std::string, const BIOS::ImageInfo*>>& images,
bool per_game);
static void setDropDownValue(QComboBox* cb, const std::optional<std::string>& name, bool per_game);
static std::vector<std::pair<std::string, const BIOS::ImageInfo*>> getList(const char* directory);
private Q_SLOTS:
void refreshList();
private:
void populateDropDownForRegion(ConsoleRegion region, QComboBox* cb,
std::vector<std::pair<std::string, const BIOS::ImageInfo*>>& images);
void setDropDownValue(QComboBox* cb, const std::optional<std::string>& name);
Ui::BIOSSettingsWidget m_ui;
SettingsDialog* m_dialog;

View file

@ -51,8 +51,10 @@
<ClCompile Include="qtprogresscallback.cpp" />
<ClCompile Include="qtutils.cpp" />
<ClCompile Include="settingsdialog.cpp" />
<ClCompile Include="setupwizarddialog.cpp" />
</ItemGroup>
<ItemGroup>
<QtMoc Include="setupwizarddialog.h" />
<QtMoc Include="aboutdialog.h" />
<QtMoc Include="audiosettingswidget.h" />
<QtMoc Include="biossettingswidget.h" />
@ -264,6 +266,7 @@
<ClCompile Include="$(IntDir)moc_qthost.cpp" />
<ClCompile Include="$(IntDir)moc_qtprogresscallback.cpp" />
<ClCompile Include="$(IntDir)moc_settingsdialog.cpp" />
<ClCompile Include="$(IntDir)moc_setupwizarddialog.cpp" />
<ClCompile Include="$(IntDir)qrc_resources.cpp" />
</ItemGroup>
<ItemGroup>
@ -323,6 +326,9 @@
<QtUi Include="controllerledsettingsdialog.ui">
<FileType>Document</FileType>
</QtUi>
<QtUi Include="setupwizarddialog.ui">
<FileType>Document</FileType>
</QtUi>
<None Include="translations\duckstation-qt_es-es.ts" />
<None Include="translations\duckstation-qt_tr.ts" />
</ItemGroup>
@ -382,4 +388,4 @@
</ItemDefinitionGroup>
<Import Project="..\..\dep\msvc\vsprops\Targets.props" />
<Import Project="..\..\dep\msvc\vsprops\QtCompile.targets" />
</Project>
</Project>

View file

@ -92,6 +92,7 @@
<ClCompile Include="colorpickerbutton.cpp" />
<ClCompile Include="$(IntDir)moc_colorpickerbutton.cpp" />
<ClCompile Include="pch.cpp" />
<ClCompile Include="setupwizarddialog.cpp" />
</ItemGroup>
<ItemGroup>
<ClInclude Include="qtutils.h" />
@ -152,6 +153,7 @@
<QtMoc Include="foldersettingswidget.h" />
<QtMoc Include="coverdownloaddialog.h" />
<QtMoc Include="colorpickerbutton.h" />
<QtMoc Include="setupwizarddialog.h" />
</ItemGroup>
<ItemGroup>
<QtUi Include="consolesettingswidget.ui" />
@ -192,6 +194,7 @@
<QtUi Include="controllerbindingwidget_mouse.ui" />
<QtUi Include="coverdownloaddialog.ui" />
<QtUi Include="controllerledsettingsdialog.ui" />
<QtUi Include="setupwizarddialog.ui" />
</ItemGroup>
<ItemGroup>
<Natvis Include="qt5.natvis" />

View file

@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: 2019-2022 Connor McLaughlin <stenzek@gmail.com>
// SPDX-FileCopyrightText: 2019-2023 Connor McLaughlin <stenzek@gmail.com>
// SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0)
#include "generalsettingswidget.h"
@ -10,6 +10,21 @@
#include "settingsdialog.h"
#include "settingwidgetbinder.h"
const char* GeneralSettingsWidget::THEME_NAMES[] = {
QT_TRANSLATE_NOOP("MainWindow", "Native"),
QT_TRANSLATE_NOOP("MainWindow", "Fusion"),
QT_TRANSLATE_NOOP("MainWindow", "Dark Fusion (Gray)"),
QT_TRANSLATE_NOOP("MainWindow", "Dark Fusion (Blue)"),
QT_TRANSLATE_NOOP("MainWindow", "QDarkStyle"),
nullptr,
};
const char* GeneralSettingsWidget::THEME_VALUES[] = {
"", "fusion", "darkfusion", "darkfusionblue", "qdarkstyle", nullptr,
};
const char* GeneralSettingsWidget::DEFAULT_THEME_NAME = "darkfusion";
GeneralSettingsWidget::GeneralSettingsWidget(SettingsDialog* dialog, QWidget* parent)
: QWidget(parent), m_dialog(dialog)
{

View file

@ -24,4 +24,9 @@ private:
Ui::GeneralSettingsWidget m_ui;
SettingsDialog* m_dialog;
public:
static const char* THEME_NAMES[];
static const char* THEME_VALUES[];
static const char* DEFAULT_THEME_NAME;
};

View file

@ -10,6 +10,7 @@
#include "displaywidget.h"
#include "gamelistsettingswidget.h"
#include "gamelistwidget.h"
#include "generalsettingswidget.h"
#include "memorycardeditordialog.h"
#include "qthost.h"
#include "qtutils.h"
@ -64,8 +65,6 @@ static constexpr char DISC_IMAGE_FILTER[] = QT_TRANSLATE_NOOP(
"(*.ecm);;Media Descriptor Sidecar Images (*.mds);;PlayStation EBOOTs (*.pbp *.PBP);;PlayStation Executables (*.exe "
"*.psexe *.ps-exe);;Portable Sound Format Files (*.psf *.minipsf);;Playlists (*.m3u)");
const char* DEFAULT_THEME_NAME = "darkfusion";
MainWindow* g_main_window = nullptr;
static QString s_unthemed_style_name;
static bool s_unthemed_style_name_set;
@ -2030,20 +2029,17 @@ void MainWindow::connectSignals()
SettingWidgetBinder::BindWidgetToBoolSetting(nullptr, m_ui.actionDebugShowMDECState, "Debug", "ShowMDECState", false);
SettingWidgetBinder::BindWidgetToBoolSetting(nullptr, m_ui.actionDebugShowDMAState, "Debug", "ShowDMAState", false);
addThemeToMenu(tr("Default"), QStringLiteral("default"));
addThemeToMenu(tr("Fusion"), QStringLiteral("fusion"));
addThemeToMenu(tr("Dark Fusion (Gray)"), QStringLiteral("darkfusion"));
addThemeToMenu(tr("Dark Fusion (Blue)"), QStringLiteral("darkfusionblue"));
addThemeToMenu(tr("QDarkStyle"), QStringLiteral("qdarkstyle"));
updateMenuSelectedTheme();
}
for (u32 i = 0; GeneralSettingsWidget::THEME_NAMES[i]; i++)
{
const QString key = QString::fromUtf8(GeneralSettingsWidget::THEME_NAMES[i]);
QAction* action =
m_ui.menuSettingsTheme->addAction(qApp->translate("MainWindow", GeneralSettingsWidget::THEME_NAMES[i]));
action->setCheckable(true);
action->setData(key);
connect(action, &QAction::toggled, [this, key](bool) { setTheme(key); });
}
void MainWindow::addThemeToMenu(const QString& name, const QString& key)
{
QAction* action = m_ui.menuSettingsTheme->addAction(name);
action->setCheckable(true);
action->setData(key);
connect(action, &QAction::toggled, [this, key](bool) { setTheme(key); });
updateMenuSelectedTheme();
}
void MainWindow::setTheme(const QString& theme)
@ -2062,7 +2058,7 @@ void MainWindow::updateTheme()
void MainWindow::setStyleFromSettings()
{
const std::string theme(Host::GetBaseStringSettingValue("UI", "Theme", DEFAULT_THEME_NAME));
const std::string theme(Host::GetBaseStringSettingValue("UI", "Theme", GeneralSettingsWidget::DEFAULT_THEME_NAME));
// setPalette() shouldn't be necessary, as the documentation claims that setStyle() resets the palette, but it
// is here, to work around a bug in 6.4.x and 6.5.x where the palette doesn't restore after changing themes.
@ -2332,7 +2328,8 @@ void MainWindow::updateDebugMenuCropMode()
void MainWindow::updateMenuSelectedTheme()
{
QString theme = QString::fromStdString(Host::GetBaseStringSettingValue("UI", "Theme", DEFAULT_THEME_NAME));
QString theme =
QString::fromStdString(Host::GetBaseStringSettingValue("UI", "Theme", GeneralSettingsWidget::DEFAULT_THEME_NAME));
for (QObject* obj : m_ui.menuSettingsTheme->children())
{

View file

@ -188,7 +188,6 @@ private:
static void setIconThemeFromSettings();
void setupAdditionalUi();
void connectSignals();
void addThemeToMenu(const QString& name, const QString& key);
void updateEmulationActions(bool starting, bool running, bool cheevos_challenge_mode);
void updateStatusBarWidgetVisibility();

View file

@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: 2019-2022 Connor McLaughlin <stenzek@gmail.com>
// SPDX-FileCopyrightText: 2019-2023 Connor McLaughlin <stenzek@gmail.com>
// SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0)
#include "qthost.h"
@ -6,6 +6,7 @@
#include "mainwindow.h"
#include "qtprogresscallback.h"
#include "qtutils.h"
#include "setupwizarddialog.h"
#include "core/achievements.h"
#include "core/cheats.h"
@ -86,6 +87,7 @@ static void SetDataDirectory();
static bool SetCriticalFolders();
static void SetDefaultSettings(SettingsInterface& si, bool system, bool controller);
static void SaveSettings();
static bool RunSetupWizard();
static void InitializeEarlyConsole();
static void HookSignals();
static void PrintCommandLineVersion();
@ -100,6 +102,7 @@ static bool s_batch_mode = false;
static bool s_nogui_mode = false;
static bool s_start_fullscreen_ui = false;
static bool s_start_fullscreen_ui_fullscreen = false;
static bool s_run_setup_wizard = false;
EmuThread* g_emu_thread;
GDBServer* g_gdb_server;
@ -161,6 +164,7 @@ bool QtHost::InitializeConfig(std::string settings_filename)
settings_filename = Path::Combine(EmuFolders::DataRoot, "settings.ini");
Log_InfoPrintf("Loading config from %s.", settings_filename.c_str());
s_run_setup_wizard = s_run_setup_wizard || !FileSystem::FileExists(settings_filename.c_str());
s_base_settings_interface = std::make_unique<INISettingsInterface>(std::move(settings_filename));
Host::Internal::SetBaseSettingsLayer(s_base_settings_interface.get());
@ -179,9 +183,16 @@ bool QtHost::InitializeConfig(std::string settings_filename)
s_base_settings_interface->SetUIntValue("Main", "SettingsVersion", SETTINGS_VERSION);
s_base_settings_interface->SetBoolValue("ControllerPorts", "ControllerSettingsMigrated", true);
SetDefaultSettings(*s_base_settings_interface, true, true);
s_base_settings_interface->Save();
// Don't save if we're running the setup wizard. We want to run it next time if they don't finish it.
if (!s_run_setup_wizard)
s_base_settings_interface->Save();
}
// Setup wizard was incomplete last time?
s_run_setup_wizard =
s_run_setup_wizard || s_base_settings_interface->GetBoolValue("Main", "SetupWizardIncomplete", false);
EmuFolders::LoadConfig(*s_base_settings_interface.get());
EmuFolders::EnsureFoldersExist();
@ -1879,6 +1890,11 @@ bool QtHost::ParseCommandLineParametersAndInitializeConfig(QApplication& app,
s_start_fullscreen_ui = true;
continue;
}
else if (CHECK_ARG("-setupwizard"))
{
s_run_setup_wizard = true;
continue;
}
else if (CHECK_ARG("-earlyconsole"))
{
InitializeEarlyConsole();
@ -1975,6 +1991,22 @@ bool QtHost::ParseCommandLineParametersAndInitializeConfig(QApplication& app,
return true;
}
bool QtHost::RunSetupWizard()
{
// Set a flag in the config so that even though we created the ini, we'll run the wizard next time.
Host::SetBaseBoolSettingValue("Main", "SetupWizardIncomplete", true);
Host::CommitBaseSettingChanges();
SetupWizardDialog dialog;
if (dialog.exec() == QDialog::Rejected)
return false;
// Remove the flag.
Host::SetBaseBoolSettingValue("Main", "SetupWizardIncomplete", false);
Host::CommitBaseSettingChanges();
return true;
}
int main(int argc, char* argv[])
{
CrashHandler::Install();
@ -1992,11 +2024,20 @@ int main(int argc, char* argv[])
MainWindow::updateApplicationTheme();
// Start up the CPU thread.
MainWindow* main_window = new MainWindow();
QtHost::HookSignals();
EmuThread::start();
// Optionally run setup wizard.
MainWindow* main_window;
int result;
if (s_run_setup_wizard && !QtHost::RunSetupWizard())
{
result = EXIT_FAILURE;
goto shutdown_and_exit;
}
// Create all window objects, the emuthread might still be starting up at this point.
main_window = new MainWindow();
main_window->initialize();
// When running in batch mode, ensure game list is loaded, but don't scan for any new files.
@ -2022,8 +2063,9 @@ int main(int argc, char* argv[])
main_window->startupUpdateCheck();
// This doesn't return until we exit.
const int result = app.exec();
result = app.exec();
shutdown_and_exit:
// Shutting down.
EmuThread::stop();

View file

@ -252,6 +252,9 @@ void RunOnUIThread(const std::function<void()>& func, bool block = false);
/// Returns a list of supported languages and codes (suffixes for translation files).
std::vector<std::pair<QString, QString>> GetAvailableLanguageList();
/// Default language for the platform.
const char* GetDefaultLanguage();
/// Call when the language changes.
void InstallTranslator();

View file

@ -67,7 +67,8 @@ void QtHost::InstallTranslator()
}
s_translators.clear();
const QString language(QString::fromStdString(Host::GetBaseStringSettingValue("Main", "Language", "en")));
const QString language(
QString::fromStdString(Host::GetBaseStringSettingValue("Main", "Language", GetDefaultLanguage())));
// install the base qt translation first
const QString base_dir(QStringLiteral("%1/translations").arg(qApp->applicationDirPath()));
@ -193,6 +194,12 @@ std::vector<std::pair<QString, QString>> QtHost::GetAvailableLanguageList()
{QStringLiteral("简体中文"), QStringLiteral("zh-cn")}};
}
const char* QtHost::GetDefaultLanguage()
{
// TODO: Default system language instead.
return "en";
}
static constexpr const ImWchar s_base_latin_range[] = {
0x0020, 0x00FF, // Basic Latin + Latin Supplement
};

View file

@ -963,14 +963,18 @@ static void BindWidgetToEnumSetting(SettingsInterface* sif, WidgetType* widget,
template<typename WidgetType>
static void BindWidgetToEnumSetting(SettingsInterface* sif, WidgetType* widget, std::string section, std::string key,
const char** enum_names, const char** enum_values, const char* default_value)
const char** enum_names, const char** enum_values, const char* default_value,
const char* translation_ctx = nullptr)
{
using Accessor = SettingAccessor<WidgetType>;
const std::string value = Host::GetBaseStringSettingValue(section.c_str(), key.c_str(), default_value);
for (int i = 0; enum_names[i] != nullptr; i++)
widget->addItem(QString::fromUtf8(enum_names[i]));
{
widget->addItem(translation_ctx ? qApp->translate(translation_ctx, enum_names[i]) :
QString::fromUtf8(enum_names[i]));
}
int enum_index = -1;
for (int i = 0; enum_values[i] != nullptr; i++)

View file

@ -0,0 +1,508 @@
// SPDX-FileCopyrightText: 2019-2023 Connor McLaughlin <stenzek@gmail.com>.
// SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0)
#include "setupwizarddialog.h"
#include "controllersettingwidgetbinder.h"
#include "generalsettingswidget.h"
#include "mainwindow.h"
#include "qthost.h"
#include "qtutils.h"
#include "settingwidgetbinder.h"
#include "core/controller.h"
#include "util/input_manager.h"
#include "common/file_system.h"
#include <QtWidgets/QMessageBox>
SetupWizardDialog::SetupWizardDialog()
{
setupUi();
updatePageLabels(-1);
updatePageButtons();
}
SetupWizardDialog::~SetupWizardDialog() = default;
void SetupWizardDialog::resizeEvent(QResizeEvent* event)
{
QDialog::resizeEvent(event);
resizeDirectoryListColumns();
}
bool SetupWizardDialog::canShowNextPage()
{
const int current_page = m_ui.pages->currentIndex();
switch (current_page)
{
case Page_BIOS:
{
if (!BIOS::HasAnyBIOSImages())
{
if (QMessageBox::question(
this, tr("Warning"),
tr("No BIOS images were found. DuckStation <strong>will not</strong> be able to run games without a "
"BIOS image.<br><br>Are you sure you wish to continue without selecting a BIOS image?")) !=
QMessageBox::Yes)
{
return false;
}
}
}
break;
case Page_GameList:
{
if (m_ui.searchDirectoryList->rowCount() == 0)
{
if (QMessageBox::question(
this, tr("Warning"),
tr("No game directories have been selected. You will have to manually open any game dumps you "
"want to play, DuckStation's list will be empty.\n\nAre you sure you want to continue?")) !=
QMessageBox::Yes)
{
return false;
}
}
}
break;
default:
break;
}
return true;
}
void SetupWizardDialog::previousPage()
{
const int current_page = m_ui.pages->currentIndex();
if (current_page == 0)
return;
m_ui.pages->setCurrentIndex(current_page - 1);
updatePageLabels(current_page);
updatePageButtons();
}
void SetupWizardDialog::nextPage()
{
const int current_page = m_ui.pages->currentIndex();
if (current_page == Page_Complete)
{
accept();
return;
}
if (!canShowNextPage())
return;
const int new_page = current_page + 1;
m_ui.pages->setCurrentIndex(new_page);
updatePageLabels(current_page);
updatePageButtons();
pageChangedTo(new_page);
}
void SetupWizardDialog::pageChangedTo(int page)
{
switch (page)
{
case Page_GameList:
resizeDirectoryListColumns();
break;
default:
break;
}
}
void SetupWizardDialog::updatePageLabels(int prev_page)
{
if (prev_page >= 0)
{
QFont prev_font = m_page_labels[prev_page]->font();
prev_font.setBold(false);
m_page_labels[prev_page]->setFont(prev_font);
}
const int page = m_ui.pages->currentIndex();
QFont font = m_page_labels[page]->font();
font.setBold(true);
m_page_labels[page]->setFont(font);
}
void SetupWizardDialog::updatePageButtons()
{
const int page = m_ui.pages->currentIndex();
m_ui.next->setText((page == Page_Complete) ? tr("&Finish") : tr("&Next"));
m_ui.back->setEnabled(page > 0);
}
void SetupWizardDialog::confirmCancel()
{
if (QMessageBox::question(this, tr("Cancel Setup"),
tr("Are you sure you want to cancel DuckStation setup?\n\nAny changes have been saved, and "
"the wizard will run again next time you start DuckStation.")) != QMessageBox::Yes)
{
return;
}
reject();
}
void SetupWizardDialog::setupUi()
{
m_ui.setupUi(this);
m_ui.logo->setPixmap(
QPixmap(QString::fromUtf8(Path::Combine(EmuFolders::Resources, "images" FS_OSPATH_SEPARATOR_STR "duck.png"))));
m_ui.pages->setCurrentIndex(0);
m_page_labels[Page_Language] = m_ui.labelLanguage;
m_page_labels[Page_BIOS] = m_ui.labelBIOS;
m_page_labels[Page_GameList] = m_ui.labelGameList;
m_page_labels[Page_Controller] = m_ui.labelController;
m_page_labels[Page_Complete] = m_ui.labelComplete;
connect(m_ui.back, &QPushButton::clicked, this, &SetupWizardDialog::previousPage);
connect(m_ui.next, &QPushButton::clicked, this, &SetupWizardDialog::nextPage);
connect(m_ui.cancel, &QPushButton::clicked, this, &SetupWizardDialog::confirmCancel);
setupLanguagePage();
setupBIOSPage();
setupGameListPage();
setupControllerPage(true);
}
void SetupWizardDialog::setupLanguagePage()
{
SettingWidgetBinder::BindWidgetToEnumSetting(nullptr, m_ui.theme, "UI", "Theme", GeneralSettingsWidget::THEME_NAMES,
GeneralSettingsWidget::THEME_VALUES,
GeneralSettingsWidget::DEFAULT_THEME_NAME, "InterfaceSettingsWidget");
connect(m_ui.theme, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &SetupWizardDialog::themeChanged);
for (const std::pair<QString, QString>& it : QtHost::GetAvailableLanguageList())
m_ui.language->addItem(it.first, it.second);
SettingWidgetBinder::BindWidgetToStringSetting(nullptr, m_ui.language, "Main", "Language",
QtHost::GetDefaultLanguage());
connect(m_ui.language, QOverload<int>::of(&QComboBox::currentIndexChanged), this,
&SetupWizardDialog::languageChanged);
SettingWidgetBinder::BindWidgetToBoolSetting(nullptr, m_ui.autoUpdateEnabled, "AutoUpdater", "CheckAtStartup", true);
}
void SetupWizardDialog::themeChanged()
{
// Main window gets recreated at the end here anyway, so it's fine to just yolo it.
MainWindow::updateApplicationTheme();
}
void SetupWizardDialog::languageChanged()
{
// Skip the recreation, since we don't have many dynamic UI elements.
QtHost::InstallTranslator();
m_ui.retranslateUi(this);
setupControllerPage(false);
}
void SetupWizardDialog::setupBIOSPage()
{
SettingWidgetBinder::BindWidgetToFolderSetting(nullptr, m_ui.biosSearchDirectory, m_ui.browseBiosSearchDirectory,
m_ui.openBiosSearchDirectory, m_ui.resetBiosSearchDirectory, "BIOS",
"SearchDirectory", Path::Combine(EmuFolders::DataRoot, "bios"));
refreshBiosList();
connect(m_ui.biosSearchDirectory, &QLineEdit::textChanged, this, &SetupWizardDialog::refreshBiosList);
connect(m_ui.refreshBiosList, &QPushButton::clicked, this, &SetupWizardDialog::refreshBiosList);
}
void SetupWizardDialog::refreshBiosList()
{
auto list = BIOSSettingsWidget::getList(m_ui.biosSearchDirectory->text().toUtf8().constData());
BIOSSettingsWidget::populateDropDownForRegion(ConsoleRegion::NTSC_U, m_ui.imageNTSCU, list, false);
BIOSSettingsWidget::populateDropDownForRegion(ConsoleRegion::NTSC_J, m_ui.imageNTSCJ, list, false);
BIOSSettingsWidget::populateDropDownForRegion(ConsoleRegion::PAL, m_ui.imagePAL, list, false);
BIOSSettingsWidget::setDropDownValue(m_ui.imageNTSCU, Host::GetBaseStringSettingValue("BIOS", "PathNTSCU"), false);
BIOSSettingsWidget::setDropDownValue(m_ui.imageNTSCJ, Host::GetBaseStringSettingValue("BIOS", "PathNTSCJ"), false);
BIOSSettingsWidget::setDropDownValue(m_ui.imagePAL, Host::GetBaseStringSettingValue("BIOS", "PathPAL"), false);
}
void SetupWizardDialog::setupGameListPage()
{
m_ui.searchDirectoryList->setSelectionMode(QAbstractItemView::SingleSelection);
m_ui.searchDirectoryList->setSelectionBehavior(QAbstractItemView::SelectRows);
m_ui.searchDirectoryList->setAlternatingRowColors(true);
m_ui.searchDirectoryList->setShowGrid(false);
m_ui.searchDirectoryList->horizontalHeader()->setHighlightSections(false);
m_ui.searchDirectoryList->verticalHeader()->hide();
m_ui.searchDirectoryList->setCurrentIndex({});
m_ui.searchDirectoryList->setContextMenuPolicy(Qt::ContextMenuPolicy::CustomContextMenu);
connect(m_ui.searchDirectoryList, &QTableWidget::customContextMenuRequested, this,
&SetupWizardDialog::onDirectoryListContextMenuRequested);
connect(m_ui.addSearchDirectoryButton, &QPushButton::clicked, this,
&SetupWizardDialog::onAddSearchDirectoryButtonClicked);
connect(m_ui.removeSearchDirectoryButton, &QPushButton::clicked, this,
&SetupWizardDialog::onRemoveSearchDirectoryButtonClicked);
refreshDirectoryList();
}
void SetupWizardDialog::onDirectoryListContextMenuRequested(const QPoint& point)
{
QModelIndexList selection = m_ui.searchDirectoryList->selectionModel()->selectedIndexes();
if (selection.size() < 1)
return;
const int row = selection[0].row();
QMenu menu;
menu.addAction(tr("Remove"), [this]() { onRemoveSearchDirectoryButtonClicked(); });
menu.addSeparator();
menu.addAction(tr("Open Directory..."), [this, row]() {
QtUtils::OpenURL(this, QUrl::fromLocalFile(m_ui.searchDirectoryList->item(row, 0)->text()));
});
menu.exec(m_ui.searchDirectoryList->mapToGlobal(point));
}
void SetupWizardDialog::onAddSearchDirectoryButtonClicked()
{
QString dir = QDir::toNativeSeparators(QFileDialog::getExistingDirectory(this, tr("Select Search Directory")));
if (dir.isEmpty())
return;
QMessageBox::StandardButton selection =
QMessageBox::question(this, tr("Scan Recursively?"),
tr("Would you like to scan the directory \"%1\" recursively?\n\nScanning recursively takes "
"more time, but will identify files in subdirectories.")
.arg(dir),
QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel);
if (selection == QMessageBox::Cancel)
return;
const bool recursive = (selection == QMessageBox::Yes);
const std::string spath = dir.toStdString();
Host::RemoveValueFromBaseStringListSetting("GameList", recursive ? "Paths" : "RecursivePaths", spath.c_str());
Host::AddValueToBaseStringListSetting("GameList", recursive ? "RecursivePaths" : "Paths", spath.c_str());
Host::CommitBaseSettingChanges();
refreshDirectoryList();
}
void SetupWizardDialog::onRemoveSearchDirectoryButtonClicked()
{
const int row = m_ui.searchDirectoryList->currentRow();
std::unique_ptr<QTableWidgetItem> item((row >= 0) ? m_ui.searchDirectoryList->takeItem(row, 0) : nullptr);
if (!item)
return;
const std::string spath = item->text().toStdString();
if (!Host::RemoveValueFromBaseStringListSetting("GameList", "Paths", spath.c_str()) &&
!Host::RemoveValueFromBaseStringListSetting("GameList", "RecursivePaths", spath.c_str()))
{
return;
}
Host::CommitBaseSettingChanges();
refreshDirectoryList();
}
void SetupWizardDialog::addPathToTable(const std::string& path, bool recursive)
{
const int row = m_ui.searchDirectoryList->rowCount();
m_ui.searchDirectoryList->insertRow(row);
QTableWidgetItem* item = new QTableWidgetItem();
item->setText(QString::fromStdString(path));
item->setFlags(item->flags() & ~(Qt::ItemIsEditable));
m_ui.searchDirectoryList->setItem(row, 0, item);
QCheckBox* cb = new QCheckBox(m_ui.searchDirectoryList);
m_ui.searchDirectoryList->setCellWidget(row, 1, cb);
cb->setChecked(recursive);
connect(cb, &QCheckBox::stateChanged, [item](int state) {
const std::string path(item->text().toStdString());
if (state == Qt::Checked)
{
Host::RemoveValueFromBaseStringListSetting("GameList", "Paths", path.c_str());
Host::AddValueToBaseStringListSetting("GameList", "RecursivePaths", path.c_str());
}
else
{
Host::RemoveValueFromBaseStringListSetting("GameList", "RecursivePaths", path.c_str());
Host::AddValueToBaseStringListSetting("GameList", "Paths", path.c_str());
}
Host::CommitBaseSettingChanges();
});
}
void SetupWizardDialog::refreshDirectoryList()
{
QSignalBlocker sb(m_ui.searchDirectoryList);
while (m_ui.searchDirectoryList->rowCount() > 0)
m_ui.searchDirectoryList->removeRow(0);
std::vector<std::string> path_list = Host::GetBaseStringListSetting("GameList", "Paths");
for (const std::string& entry : path_list)
addPathToTable(entry, false);
path_list = Host::GetBaseStringListSetting("GameList", "RecursivePaths");
for (const std::string& entry : path_list)
addPathToTable(entry, true);
m_ui.searchDirectoryList->sortByColumn(0, Qt::AscendingOrder);
}
void SetupWizardDialog::resizeDirectoryListColumns()
{
QtUtils::ResizeColumnsForTableView(m_ui.searchDirectoryList, {-1, 100});
}
void SetupWizardDialog::setupControllerPage(bool initial)
{
static constexpr u32 NUM_PADS = 2;
struct PadWidgets
{
QComboBox* type_combo;
QLabel* mapping_result;
QToolButton* mapping_button;
};
const PadWidgets pad_widgets[NUM_PADS] = {
{m_ui.controller1Type, m_ui.controller1Mapping, m_ui.controller1AutomaticMapping},
{m_ui.controller2Type, m_ui.controller2Mapping, m_ui.controller2AutomaticMapping},
};
if (!initial)
{
for (const PadWidgets& w : pad_widgets)
{
w.type_combo->blockSignals(true);
w.type_combo->clear();
}
}
for (u32 port = 0; port < NUM_PADS; port++)
{
const std::string section = fmt::format("Pad{}", port + 1);
const PadWidgets& w = pad_widgets[port];
for (u32 i = 0; i < static_cast<u32>(ControllerType::Count); i++)
{
const ControllerType ctype = static_cast<ControllerType>(i);
const Controller::ControllerInfo* cinfo = Controller::GetControllerInfo(ctype);
if (!cinfo)
continue;
w.type_combo->addItem(qApp->translate("ControllerType", cinfo->display_name), QString::fromUtf8(cinfo->name));
}
ControllerSettingWidgetBinder::BindWidgetToInputProfileString(
nullptr, w.type_combo, section, "Type", Controller::GetControllerInfo(Controller::GetDefaultPadType(port))->name);
w.mapping_result->setText((port == 0) ? tr("Default (Keyboard)") : tr("Default (None)"));
if (initial)
{
connect(w.mapping_button, &QAbstractButton::clicked, this,
[this, port, label = w.mapping_result]() { openAutomaticMappingMenu(port, label); });
}
}
if (initial)
{
// Trigger enumeration to populate the device list.
connect(g_emu_thread, &EmuThread::onInputDevicesEnumerated, this, &SetupWizardDialog::onInputDevicesEnumerated);
connect(g_emu_thread, &EmuThread::onInputDeviceConnected, this, &SetupWizardDialog::onInputDeviceConnected);
connect(g_emu_thread, &EmuThread::onInputDeviceDisconnected, this, &SetupWizardDialog::onInputDeviceDisconnected);
g_emu_thread->enumerateInputDevices();
}
if (!initial)
{
for (const PadWidgets& w : pad_widgets)
w.type_combo->blockSignals(false);
}
}
void SetupWizardDialog::openAutomaticMappingMenu(u32 port, QLabel* update_label)
{
QMenu menu(this);
bool added = false;
for (const QPair<QString, QString>& dev : m_device_list)
{
// we set it as data, because the device list could get invalidated while the menu is up
QAction* action = menu.addAction(QStringLiteral("%1 (%2)").arg(dev.first).arg(dev.second));
action->setData(dev.first);
connect(action, &QAction::triggered, this, [this, port, update_label, action]() {
doDeviceAutomaticBinding(port, update_label, action->data().toString());
});
added = true;
}
if (!added)
{
QAction* action = menu.addAction(tr("No devices available"));
action->setEnabled(false);
}
menu.exec(QCursor::pos());
}
void SetupWizardDialog::doDeviceAutomaticBinding(u32 port, QLabel* update_label, const QString& device)
{
std::vector<std::pair<GenericInputBinding, std::string>> mapping =
InputManager::GetGenericBindingMapping(device.toStdString());
if (mapping.empty())
{
QMessageBox::critical(
this, tr("Automatic Binding"),
tr("No generic bindings were generated for device '%1'. The controller/source may not support automatic "
"mapping.")
.arg(device));
return;
}
bool result;
{
auto lock = Host::GetSettingsLock();
result = InputManager::MapController(*Host::Internal::GetBaseSettingsLayer(), port, mapping);
}
if (!result)
return;
Host::CommitBaseSettingChanges();
update_label->setText(device);
}
void SetupWizardDialog::onInputDevicesEnumerated(const QList<QPair<QString, QString>>& devices)
{
m_device_list = devices;
}
void SetupWizardDialog::onInputDeviceConnected(const QString& identifier, const QString& device_name)
{
m_device_list.emplace_back(identifier, device_name);
}
void SetupWizardDialog::onInputDeviceDisconnected(const QString& identifier)
{
for (auto iter = m_device_list.begin(); iter != m_device_list.end(); ++iter)
{
if (iter->first == identifier)
{
m_device_list.erase(iter);
break;
}
}
}

View file

@ -0,0 +1,82 @@
// SPDX-FileCopyrightText: 2019-2023 Connor McLaughlin <stenzek@gmail.com>.
// SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0)
#pragma once
#include "biossettingswidget.h"
#include "ui_setupwizarddialog.h"
#include <QtCore/QList>
#include <QtCore/QString>
#include <QtCore/QVector>
#include <QtWidgets/QDialog>
#include "core/bios.h"
class SetupWizardDialog final : public QDialog
{
Q_OBJECT
public:
SetupWizardDialog();
~SetupWizardDialog();
private Q_SLOTS:
bool canShowNextPage();
void previousPage();
void nextPage();
void confirmCancel();
void themeChanged();
void languageChanged();
void refreshBiosList();
// void biosListItemChanged(const QTreeWidgetItem* current, const QTreeWidgetItem* previous);
// void listRefreshed(const QVector<BIOSInfo>& items);
void onDirectoryListContextMenuRequested(const QPoint& point);
void onAddSearchDirectoryButtonClicked();
void onRemoveSearchDirectoryButtonClicked();
void refreshDirectoryList();
void resizeDirectoryListColumns();
void onInputDevicesEnumerated(const QList<QPair<QString, QString>>& devices);
void onInputDeviceConnected(const QString& identifier, const QString& device_name);
void onInputDeviceDisconnected(const QString& identifier);
protected:
void resizeEvent(QResizeEvent* event);
private:
enum Page : u32
{
Page_Language,
Page_BIOS,
Page_GameList,
Page_Controller,
Page_Complete,
Page_Count,
};
void setupUi();
void setupLanguagePage();
void setupBIOSPage();
void setupGameListPage();
void setupControllerPage(bool initial);
void pageChangedTo(int page);
void updatePageLabels(int prev_page);
void updatePageButtons();
void addPathToTable(const std::string& path, bool recursive);
void openAutomaticMappingMenu(u32 port, QLabel* update_label);
void doDeviceAutomaticBinding(u32 port, QLabel* update_label, const QString& device);
Ui::SetupWizardDialog m_ui;
std::array<QLabel*, Page_Count> m_page_labels;
QList<QPair<QString, QString>> m_device_list;
};

View file

@ -0,0 +1,734 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>SetupWizardDialog</class>
<widget class="QDialog" name="SetupWizardDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>760</width>
<height>389</height>
</rect>
</property>
<property name="windowTitle">
<string>DuckStation Setup Wizard</string>
</property>
<property name="windowIcon">
<iconset>
<normalon>:/icons/duck.png</normalon>
</iconset>
</property>
<layout class="QGridLayout" name="gridLayout">
<property name="horizontalSpacing">
<number>10</number>
</property>
<item row="0" column="1">
<widget class="QStackedWidget" name="pages">
<property name="currentIndex">
<number>0</number>
</property>
<widget class="QWidget" name="page">
<layout class="QVBoxLayout" name="verticalLayout_3">
<property name="spacing">
<number>10</number>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QLabel" name="label_2">
<property name="text">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;h1 style=&quot; margin-top:18px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;span style=&quot; font-size:xx-large; font-weight:700;&quot;&gt;Welcome to DuckStation!&lt;/span&gt;&lt;/h1&gt;&lt;p&gt;This wizard will help guide you through the configuration steps required to use the application. It is recommended if this is your first time installing DuckStation that you view the setup guide at &lt;a href=&quot;https://github.com/stenzek/duckstation#downloading-and-running&quot;&gt;&lt;span style=&quot; text-decoration: underline; color:#0000ff;&quot;&gt;https://github.com/stenzek/duckstation#downloading-and-running&lt;/span&gt;&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;By default, DuckStation will connect to the server at &lt;a href=&quot;https://github.com/&quot;&gt;&lt;span style=&quot; text-decoration: underline; color:#0000ff;&quot;&gt;github.com&lt;/span&gt;&lt;/a&gt; to check for updates, and if available and confirmed, download update packages from &lt;a href=&quot;https://github.com/&quot;&gt;&lt;span style=&quot; text-decoration: underline; color:#0000ff;&quot;&gt;github.com&lt;/span&gt;&lt;/a&gt;. If you do not wish for DuckStation to make any network connections on startup, you should uncheck the Automatic Updates option now. The Automatic Update setting can be changed later at any time in Interface Settings.&lt;/p&gt;&lt;p&gt;Please choose a language and theme to begin.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="alignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<layout class="QFormLayout" name="formLayout_3">
<item row="0" column="0">
<widget class="QLabel" name="label_3">
<property name="text">
<string>Language:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QComboBox" name="language">
<property name="minimumSize">
<size>
<width>200</width>
<height>0</height>
</size>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_5">
<property name="text">
<string>Theme:</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QComboBox" name="theme"/>
</item>
<item row="3" column="1">
<spacer name="verticalSpacer_3">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>1</width>
<height>1</height>
</size>
</property>
</spacer>
</item>
<item row="2" column="0" colspan="2">
<widget class="QCheckBox" name="autoUpdateEnabled">
<property name="text">
<string>Enable Automatic Updates</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<widget class="QWidget" name="page_2">
<layout class="QVBoxLayout" name="verticalLayout_4">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QLabel" name="label_4">
<property name="text">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;DuckStation requires a PS1 BIOS in order to run.&lt;/p&gt;&lt;p&gt;For legal reasons, you must obtain a BIOS &lt;span style=&quot; font-weight:700;&quot;&gt;from an actual PS1 unit that you own&lt;/span&gt; (borrowing doesn't count). You should use Caetla or another utility to create an image from your console's BIOS ROM on your PC.&lt;/p&gt;&lt;p&gt;Once dumped, this BIOS image should be placed in the bios folder within the data directory shown below, or you can instruct DuckStation to scan an alternative directory.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="alignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_3">
<item>
<widget class="QLabel" name="label_10">
<property name="text">
<string>BIOS Directory:</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="biosSearchDirectory"/>
</item>
<item>
<widget class="QPushButton" name="browseBiosSearchDirectory">
<property name="text">
<string>Browse...</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="resetBiosSearchDirectory">
<property name="text">
<string>Reset</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QFormLayout" name="formLayout_4">
<item row="0" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>NTSC-J (Japan):</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QComboBox" name="imageNTSCJ">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_15">
<property name="text">
<string>NTSC-U/C (US/Canada):</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QComboBox" name="imageNTSCU">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_16">
<property name="text">
<string>PAL (Europe, Australia):</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QComboBox" name="imagePAL">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<spacer name="horizontalSpacer_4">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="openBiosSearchDirectory">
<property name="text">
<string>Open in Explorer...</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="refreshBiosList">
<property name="text">
<string>Refresh List</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<spacer name="verticalSpacer_2">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>1</width>
<height>1</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
<widget class="QWidget" name="page_3">
<layout class="QVBoxLayout" name="verticalLayout_5">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QLabel" name="label_6">
<property name="text">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;DuckStation will automatically scan and identify games from the selected directories below, and populate the game list. These games should be dumped from discs you own. Utilities such as ImgBurn can be used to create images of game discs in .bin/.cue format.&lt;/p&gt;&lt;p&gt;Supported formats for dumps include: &lt;span style=&quot; font-weight:700;&quot;&gt;.cue&lt;/span&gt; (Cue Sheets), &lt;span style=&quot; font-weight:700;&quot;&gt;.iso/.img&lt;/span&gt; (Single Track Image), &lt;span style=&quot; font-weight:700;&quot;&gt;.ecm&lt;/span&gt; (Error Code Modeling Image), &lt;span style=&quot; font-weight:700;&quot;&gt;.mds&lt;/span&gt; (Media Descriptor Sidecar), &lt;span style=&quot; font-weight:700;&quot;&gt;.chd&lt;/span&gt; (Compressed Hunks of Data), &lt;span style=&quot; font-weight:700;&quot;&gt;.pbp&lt;/span&gt; (PlayStation Portable, Only Decrypted).&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="alignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_4">
<item>
<widget class="QLabel" name="label_7">
<property name="text">
<string>Search Directories (will be scanned for games)</string>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_5">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QToolButton" name="addSearchDirectoryButton">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Minimum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Add...</string>
</property>
<property name="icon">
<iconset theme="folder-add-line">
<normaloff>.</normaloff>.</iconset>
</property>
<property name="toolButtonStyle">
<enum>Qt::ToolButtonTextBesideIcon</enum>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="removeSearchDirectoryButton">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Minimum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Remove</string>
</property>
<property name="icon">
<iconset theme="folder-reduce-line">
<normaloff>.</normaloff>.</iconset>
</property>
<property name="toolButtonStyle">
<enum>Qt::ToolButtonTextBesideIcon</enum>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QTableWidget" name="searchDirectoryList">
<column>
<property name="text">
<string>Search Directory</string>
</property>
</column>
<column>
<property name="text">
<string>Scan Recursively</string>
</property>
</column>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="page_4">
<layout class="QVBoxLayout" name="verticalLayout_6">
<property name="spacing">
<number>10</number>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QLabel" name="label_8">
<property name="text">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;By default, DuckStation will map your keyboard to the virtual controller.&lt;/p&gt;&lt;p&gt;&lt;span style=&quot; font-weight:700;&quot;&gt;To use an external controller, you must map it first. &lt;/span&gt;On this screen, you can automatically map any controller which is currently connected. If your controller is not currently connected, you can plug it in now.&lt;/p&gt;&lt;p&gt;To change controller bindings in more detail, or use multi-tap, open the Settings menu and choose Controllers once you have completed the Setup Wizard.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox">
<property name="title">
<string>Controller Port 1</string>
</property>
<layout class="QFormLayout" name="formLayout">
<item row="0" column="1">
<widget class="QComboBox" name="controller1Type"/>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_12">
<property name="text">
<string>Controller Mapped To:</string>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QLabel" name="label_11">
<property name="text">
<string>Controller Type:</string>
</property>
</widget>
</item>
<item row="1" column="1">
<layout class="QHBoxLayout" name="horizontalLayout_5">
<item>
<widget class="QLabel" name="controller1Mapping">
<property name="text">
<string>Default (Keyboard)</string>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_6">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QToolButton" name="controller1AutomaticMapping">
<property name="text">
<string>Automatic Mapping</string>
</property>
<property name="icon">
<iconset theme="gamepad-line" />
</property>
<property name="toolButtonStyle">
<enum>Qt::ToolButtonTextBesideIcon</enum>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox_2">
<property name="title">
<string>Controller Port 2</string>
</property>
<layout class="QFormLayout" name="formLayout_2">
<item row="0" column="0">
<widget class="QLabel" name="label_13">
<property name="text">
<string>Controller Type:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QComboBox" name="controller2Type"/>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_14">
<property name="text">
<string>Controller Mapped To:</string>
</property>
</widget>
</item>
<item row="1" column="1">
<layout class="QHBoxLayout" name="horizontalLayout_6">
<item>
<widget class="QLabel" name="controller2Mapping">
<property name="text">
<string>Default (Keyboard)</string>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_8">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QToolButton" name="controller2AutomaticMapping">
<property name="text">
<string>Automatic Mapping</string>
</property>
<property name="icon">
<iconset theme="gamepad-line" />
</property>
<property name="toolButtonStyle">
<enum>Qt::ToolButtonTextBesideIcon</enum>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</item>
<item>
<spacer name="verticalSpacer_4">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
<widget class="QWidget" name="page_5">
<layout class="QVBoxLayout" name="verticalLayout_7">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QLabel" name="label_9">
<property name="text">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;h1 style=&quot; margin-top:18px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;span style=&quot; font-size:xx-large; font-weight:700;&quot;&gt;Setup Complete!&lt;/span&gt;&lt;/h1&gt;&lt;p&gt;You are now ready to run games.&lt;/p&gt;&lt;p&gt;Further options are available under the settings menu. You can also use the Big Picture UI for navigation entirely with a gamepad.&lt;/p&gt;&lt;p&gt;We hope you enjoy using DuckStation.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="alignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
</widget>
</item>
<item row="0" column="0">
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QLabel" name="logo">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>128</width>
<height>128</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>128</width>
<height>128</height>
</size>
</property>
<property name="text">
<string/>
</property>
<property name="scaledContents">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacer_5">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Fixed</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<layout class="QVBoxLayout" name="verticalLayout">
<property name="spacing">
<number>20</number>
</property>
<item>
<widget class="QLabel" name="labelLanguage">
<property name="font">
<font>
<bold>true</bold>
</font>
</property>
<property name="text">
<string>Language</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="labelBIOS">
<property name="text">
<string>BIOS Image</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="labelGameList">
<property name="text">
<string>Game Directories</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="labelController">
<property name="text">
<string>Controller Setup</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="labelComplete">
<property name="text">
<string>Complete</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>1</width>
<height>1</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item row="1" column="0" colspan="2">
<layout class="QHBoxLayout" name="horizontalLayout">
<property name="spacing">
<number>6</number>
</property>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="back">
<property name="text">
<string>&amp;Back</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="next">
<property name="text">
<string>&amp;Next</string>
</property>
<property name="default">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="cancel">
<property name="text">
<string>&amp;Cancel</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<resources>
<include location="resources/resources.qrc"/>
<include location="resources/resources.qrc"/>
</resources>
<connections/>
</ui>