diff options
Diffstat (limited to 'src/yuzu')
32 files changed, 1280 insertions, 273 deletions
diff --git a/src/yuzu/CMakeLists.txt b/src/yuzu/CMakeLists.txt index 04464ad5e..f9ca2948e 100644 --- a/src/yuzu/CMakeLists.txt +++ b/src/yuzu/CMakeLists.txt @@ -56,6 +56,8 @@ add_executable(yuzu main.h ui_settings.cpp ui_settings.h + util/limitable_input_dialog.cpp + util/limitable_input_dialog.h util/spinbox.cpp util/spinbox.h util/util.cpp @@ -82,10 +84,10 @@ set(UIS ) file(GLOB COMPAT_LIST - ${CMAKE_BINARY_DIR}/dist/compatibility_list/compatibility_list.qrc - ${CMAKE_BINARY_DIR}/dist/compatibility_list/compatibility_list.json) -file(GLOB_RECURSE ICONS ${CMAKE_SOURCE_DIR}/dist/icons/*) -file(GLOB_RECURSE THEMES ${CMAKE_SOURCE_DIR}/dist/qt_themes/*) + ${PROJECT_BINARY_DIR}/dist/compatibility_list/compatibility_list.qrc + ${PROJECT_BINARY_DIR}/dist/compatibility_list/compatibility_list.json) +file(GLOB_RECURSE ICONS ${PROJECT_SOURCE_DIR}/dist/icons/*) +file(GLOB_RECURSE THEMES ${PROJECT_SOURCE_DIR}/dist/qt_themes/*) qt5_wrap_ui(UI_HDRS ${UIS}) @@ -121,7 +123,7 @@ target_link_libraries(yuzu PRIVATE Boost::boost glad Qt5::OpenGL Qt5::Widgets) target_link_libraries(yuzu PRIVATE ${PLATFORM_LIBRARIES} Threads::Threads) if (YUZU_ENABLE_COMPATIBILITY_REPORTING) - add_definitions(-DYUZU_ENABLE_COMPATIBILITY_REPORTING) + target_compile_definitions(yuzu PRIVATE -DYUZU_ENABLE_COMPATIBILITY_REPORTING) endif() if (USE_DISCORD_PRESENCE) diff --git a/src/yuzu/bootmanager.cpp b/src/yuzu/bootmanager.cpp index 4e4c108ab..39eef8858 100644 --- a/src/yuzu/bootmanager.cpp +++ b/src/yuzu/bootmanager.cpp @@ -8,7 +8,6 @@ #include "common/microprofile.h" #include "common/scm_rev.h" -#include "common/string_util.h" #include "core/core.h" #include "core/frontend/framebuffer_layout.h" #include "core/settings.h" @@ -107,9 +106,9 @@ private: GRenderWindow::GRenderWindow(QWidget* parent, EmuThread* emu_thread) : QWidget(parent), child(nullptr), emu_thread(emu_thread) { - std::string window_title = fmt::format("yuzu {} | {}-{}", Common::g_build_name, - Common::g_scm_branch, Common::g_scm_desc); - setWindowTitle(QString::fromStdString(window_title)); + setWindowTitle(QStringLiteral("yuzu %1 | %2-%3") + .arg(Common::g_build_name, Common::g_scm_branch, Common::g_scm_desc)); + setAttribute(Qt::WA_AcceptTouchEvents); InputCommon::Init(); InputCommon::StartJoystickEventHandler(); @@ -190,11 +189,17 @@ QByteArray GRenderWindow::saveGeometry() { return geometry; } -qreal GRenderWindow::windowPixelRatio() { +qreal GRenderWindow::windowPixelRatio() const { // windowHandle() might not be accessible until the window is displayed to screen. return windowHandle() ? windowHandle()->screen()->devicePixelRatio() : 1.0f; } +std::pair<unsigned, unsigned> GRenderWindow::ScaleTouch(const QPointF pos) const { + const qreal pixel_ratio = windowPixelRatio(); + return {static_cast<unsigned>(std::max(std::round(pos.x() * pixel_ratio), qreal{0.0})), + static_cast<unsigned>(std::max(std::round(pos.y() * pixel_ratio), qreal{0.0}))}; +} + void GRenderWindow::closeEvent(QCloseEvent* event) { emit Closed(); QWidget::closeEvent(event); @@ -209,31 +214,81 @@ void GRenderWindow::keyReleaseEvent(QKeyEvent* event) { } void GRenderWindow::mousePressEvent(QMouseEvent* event) { + if (event->source() == Qt::MouseEventSynthesizedBySystem) + return; // touch input is handled in TouchBeginEvent + auto pos = event->pos(); if (event->button() == Qt::LeftButton) { - qreal pixelRatio = windowPixelRatio(); - this->TouchPressed(static_cast<unsigned>(pos.x() * pixelRatio), - static_cast<unsigned>(pos.y() * pixelRatio)); + const auto [x, y] = ScaleTouch(pos); + this->TouchPressed(x, y); } else if (event->button() == Qt::RightButton) { InputCommon::GetMotionEmu()->BeginTilt(pos.x(), pos.y()); } } void GRenderWindow::mouseMoveEvent(QMouseEvent* event) { + if (event->source() == Qt::MouseEventSynthesizedBySystem) + return; // touch input is handled in TouchUpdateEvent + auto pos = event->pos(); - qreal pixelRatio = windowPixelRatio(); - this->TouchMoved(std::max(static_cast<unsigned>(pos.x() * pixelRatio), 0u), - std::max(static_cast<unsigned>(pos.y() * pixelRatio), 0u)); + const auto [x, y] = ScaleTouch(pos); + this->TouchMoved(x, y); InputCommon::GetMotionEmu()->Tilt(pos.x(), pos.y()); } void GRenderWindow::mouseReleaseEvent(QMouseEvent* event) { + if (event->source() == Qt::MouseEventSynthesizedBySystem) + return; // touch input is handled in TouchEndEvent + if (event->button() == Qt::LeftButton) this->TouchReleased(); else if (event->button() == Qt::RightButton) InputCommon::GetMotionEmu()->EndTilt(); } +void GRenderWindow::TouchBeginEvent(const QTouchEvent* event) { + // TouchBegin always has exactly one touch point, so take the .first() + const auto [x, y] = ScaleTouch(event->touchPoints().first().pos()); + this->TouchPressed(x, y); +} + +void GRenderWindow::TouchUpdateEvent(const QTouchEvent* event) { + QPointF pos; + int active_points = 0; + + // average all active touch points + for (const auto tp : event->touchPoints()) { + if (tp.state() & (Qt::TouchPointPressed | Qt::TouchPointMoved | Qt::TouchPointStationary)) { + active_points++; + pos += tp.pos(); + } + } + + pos /= active_points; + + const auto [x, y] = ScaleTouch(pos); + this->TouchMoved(x, y); +} + +void GRenderWindow::TouchEndEvent() { + this->TouchReleased(); +} + +bool GRenderWindow::event(QEvent* event) { + if (event->type() == QEvent::TouchBegin) { + TouchBeginEvent(static_cast<QTouchEvent*>(event)); + return true; + } else if (event->type() == QEvent::TouchUpdate) { + TouchUpdateEvent(static_cast<QTouchEvent*>(event)); + return true; + } else if (event->type() == QEvent::TouchEnd || event->type() == QEvent::TouchCancel) { + TouchEndEvent(); + return true; + } + + return QWidget::event(event); +} + void GRenderWindow::focusOutEvent(QFocusEvent* event) { QWidget::focusOutEvent(event); InputCommon::GetKeyboard()->ReleaseAllKeys(); diff --git a/src/yuzu/bootmanager.h b/src/yuzu/bootmanager.h index f133bfadf..873985564 100644 --- a/src/yuzu/bootmanager.h +++ b/src/yuzu/bootmanager.h @@ -15,6 +15,7 @@ class QKeyEvent; class QScreen; +class QTouchEvent; class GGLWidgetInternal; class GMainWindow; @@ -119,7 +120,7 @@ public: void restoreGeometry(const QByteArray& geometry); // overridden QByteArray saveGeometry(); // overridden - qreal windowPixelRatio(); + qreal windowPixelRatio() const; void closeEvent(QCloseEvent* event) override; @@ -130,6 +131,8 @@ public: void mouseMoveEvent(QMouseEvent* event) override; void mouseReleaseEvent(QMouseEvent* event) override; + bool event(QEvent* event) override; + void focusOutEvent(QFocusEvent* event) override; void OnClientAreaResized(unsigned width, unsigned height); @@ -148,6 +151,11 @@ signals: void Closed(); private: + std::pair<unsigned, unsigned> ScaleTouch(const QPointF pos) const; + void TouchBeginEvent(const QTouchEvent* event); + void TouchUpdateEvent(const QTouchEvent* event); + void TouchEndEvent(); + void OnMinimalClientAreaChangeRequest( const std::pair<unsigned, unsigned>& minimal_size) override; diff --git a/src/yuzu/compatdb.cpp b/src/yuzu/compatdb.cpp index 91e754274..5f0896f84 100644 --- a/src/yuzu/compatdb.cpp +++ b/src/yuzu/compatdb.cpp @@ -5,6 +5,7 @@ #include <QButtonGroup> #include <QMessageBox> #include <QPushButton> +#include <QtConcurrent/qtconcurrentrun.h> #include "common/logging/log.h" #include "common/telemetry.h" #include "core/core.h" @@ -23,6 +24,8 @@ CompatDB::CompatDB(QWidget* parent) connect(ui->radioButton_IntroMenu, &QRadioButton::clicked, this, &CompatDB::EnableNext); connect(ui->radioButton_WontBoot, &QRadioButton::clicked, this, &CompatDB::EnableNext); connect(button(NextButton), &QPushButton::clicked, this, &CompatDB::Submit); + connect(&testcase_watcher, &QFutureWatcher<bool>::finished, this, + &CompatDB::OnTestcaseSubmitted); } CompatDB::~CompatDB() = default; @@ -48,18 +51,38 @@ void CompatDB::Submit() { } break; case CompatDBPage::Final: + back(); LOG_DEBUG(Frontend, "Compatibility Rating: {}", compatibility->checkedId()); Core::Telemetry().AddField(Telemetry::FieldType::UserFeedback, "Compatibility", compatibility->checkedId()); - // older versions of QT don't support the "NoCancelButtonOnLastPage" option, this is a - // workaround + + button(NextButton)->setEnabled(false); + button(NextButton)->setText(tr("Submitting")); button(QWizard::CancelButton)->setVisible(false); + + testcase_watcher.setFuture(QtConcurrent::run( + [this]() { return Core::System::GetInstance().TelemetrySession().SubmitTestcase(); })); break; default: LOG_ERROR(Frontend, "Unexpected page: {}", currentId()); } } +void CompatDB::OnTestcaseSubmitted() { + if (!testcase_watcher.result()) { + QMessageBox::critical(this, tr("Communication error"), + tr("An error occured while sending the Testcase")); + button(NextButton)->setEnabled(true); + button(NextButton)->setText(tr("Next")); + button(QWizard::CancelButton)->setVisible(true); + } else { + next(); + // older versions of QT don't support the "NoCancelButtonOnLastPage" option, this is a + // workaround + button(QWizard::CancelButton)->setVisible(false); + } +} + void CompatDB::EnableNext() { button(NextButton)->setEnabled(true); } diff --git a/src/yuzu/compatdb.h b/src/yuzu/compatdb.h index ca0dd11d6..5381f67f7 100644 --- a/src/yuzu/compatdb.h +++ b/src/yuzu/compatdb.h @@ -5,6 +5,7 @@ #pragma once #include <memory> +#include <QFutureWatcher> #include <QWizard> namespace Ui { @@ -19,8 +20,11 @@ public: ~CompatDB(); private: + QFutureWatcher<bool> testcase_watcher; + std::unique_ptr<Ui::CompatDB> ui; void Submit(); + void OnTestcaseSubmitted(); void EnableNext(); }; diff --git a/src/yuzu/configuration/config.cpp b/src/yuzu/configuration/config.cpp index 650dd03c0..d4fd60a73 100644 --- a/src/yuzu/configuration/config.cpp +++ b/src/yuzu/configuration/config.cpp @@ -4,6 +4,7 @@ #include <QSettings> #include "common/file_util.h" +#include "core/hle/service/acc/profile_manager.h" #include "input_common/main.h" #include "yuzu/configuration/config.h" #include "yuzu/ui_settings.h" @@ -12,11 +13,16 @@ Config::Config() { // TODO: Don't hardcode the path; let the frontend decide where to put the config files. qt_config_loc = FileUtil::GetUserPath(FileUtil::UserPath::ConfigDir) + "qt-config.ini"; FileUtil::CreateFullPath(qt_config_loc); - qt_config = new QSettings(QString::fromStdString(qt_config_loc), QSettings::IniFormat); + qt_config = + std::make_unique<QSettings>(QString::fromStdString(qt_config_loc), QSettings::IniFormat); Reload(); } +Config::~Config() { + Save(); +} + const std::array<int, Settings::NativeButton::NumButtons> Config::default_buttons = { Qt::Key_A, Qt::Key_S, Qt::Key_Z, Qt::Key_X, Qt::Key_3, Qt::Key_4, Qt::Key_Q, Qt::Key_W, Qt::Key_1, Qt::Key_2, Qt::Key_N, Qt::Key_M, Qt::Key_F, Qt::Key_T, @@ -85,8 +91,8 @@ void Config::ReadValues() { Settings::values.resolution_factor = qt_config->value("resolution_factor", 1.0).toFloat(); Settings::values.use_frame_limit = qt_config->value("use_frame_limit", true).toBool(); Settings::values.frame_limit = qt_config->value("frame_limit", 100).toInt(); - Settings::values.use_accurate_framebuffers = - qt_config->value("use_accurate_framebuffers", false).toBool(); + Settings::values.use_accurate_gpu_emulation = + qt_config->value("use_accurate_gpu_emulation", false).toBool(); Settings::values.bg_red = qt_config->value("bg_red", 0.0).toFloat(); Settings::values.bg_green = qt_config->value("bg_green", 0.0).toFloat(); @@ -122,7 +128,11 @@ void Config::ReadValues() { qt_config->beginGroup("System"); Settings::values.use_docked_mode = qt_config->value("use_docked_mode", false).toBool(); - Settings::values.username = qt_config->value("username", "yuzu").toString().toStdString(); + Settings::values.enable_nfc = qt_config->value("enable_nfc", true).toBool(); + + Settings::values.current_user = std::clamp<int>(qt_config->value("current_user", 0).toInt(), 0, + Service::Account::MAX_USERS - 1); + Settings::values.language_index = qt_config->value("language_index", 1).toInt(); qt_config->endGroup(); @@ -134,6 +144,7 @@ void Config::ReadValues() { qt_config->beginGroup("Debugging"); Settings::values.use_gdbstub = qt_config->value("use_gdbstub", false).toBool(); Settings::values.gdbstub_port = qt_config->value("gdbstub_port", 24689).toInt(); + Settings::values.program_args = qt_config->value("program_args", "").toString().toStdString(); qt_config->endGroup(); qt_config->beginGroup("WebService"); @@ -232,7 +243,7 @@ void Config::SaveValues() { qt_config->setValue("resolution_factor", (double)Settings::values.resolution_factor); qt_config->setValue("use_frame_limit", Settings::values.use_frame_limit); qt_config->setValue("frame_limit", Settings::values.frame_limit); - qt_config->setValue("use_accurate_framebuffers", Settings::values.use_accurate_framebuffers); + qt_config->setValue("use_accurate_gpu_emulation", Settings::values.use_accurate_gpu_emulation); // Cast to double because Qt's written float values are not human-readable qt_config->setValue("bg_red", (double)Settings::values.bg_red); @@ -257,7 +268,9 @@ void Config::SaveValues() { qt_config->beginGroup("System"); qt_config->setValue("use_docked_mode", Settings::values.use_docked_mode); - qt_config->setValue("username", QString::fromStdString(Settings::values.username)); + qt_config->setValue("enable_nfc", Settings::values.enable_nfc); + qt_config->setValue("current_user", Settings::values.current_user); + qt_config->setValue("language_index", Settings::values.language_index); qt_config->endGroup(); @@ -269,6 +282,7 @@ void Config::SaveValues() { qt_config->beginGroup("Debugging"); qt_config->setValue("use_gdbstub", Settings::values.use_gdbstub); qt_config->setValue("gdbstub_port", Settings::values.gdbstub_port); + qt_config->setValue("program_args", QString::fromStdString(Settings::values.program_args)); qt_config->endGroup(); qt_config->beginGroup("WebService"); @@ -333,9 +347,3 @@ void Config::Reload() { void Config::Save() { SaveValues(); } - -Config::~Config() { - Save(); - - delete qt_config; -} diff --git a/src/yuzu/configuration/config.h b/src/yuzu/configuration/config.h index cbf745ea2..9c99c1b75 100644 --- a/src/yuzu/configuration/config.h +++ b/src/yuzu/configuration/config.h @@ -5,6 +5,7 @@ #pragma once #include <array> +#include <memory> #include <string> #include <QVariant> #include "core/settings.h" @@ -12,12 +13,6 @@ class QSettings; class Config { - QSettings* qt_config; - std::string qt_config_loc; - - void ReadValues(); - void SaveValues(); - public: Config(); ~Config(); @@ -27,4 +22,11 @@ public: static const std::array<int, Settings::NativeButton::NumButtons> default_buttons; static const std::array<std::array<int, 5>, Settings::NativeAnalog::NumAnalogs> default_analogs; + +private: + void ReadValues(); + void SaveValues(); + + std::unique_ptr<QSettings> qt_config; + std::string qt_config_loc; }; diff --git a/src/yuzu/configuration/configure_debug.cpp b/src/yuzu/configuration/configure_debug.cpp index 45d84f19a..9e765fc93 100644 --- a/src/yuzu/configuration/configure_debug.cpp +++ b/src/yuzu/configuration/configure_debug.cpp @@ -33,6 +33,7 @@ void ConfigureDebug::setConfiguration() { ui->toggle_console->setEnabled(!Core::System::GetInstance().IsPoweredOn()); ui->toggle_console->setChecked(UISettings::values.show_console); ui->log_filter_edit->setText(QString::fromStdString(Settings::values.log_filter)); + ui->homebrew_args_edit->setText(QString::fromStdString(Settings::values.program_args)); } void ConfigureDebug::applyConfiguration() { @@ -40,6 +41,7 @@ void ConfigureDebug::applyConfiguration() { Settings::values.gdbstub_port = ui->gdbport_spinbox->value(); UISettings::values.show_console = ui->toggle_console->isChecked(); Settings::values.log_filter = ui->log_filter_edit->text().toStdString(); + Settings::values.program_args = ui->homebrew_args_edit->text().toStdString(); Debugger::ToggleConsole(); Log::Filter filter; filter.ParseFilterString(Settings::values.log_filter); diff --git a/src/yuzu/configuration/configure_debug.ui b/src/yuzu/configuration/configure_debug.ui index 5ae7276bd..ff4987604 100644 --- a/src/yuzu/configuration/configure_debug.ui +++ b/src/yuzu/configuration/configure_debug.ui @@ -107,6 +107,29 @@ </widget> </item> <item> + <widget class="QGroupBox" name="groupBox_3"> + <property name="title"> + <string>Homebrew</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <layout class="QHBoxLayout" name="horizontalLayout"> + <item> + <widget class="QLabel" name="label"> + <property name="text"> + <string>Arguments String</string> + </property> + </widget> + </item> + <item> + <widget class="QLineEdit" name="homebrew_args_edit"/> + </item> + </layout> + </item> + </layout> + </widget> + </item> + <item> <spacer name="verticalSpacer"> <property name="orientation"> <enum>Qt::Vertical</enum> diff --git a/src/yuzu/configuration/configure_general.cpp b/src/yuzu/configuration/configure_general.cpp index f5db9e55b..537d6e576 100644 --- a/src/yuzu/configuration/configure_general.cpp +++ b/src/yuzu/configuration/configure_general.cpp @@ -31,6 +31,7 @@ void ConfigureGeneral::setConfiguration() { ui->theme_combobox->setCurrentIndex(ui->theme_combobox->findData(UISettings::values.theme)); ui->use_cpu_jit->setChecked(Settings::values.use_cpu_jit); ui->use_docked_mode->setChecked(Settings::values.use_docked_mode); + ui->enable_nfc->setChecked(Settings::values.enable_nfc); } void ConfigureGeneral::PopulateHotkeyList(const HotkeyRegistry& registry) { @@ -45,4 +46,5 @@ void ConfigureGeneral::applyConfiguration() { Settings::values.use_cpu_jit = ui->use_cpu_jit->isChecked(); Settings::values.use_docked_mode = ui->use_docked_mode->isChecked(); + Settings::values.enable_nfc = ui->enable_nfc->isChecked(); } diff --git a/src/yuzu/configuration/configure_general.ui b/src/yuzu/configuration/configure_general.ui index 1775c4d40..b82fffde8 100644 --- a/src/yuzu/configuration/configure_general.ui +++ b/src/yuzu/configuration/configure_general.ui @@ -68,19 +68,26 @@ <property name="title"> <string>Emulation</string> </property> - <layout class="QHBoxLayout" name="EmulationHorizontalLayout"> + <layout class="QHBoxLayout" name="EmulationHorizontalLayout"> + <item> + <layout class="QVBoxLayout" name="EmulationVerticalLayout"> + <item> + <widget class="QCheckBox" name="use_docked_mode"> + <property name="text"> + <string>Enable docked mode</string> + </property> + </widget> + </item> <item> - <layout class="QVBoxLayout" name="EmulationVerticalLayout"> - <item> - <widget class="QCheckBox" name="use_docked_mode"> - <property name="text"> - <string>Enable docked mode</string> - </property> - </widget> - </item> - </layout> + <widget class="QCheckBox" name="enable_nfc"> + <property name="text"> + <string>Enable NFC</string> + </property> + </widget> </item> - </layout> + </layout> + </item> + </layout> </widget> </item> <item> diff --git a/src/yuzu/configuration/configure_graphics.cpp b/src/yuzu/configuration/configure_graphics.cpp index cd1549462..8290b4384 100644 --- a/src/yuzu/configuration/configure_graphics.cpp +++ b/src/yuzu/configuration/configure_graphics.cpp @@ -75,7 +75,7 @@ void ConfigureGraphics::setConfiguration() { static_cast<int>(FromResolutionFactor(Settings::values.resolution_factor))); ui->toggle_frame_limit->setChecked(Settings::values.use_frame_limit); ui->frame_limit->setValue(Settings::values.frame_limit); - ui->use_accurate_framebuffers->setChecked(Settings::values.use_accurate_framebuffers); + ui->use_accurate_gpu_emulation->setChecked(Settings::values.use_accurate_gpu_emulation); bg_color = QColor::fromRgbF(Settings::values.bg_red, Settings::values.bg_green, Settings::values.bg_blue); ui->bg_button->setStyleSheet( @@ -87,7 +87,7 @@ void ConfigureGraphics::applyConfiguration() { ToResolutionFactor(static_cast<Resolution>(ui->resolution_factor_combobox->currentIndex())); Settings::values.use_frame_limit = ui->toggle_frame_limit->isChecked(); Settings::values.frame_limit = ui->frame_limit->value(); - Settings::values.use_accurate_framebuffers = ui->use_accurate_framebuffers->isChecked(); + Settings::values.use_accurate_gpu_emulation = ui->use_accurate_gpu_emulation->isChecked(); Settings::values.bg_red = static_cast<float>(bg_color.redF()); Settings::values.bg_green = static_cast<float>(bg_color.greenF()); Settings::values.bg_blue = static_cast<float>(bg_color.blueF()); diff --git a/src/yuzu/configuration/configure_graphics.ui b/src/yuzu/configuration/configure_graphics.ui index 8fc00af1b..91fcad994 100644 --- a/src/yuzu/configuration/configure_graphics.ui +++ b/src/yuzu/configuration/configure_graphics.ui @@ -50,9 +50,9 @@ </layout> </item> <item> - <widget class="QCheckBox" name="use_accurate_framebuffers"> + <widget class="QCheckBox" name="use_accurate_gpu_emulation"> <property name="text"> - <string>Use accurate framebuffers (slow)</string> + <string>Use accurate GPU emulation (slow)</string> </property> </widget> </item> diff --git a/src/yuzu/configuration/configure_input.cpp b/src/yuzu/configuration/configure_input.cpp index 473937ea9..42a7beac6 100644 --- a/src/yuzu/configuration/configure_input.cpp +++ b/src/yuzu/configuration/configure_input.cpp @@ -5,6 +5,7 @@ #include <algorithm> #include <memory> #include <utility> +#include <QMenu> #include <QMessageBox> #include <QTimer> #include "common/param_package.h" @@ -128,28 +129,63 @@ ConfigureInput::ConfigureInput(QWidget* parent) analog_map_stick = {ui->buttonLStickAnalog, ui->buttonRStickAnalog}; for (int button_id = 0; button_id < Settings::NativeButton::NumButtons; button_id++) { - if (button_map[button_id]) - connect(button_map[button_id], &QPushButton::released, [=]() { - handleClick( - button_map[button_id], - [=](const Common::ParamPackage& params) { buttons_param[button_id] = params; }, - InputCommon::Polling::DeviceType::Button); - }); + if (!button_map[button_id]) + continue; + button_map[button_id]->setContextMenuPolicy(Qt::CustomContextMenu); + connect(button_map[button_id], &QPushButton::released, [=]() { + handleClick( + button_map[button_id], + [=](const Common::ParamPackage& params) { buttons_param[button_id] = params; }, + InputCommon::Polling::DeviceType::Button); + }); + connect(button_map[button_id], &QPushButton::customContextMenuRequested, + [=](const QPoint& menu_location) { + QMenu context_menu; + context_menu.addAction(tr("Clear"), [&] { + buttons_param[button_id].Clear(); + button_map[button_id]->setText(tr("[not set]")); + }); + context_menu.addAction(tr("Restore Default"), [&] { + buttons_param[button_id] = Common::ParamPackage{ + InputCommon::GenerateKeyboardParam(Config::default_buttons[button_id])}; + button_map[button_id]->setText(ButtonToText(buttons_param[button_id])); + }); + context_menu.exec(button_map[button_id]->mapToGlobal(menu_location)); + }); } for (int analog_id = 0; analog_id < Settings::NativeAnalog::NumAnalogs; analog_id++) { for (int sub_button_id = 0; sub_button_id < ANALOG_SUB_BUTTONS_NUM; sub_button_id++) { - if (analog_map_buttons[analog_id][sub_button_id] != nullptr) { - connect(analog_map_buttons[analog_id][sub_button_id], &QPushButton::released, - [=]() { - handleClick(analog_map_buttons[analog_id][sub_button_id], - [=](const Common::ParamPackage& params) { - SetAnalogButton(params, analogs_param[analog_id], - analog_sub_buttons[sub_button_id]); - }, - InputCommon::Polling::DeviceType::Button); + if (!analog_map_buttons[analog_id][sub_button_id]) + continue; + analog_map_buttons[analog_id][sub_button_id]->setContextMenuPolicy( + Qt::CustomContextMenu); + connect(analog_map_buttons[analog_id][sub_button_id], &QPushButton::released, [=]() { + handleClick(analog_map_buttons[analog_id][sub_button_id], + [=](const Common::ParamPackage& params) { + SetAnalogButton(params, analogs_param[analog_id], + analog_sub_buttons[sub_button_id]); + }, + InputCommon::Polling::DeviceType::Button); + }); + connect(analog_map_buttons[analog_id][sub_button_id], + &QPushButton::customContextMenuRequested, [=](const QPoint& menu_location) { + QMenu context_menu; + context_menu.addAction(tr("Clear"), [&] { + analogs_param[analog_id].Erase(analog_sub_buttons[sub_button_id]); + analog_map_buttons[analog_id][sub_button_id]->setText(tr("[not set]")); }); - } + context_menu.addAction(tr("Restore Default"), [&] { + Common::ParamPackage params{InputCommon::GenerateKeyboardParam( + Config::default_analogs[analog_id][sub_button_id])}; + SetAnalogButton(params, analogs_param[analog_id], + analog_sub_buttons[sub_button_id]); + analog_map_buttons[analog_id][sub_button_id]->setText(AnalogToText( + analogs_param[analog_id], analog_sub_buttons[sub_button_id])); + }); + context_menu.exec(analog_map_buttons[analog_id][sub_button_id]->mapToGlobal( + menu_location)); + }); } connect(analog_map_stick[analog_id], &QPushButton::released, [=]() { QMessageBox::information(this, tr("Information"), @@ -162,6 +198,7 @@ ConfigureInput::ConfigureInput(QWidget* parent) }); } + connect(ui->buttonClearAll, &QPushButton::released, [this] { ClearAll(); }); connect(ui->buttonRestoreDefaults, &QPushButton::released, [this]() { restoreDefaults(); }); timeout_timer->setSingleShot(true); @@ -215,7 +252,21 @@ void ConfigureInput::restoreDefaults() { } } updateButtonLabels(); - applyConfiguration(); +} + +void ConfigureInput::ClearAll() { + for (int button_id = 0; button_id < Settings::NativeButton::NumButtons; button_id++) { + if (button_map[button_id] && button_map[button_id]->isEnabled()) + buttons_param[button_id].Clear(); + } + for (int analog_id = 0; analog_id < Settings::NativeAnalog::NumAnalogs; analog_id++) { + for (int sub_button_id = 0; sub_button_id < ANALOG_SUB_BUTTONS_NUM; sub_button_id++) { + if (analog_map_buttons[analog_id][sub_button_id] && + analog_map_buttons[analog_id][sub_button_id]->isEnabled()) + analogs_param[analog_id].Erase(analog_sub_buttons[sub_button_id]); + } + } + updateButtonLabels(); } void ConfigureInput::updateButtonLabels() { @@ -271,7 +322,7 @@ void ConfigureInput::setPollingResult(const Common::ParamPackage& params, bool a } updateButtonLabels(); - input_setter = boost::none; + input_setter = {}; } void ConfigureInput::keyPressEvent(QKeyEvent* event) { diff --git a/src/yuzu/configuration/configure_input.h b/src/yuzu/configuration/configure_input.h index a0bef86d5..32c7183f9 100644 --- a/src/yuzu/configuration/configure_input.h +++ b/src/yuzu/configuration/configure_input.h @@ -7,11 +7,13 @@ #include <array> #include <functional> #include <memory> +#include <optional> #include <string> #include <unordered_map> + #include <QKeyEvent> #include <QWidget> -#include <boost/optional.hpp> + #include "common/param_package.h" #include "core/settings.h" #include "input_common/main.h" @@ -41,7 +43,7 @@ private: std::unique_ptr<QTimer> poll_timer; /// This will be the the setting function when an input is awaiting configuration. - boost::optional<std::function<void(const Common::ParamPackage&)>> input_setter; + std::optional<std::function<void(const Common::ParamPackage&)>> input_setter; std::array<Common::ParamPackage, Settings::NativeButton::NumButtons> buttons_param; std::array<Common::ParamPackage, Settings::NativeAnalog::NumAnalogs> analogs_param; @@ -72,6 +74,9 @@ private: void loadConfiguration(); /// Restore all buttons to their default values. void restoreDefaults(); + /// Clear all input configuration + void ClearAll(); + /// Update UI to reflect current configuration. void updateButtonLabels(); diff --git a/src/yuzu/configuration/configure_input.ui b/src/yuzu/configuration/configure_input.ui index 8bfa5df62..8a019a693 100644 --- a/src/yuzu/configuration/configure_input.ui +++ b/src/yuzu/configuration/configure_input.ui @@ -695,6 +695,34 @@ Capture:</string> </spacer> </item> <item> + <widget class="QPushButton" name="buttonClearAll"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Minimum" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="sizeIncrement"> + <size> + <width>0</width> + <height>0</height> + </size> + </property> + <property name="baseSize"> + <size> + <width>0</width> + <height>0</height> + </size> + </property> + <property name="layoutDirection"> + <enum>Qt::LeftToRight</enum> + </property> + <property name="text"> + <string>Clear All</string> + </property> + </widget> + </item> + <item> <widget class="QPushButton" name="buttonRestoreDefaults"> <property name="sizePolicy"> <sizepolicy hsizetype="Minimum" vsizetype="Fixed"> diff --git a/src/yuzu/configuration/configure_system.cpp b/src/yuzu/configuration/configure_system.cpp index e9ed9c38f..b4b4a4a56 100644 --- a/src/yuzu/configuration/configure_system.cpp +++ b/src/yuzu/configuration/configure_system.cpp @@ -2,14 +2,27 @@ // Licensed under GPLv2 or any later version // Refer to the license.txt file included. +#include <algorithm> +#include <QFileDialog> +#include <QGraphicsItem> +#include <QGraphicsScene> +#include <QHeaderView> #include <QMessageBox> +#include <QStandardItemModel> +#include <QTreeView> +#include <QVBoxLayout> +#include "common/assert.h" +#include "common/file_util.h" +#include "common/string_util.h" #include "core/core.h" +#include "core/hle/service/acc/profile_manager.h" #include "core/settings.h" #include "ui_configure_system.h" #include "yuzu/configuration/configure_system.h" -#include "yuzu/main.h" +#include "yuzu/util/limitable_input_dialog.h" -static const std::array<int, 12> days_in_month = {{ +namespace { +constexpr std::array<int, 12> days_in_month = {{ 31, 29, 31, @@ -24,13 +37,108 @@ static const std::array<int, 12> days_in_month = {{ 31, }}; -ConfigureSystem::ConfigureSystem(QWidget* parent) : QWidget(parent), ui(new Ui::ConfigureSystem) { +// Same backup JPEG used by acc IProfile::GetImage if no jpeg found +constexpr std::array<u8, 107> backup_jpeg{ + 0xff, 0xd8, 0xff, 0xdb, 0x00, 0x43, 0x00, 0x03, 0x02, 0x02, 0x02, 0x02, 0x02, 0x03, 0x02, 0x02, + 0x02, 0x03, 0x03, 0x03, 0x03, 0x04, 0x06, 0x04, 0x04, 0x04, 0x04, 0x04, 0x08, 0x06, 0x06, 0x05, + 0x06, 0x09, 0x08, 0x0a, 0x0a, 0x09, 0x08, 0x09, 0x09, 0x0a, 0x0c, 0x0f, 0x0c, 0x0a, 0x0b, 0x0e, + 0x0b, 0x09, 0x09, 0x0d, 0x11, 0x0d, 0x0e, 0x0f, 0x10, 0x10, 0x11, 0x10, 0x0a, 0x0c, 0x12, 0x13, + 0x12, 0x10, 0x13, 0x0f, 0x10, 0x10, 0x10, 0xff, 0xc9, 0x00, 0x0b, 0x08, 0x00, 0x01, 0x00, 0x01, + 0x01, 0x01, 0x11, 0x00, 0xff, 0xcc, 0x00, 0x06, 0x00, 0x10, 0x10, 0x05, 0xff, 0xda, 0x00, 0x08, + 0x01, 0x01, 0x00, 0x00, 0x3f, 0x00, 0xd2, 0xcf, 0x20, 0xff, 0xd9, +}; + +QString GetImagePath(Service::Account::UUID uuid) { + const auto path = FileUtil::GetUserPath(FileUtil::UserPath::NANDDir) + + "/system/save/8000000000000010/su/avators/" + uuid.FormatSwitch() + ".jpg"; + return QString::fromStdString(path); +} + +QString GetAccountUsername(const Service::Account::ProfileManager& manager, + Service::Account::UUID uuid) { + Service::Account::ProfileBase profile; + if (!manager.GetProfileBase(uuid, profile)) { + return {}; + } + + const auto text = Common::StringFromFixedZeroTerminatedBuffer( + reinterpret_cast<const char*>(profile.username.data()), profile.username.size()); + return QString::fromStdString(text); +} + +QString FormatUserEntryText(const QString& username, Service::Account::UUID uuid) { + return ConfigureSystem::tr("%1\n%2", + "%1 is the profile username, %2 is the formatted UUID (e.g. " + "00112233-4455-6677-8899-AABBCCDDEEFF))") + .arg(username, QString::fromStdString(uuid.FormatSwitch())); +} + +QPixmap GetIcon(Service::Account::UUID uuid) { + QPixmap icon{GetImagePath(uuid)}; + + if (!icon) { + icon.fill(Qt::black); + icon.loadFromData(backup_jpeg.data(), static_cast<u32>(backup_jpeg.size())); + } + + return icon.scaled(64, 64, Qt::IgnoreAspectRatio, Qt::SmoothTransformation); +} + +QString GetProfileUsernameFromUser(QWidget* parent, const QString& description_text) { + return LimitableInputDialog::GetText(parent, ConfigureSystem::tr("Enter Username"), + description_text, 1, + static_cast<int>(Service::Account::profile_username_size)); +} +} // Anonymous namespace + +ConfigureSystem::ConfigureSystem(QWidget* parent) + : QWidget(parent), ui(new Ui::ConfigureSystem), + profile_manager(std::make_unique<Service::Account::ProfileManager>()) { ui->setupUi(this); connect(ui->combo_birthmonth, static_cast<void (QComboBox::*)(int)>(&QComboBox::currentIndexChanged), this, - &ConfigureSystem::updateBirthdayComboBox); + &ConfigureSystem::UpdateBirthdayComboBox); connect(ui->button_regenerate_console_id, &QPushButton::clicked, this, - &ConfigureSystem::refreshConsoleID); + &ConfigureSystem::RefreshConsoleID); + + layout = new QVBoxLayout; + tree_view = new QTreeView; + item_model = new QStandardItemModel(tree_view); + tree_view->setModel(item_model); + + tree_view->setAlternatingRowColors(true); + tree_view->setSelectionMode(QHeaderView::SingleSelection); + tree_view->setSelectionBehavior(QHeaderView::SelectRows); + tree_view->setVerticalScrollMode(QHeaderView::ScrollPerPixel); + tree_view->setHorizontalScrollMode(QHeaderView::ScrollPerPixel); + tree_view->setSortingEnabled(true); + tree_view->setEditTriggers(QHeaderView::NoEditTriggers); + tree_view->setUniformRowHeights(true); + tree_view->setIconSize({64, 64}); + tree_view->setContextMenuPolicy(Qt::NoContextMenu); + + item_model->insertColumns(0, 1); + item_model->setHeaderData(0, Qt::Horizontal, "Users"); + + // We must register all custom types with the Qt Automoc system so that we are able to use it + // with signals/slots. In this case, QList falls under the umbrells of custom types. + qRegisterMetaType<QList<QStandardItem*>>("QList<QStandardItem*>"); + + layout->setContentsMargins(0, 0, 0, 0); + layout->setSpacing(0); + layout->addWidget(tree_view); + + ui->scrollArea->setLayout(layout); + + connect(tree_view, &QTreeView::clicked, this, &ConfigureSystem::SelectUser); + + connect(ui->pm_add, &QPushButton::pressed, this, &ConfigureSystem::AddUser); + connect(ui->pm_rename, &QPushButton::pressed, this, &ConfigureSystem::RenameUser); + connect(ui->pm_remove, &QPushButton::pressed, this, &ConfigureSystem::DeleteUser); + connect(ui->pm_set_image, &QPushButton::pressed, this, &ConfigureSystem::SetUserImage); + + scene = new QGraphicsScene; + ui->current_user_icon->setScene(scene); this->setConfiguration(); } @@ -39,8 +147,45 @@ ConfigureSystem::~ConfigureSystem() = default; void ConfigureSystem::setConfiguration() { enabled = !Core::System::GetInstance().IsPoweredOn(); - ui->edit_username->setText(QString::fromStdString(Settings::values.username)); + ui->combo_language->setCurrentIndex(Settings::values.language_index); + + item_model->removeRows(0, item_model->rowCount()); + list_items.clear(); + + PopulateUserList(); + UpdateCurrentUser(); +} + +void ConfigureSystem::PopulateUserList() { + const auto& profiles = profile_manager->GetAllUsers(); + for (const auto& user : profiles) { + Service::Account::ProfileBase profile; + if (!profile_manager->GetProfileBase(user, profile)) + continue; + + const auto username = Common::StringFromFixedZeroTerminatedBuffer( + reinterpret_cast<const char*>(profile.username.data()), profile.username.size()); + + list_items.push_back(QList<QStandardItem*>{new QStandardItem{ + GetIcon(user), FormatUserEntryText(QString::fromStdString(username), user)}}); + } + + for (const auto& item : list_items) + item_model->appendRow(item); +} + +void ConfigureSystem::UpdateCurrentUser() { + ui->pm_add->setEnabled(profile_manager->GetUserCount() < Service::Account::MAX_USERS); + + const auto& current_user = profile_manager->GetUser(Settings::values.current_user); + ASSERT(current_user); + const auto username = GetAccountUsername(*profile_manager, *current_user); + + scene->clear(); + scene->addPixmap( + GetIcon(*current_user).scaled(48, 48, Qt::IgnoreAspectRatio, Qt::SmoothTransformation)); + ui->current_user_username->setText(username); } void ConfigureSystem::ReadSystemSettings() {} @@ -48,12 +193,12 @@ void ConfigureSystem::ReadSystemSettings() {} void ConfigureSystem::applyConfiguration() { if (!enabled) return; - Settings::values.username = ui->edit_username->text().toStdString(); + Settings::values.language_index = ui->combo_language->currentIndex(); Settings::Apply(); } -void ConfigureSystem::updateBirthdayComboBox(int birthmonth_index) { +void ConfigureSystem::UpdateBirthdayComboBox(int birthmonth_index) { if (birthmonth_index < 0 || birthmonth_index >= 12) return; @@ -78,7 +223,7 @@ void ConfigureSystem::updateBirthdayComboBox(int birthmonth_index) { ui->combo_birthday->setCurrentIndex(birthday_index); } -void ConfigureSystem::refreshConsoleID() { +void ConfigureSystem::RefreshConsoleID() { QMessageBox::StandardButton reply; QString warning_text = tr("This will replace your current virtual Switch with a new one. " "Your current virtual Switch will not be recoverable. " @@ -92,3 +237,130 @@ void ConfigureSystem::refreshConsoleID() { ui->label_console_id->setText( tr("Console ID: 0x%1").arg(QString::number(console_id, 16).toUpper())); } + +void ConfigureSystem::SelectUser(const QModelIndex& index) { + Settings::values.current_user = + std::clamp<std::size_t>(index.row(), 0, profile_manager->GetUserCount() - 1); + + UpdateCurrentUser(); + + ui->pm_remove->setEnabled(profile_manager->GetUserCount() >= 2); + ui->pm_rename->setEnabled(true); + ui->pm_set_image->setEnabled(true); +} + +void ConfigureSystem::AddUser() { + const auto username = + GetProfileUsernameFromUser(this, tr("Enter a username for the new user:")); + if (username.isEmpty()) { + return; + } + + const auto uuid = Service::Account::UUID::Generate(); + profile_manager->CreateNewUser(uuid, username.toStdString()); + + item_model->appendRow(new QStandardItem{GetIcon(uuid), FormatUserEntryText(username, uuid)}); +} + +void ConfigureSystem::RenameUser() { + const auto user = tree_view->currentIndex().row(); + const auto uuid = profile_manager->GetUser(user); + ASSERT(uuid); + + Service::Account::ProfileBase profile; + if (!profile_manager->GetProfileBase(*uuid, profile)) + return; + + const auto new_username = GetProfileUsernameFromUser(this, tr("Enter a new username:")); + if (new_username.isEmpty()) { + return; + } + + const auto username_std = new_username.toStdString(); + std::fill(profile.username.begin(), profile.username.end(), '\0'); + std::copy(username_std.begin(), username_std.end(), profile.username.begin()); + + profile_manager->SetProfileBase(*uuid, profile); + + item_model->setItem( + user, 0, + new QStandardItem{GetIcon(*uuid), + FormatUserEntryText(QString::fromStdString(username_std), *uuid)}); + UpdateCurrentUser(); +} + +void ConfigureSystem::DeleteUser() { + const auto index = tree_view->currentIndex().row(); + const auto uuid = profile_manager->GetUser(index); + ASSERT(uuid); + const auto username = GetAccountUsername(*profile_manager, *uuid); + + const auto confirm = QMessageBox::question( + this, tr("Confirm Delete"), + tr("You are about to delete user with name \"%1\". Are you sure?").arg(username)); + + if (confirm == QMessageBox::No) + return; + + if (Settings::values.current_user == tree_view->currentIndex().row()) + Settings::values.current_user = 0; + UpdateCurrentUser(); + + if (!profile_manager->RemoveUser(*uuid)) + return; + + item_model->removeRows(tree_view->currentIndex().row(), 1); + tree_view->clearSelection(); + + ui->pm_remove->setEnabled(false); + ui->pm_rename->setEnabled(false); +} + +void ConfigureSystem::SetUserImage() { + const auto index = tree_view->currentIndex().row(); + const auto uuid = profile_manager->GetUser(index); + ASSERT(uuid); + + const auto file = QFileDialog::getOpenFileName(this, tr("Select User Image"), QString(), + tr("JPEG Images (*.jpg *.jpeg)")); + + if (file.isEmpty()) { + return; + } + + const auto image_path = GetImagePath(*uuid); + if (QFile::exists(image_path) && !QFile::remove(image_path)) { + QMessageBox::warning( + this, tr("Error deleting image"), + tr("Error occurred attempting to overwrite previous image at: %1.").arg(image_path)); + return; + } + + const auto raw_path = QString::fromStdString( + FileUtil::GetUserPath(FileUtil::UserPath::NANDDir) + "/system/save/8000000000000010"); + const QFileInfo raw_info{raw_path}; + if (raw_info.exists() && !raw_info.isDir() && !QFile::remove(raw_path)) { + QMessageBox::warning(this, tr("Error deleting file"), + tr("Unable to delete existing file: %1.").arg(raw_path)); + return; + } + + const QString absolute_dst_path = QFileInfo{image_path}.absolutePath(); + if (!QDir{raw_path}.mkpath(absolute_dst_path)) { + QMessageBox::warning( + this, tr("Error creating user image directory"), + tr("Unable to create directory %1 for storing user images.").arg(absolute_dst_path)); + return; + } + + if (!QFile::copy(file, image_path)) { + QMessageBox::warning(this, tr("Error copying user image"), + tr("Unable to copy image from %1 to %2").arg(file, image_path)); + return; + } + + const auto username = GetAccountUsername(*profile_manager, *uuid); + item_model->setItem(index, 0, + new QStandardItem{GetIcon(*uuid), FormatUserEntryText(username, *uuid)}); + UpdateCurrentUser(); +} diff --git a/src/yuzu/configuration/configure_system.h b/src/yuzu/configuration/configure_system.h index f13de17d4..07764e1f7 100644 --- a/src/yuzu/configuration/configure_system.h +++ b/src/yuzu/configuration/configure_system.h @@ -5,8 +5,20 @@ #pragma once #include <memory> + +#include <QList> #include <QWidget> +class QGraphicsScene; +class QStandardItem; +class QStandardItemModel; +class QTreeView; +class QVBoxLayout; + +namespace Service::Account { +class ProfileManager; +} + namespace Ui { class ConfigureSystem; } @@ -16,23 +28,39 @@ class ConfigureSystem : public QWidget { public: explicit ConfigureSystem(QWidget* parent = nullptr); - ~ConfigureSystem(); + ~ConfigureSystem() override; void applyConfiguration(); void setConfiguration(); -public slots: - void updateBirthdayComboBox(int birthmonth_index); - void refreshConsoleID(); - private: void ReadSystemSettings(); + void UpdateBirthdayComboBox(int birthmonth_index); + void RefreshConsoleID(); + + void PopulateUserList(); + void UpdateCurrentUser(); + void SelectUser(const QModelIndex& index); + void AddUser(); + void RenameUser(); + void DeleteUser(); + void SetUserImage(); + + QVBoxLayout* layout; + QTreeView* tree_view; + QStandardItemModel* item_model; + QGraphicsScene* scene; + + std::vector<QList<QStandardItem*>> list_items; + std::unique_ptr<Ui::ConfigureSystem> ui; - bool enabled; + bool enabled = false; + + int birthmonth = 0; + int birthday = 0; + int language_index = 0; + int sound_index = 0; - std::u16string username; - int birthmonth, birthday; - int language_index; - int sound_index; + std::unique_ptr<Service::Account::ProfileManager> profile_manager; }; diff --git a/src/yuzu/configuration/configure_system.ui b/src/yuzu/configuration/configure_system.ui index f3f8db038..020b32a37 100644 --- a/src/yuzu/configuration/configure_system.ui +++ b/src/yuzu/configuration/configure_system.ui @@ -7,7 +7,7 @@ <x>0</x> <y>0</y> <width>360</width> - <height>377</height> + <height>483</height> </rect> </property> <property name="windowTitle"> @@ -22,34 +22,28 @@ <string>System Settings</string> </property> <layout class="QGridLayout" name="gridLayout"> - <item row="0" column="0"> - <widget class="QLabel" name="label_username"> + <item row="1" column="0"> + <widget class="QLabel" name="label_language"> <property name="text"> - <string>Username</string> + <string>Language</string> </property> </widget> </item> - <item row="0" column="1"> - <widget class="QLineEdit" name="edit_username"> - <property name="sizePolicy"> - <sizepolicy hsizetype="Preferred" vsizetype="Fixed"> - <horstretch>0</horstretch> - <verstretch>0</verstretch> - </sizepolicy> - </property> - <property name="maxLength"> - <number>32</number> + <item row="0" column="0"> + <widget class="QLabel" name="label_birthday"> + <property name="text"> + <string>Birthday</string> </property> </widget> </item> - <item row="1" column="0"> - <widget class="QLabel" name="label_birthday"> + <item row="3" column="0"> + <widget class="QLabel" name="label_console_id"> <property name="text"> - <string>Birthday</string> + <string>Console ID:</string> </property> </widget> </item> - <item row="1" column="1"> + <item row="0" column="1"> <layout class="QHBoxLayout" name="horizontalLayout_birthday2"> <item> <widget class="QComboBox" name="combo_birthmonth"> @@ -120,14 +114,7 @@ </item> </layout> </item> - <item row="2" column="0"> - <widget class="QLabel" name="label_language"> - <property name="text"> - <string>Language</string> - </property> - </widget> - </item> - <item row="2" column="1"> + <item row="1" column="1"> <widget class="QComboBox" name="combo_language"> <property name="toolTip"> <string>Note: this can be overridden when region setting is auto-select</string> @@ -187,31 +174,31 @@ <string>Russian (Русский)</string> </property> </item> - <item> - <property name="text"> - <string>Taiwanese</string> - </property> - </item> - <item> - <property name="text"> - <string>British English</string> - </property> - </item> - <item> - <property name="text"> - <string>Canadian French</string> - </property> - </item> - <item> - <property name="text"> - <string>Latin American Spanish</string> - </property> - </item> - <item> - <property name="text"> - <string>Simplified Chinese</string> - </property> - </item> + <item> + <property name="text"> + <string>Taiwanese</string> + </property> + </item> + <item> + <property name="text"> + <string>British English</string> + </property> + </item> + <item> + <property name="text"> + <string>Canadian French</string> + </property> + </item> + <item> + <property name="text"> + <string>Latin American Spanish</string> + </property> + </item> + <item> + <property name="text"> + <string>Simplified Chinese</string> + </property> + </item> <item> <property name="text"> <string>Traditional Chinese (正體中文)</string> @@ -219,14 +206,14 @@ </item> </widget> </item> - <item row="3" column="0"> + <item row="2" column="0"> <widget class="QLabel" name="label_sound"> <property name="text"> <string>Sound output mode</string> </property> </widget> </item> - <item row="3" column="1"> + <item row="2" column="1"> <widget class="QComboBox" name="combo_sound"> <item> <property name="text"> @@ -245,14 +232,7 @@ </item> </widget> </item> - <item row="4" column="0"> - <widget class="QLabel" name="label_console_id"> - <property name="text"> - <string>Console ID:</string> - </property> - </widget> - </item> - <item row="4" column="1"> + <item row="3" column="1"> <widget class="QPushButton" name="button_regenerate_console_id"> <property name="sizePolicy"> <sizepolicy hsizetype="Fixed" vsizetype="Fixed"> @@ -272,6 +252,143 @@ </widget> </item> <item> + <widget class="QGroupBox" name="gridGroupBox"> + <property name="title"> + <string>Profile Manager</string> + </property> + <layout class="QGridLayout" name="gridLayout_2"> + <property name="sizeConstraint"> + <enum>QLayout::SetNoConstraint</enum> + </property> + <item row="0" column="0"> + <layout class="QHBoxLayout" name="horizontalLayout_2"> + <item> + <widget class="QLabel" name="label"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Preferred" vsizetype="Minimum"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string>Current User</string> + </property> + </widget> + </item> + <item> + <widget class="QGraphicsView" name="current_user_icon"> + <property name="minimumSize"> + <size> + <width>48</width> + <height>48</height> + </size> + </property> + <property name="maximumSize"> + <size> + <width>48</width> + <height>48</height> + </size> + </property> + <property name="verticalScrollBarPolicy"> + <enum>Qt::ScrollBarAlwaysOff</enum> + </property> + <property name="horizontalScrollBarPolicy"> + <enum>Qt::ScrollBarAlwaysOff</enum> + </property> + <property name="interactive"> + <bool>false</bool> + </property> + </widget> + </item> + <item> + <widget class="QLabel" name="current_user_username"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Preferred" vsizetype="Minimum"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string>Username</string> + </property> + </widget> + </item> + </layout> + </item> + <item row="1" column="0"> + <widget class="QScrollArea" name="scrollArea"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Expanding" vsizetype="Expanding"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="frameShape"> + <enum>QFrame::StyledPanel</enum> + </property> + <property name="widgetResizable"> + <bool>false</bool> + </property> + </widget> + </item> + <item row="2" column="0"> + <layout class="QHBoxLayout" name="horizontalLayout_3"> + <item> + <widget class="QPushButton" name="pm_set_image"> + <property name="enabled"> + <bool>false</bool> + </property> + <property name="text"> + <string>Set Image</string> + </property> + </widget> + </item> + <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="pm_add"> + <property name="text"> + <string>Add</string> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="pm_rename"> + <property name="enabled"> + <bool>false</bool> + </property> + <property name="text"> + <string>Rename</string> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="pm_remove"> + <property name="enabled"> + <bool>false</bool> + </property> + <property name="text"> + <string>Remove</string> + </property> + </widget> + </item> + </layout> + </item> + </layout> + </widget> + </item> + <item> <widget class="QLabel" name="label_disable_info"> <property name="text"> <string>System settings are available only when game is not running.</string> @@ -281,19 +398,6 @@ </property> </widget> </item> - <item> - <spacer name="verticalSpacer"> - <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> </item> </layout> diff --git a/src/yuzu/debugger/graphics/graphics_breakpoints.cpp b/src/yuzu/debugger/graphics/graphics_breakpoints.cpp index b5c88f944..67ed0ba6d 100644 --- a/src/yuzu/debugger/graphics/graphics_breakpoints.cpp +++ b/src/yuzu/debugger/graphics/graphics_breakpoints.cpp @@ -2,7 +2,6 @@ // Licensed under GPLv2 or any later version // Refer to the license.txt file included. -#include <map> #include <QLabel> #include <QMetaType> #include <QPushButton> @@ -32,21 +31,8 @@ QVariant BreakPointModel::data(const QModelIndex& index, int role) const { switch (role) { case Qt::DisplayRole: { if (index.column() == 0) { - static const std::map<Tegra::DebugContext::Event, QString> map = { - {Tegra::DebugContext::Event::MaxwellCommandLoaded, tr("Maxwell command loaded")}, - {Tegra::DebugContext::Event::MaxwellCommandProcessed, - tr("Maxwell command processed")}, - {Tegra::DebugContext::Event::IncomingPrimitiveBatch, - tr("Incoming primitive batch")}, - {Tegra::DebugContext::Event::FinishedPrimitiveBatch, - tr("Finished primitive batch")}, - }; - - DEBUG_ASSERT(map.size() == - static_cast<std::size_t>(Tegra::DebugContext::Event::NumEvents)); - return (map.find(event) != map.end()) ? map.at(event) : QString(); + return DebugContextEventToString(event); } - break; } @@ -128,6 +114,23 @@ void BreakPointModel::OnResumed() { active_breakpoint = context->active_breakpoint; } +QString BreakPointModel::DebugContextEventToString(Tegra::DebugContext::Event event) { + switch (event) { + case Tegra::DebugContext::Event::MaxwellCommandLoaded: + return tr("Maxwell command loaded"); + case Tegra::DebugContext::Event::MaxwellCommandProcessed: + return tr("Maxwell command processed"); + case Tegra::DebugContext::Event::IncomingPrimitiveBatch: + return tr("Incoming primitive batch"); + case Tegra::DebugContext::Event::FinishedPrimitiveBatch: + return tr("Finished primitive batch"); + case Tegra::DebugContext::Event::NumEvents: + break; + } + + return tr("Unknown debug context event"); +} + GraphicsBreakPointsWidget::GraphicsBreakPointsWidget( std::shared_ptr<Tegra::DebugContext> debug_context, QWidget* parent) : QDockWidget(tr("Maxwell Breakpoints"), parent), Tegra::DebugContext::BreakPointObserver( diff --git a/src/yuzu/debugger/graphics/graphics_breakpoints_p.h b/src/yuzu/debugger/graphics/graphics_breakpoints_p.h index 7112b87e6..fb488e38f 100644 --- a/src/yuzu/debugger/graphics/graphics_breakpoints_p.h +++ b/src/yuzu/debugger/graphics/graphics_breakpoints_p.h @@ -29,6 +29,8 @@ public: void OnResumed(); private: + static QString DebugContextEventToString(Tegra::DebugContext::Event event); + std::weak_ptr<Tegra::DebugContext> context_weak; bool at_breakpoint; Tegra::DebugContext::Event active_breakpoint; diff --git a/src/yuzu/debugger/graphics/graphics_surface.cpp b/src/yuzu/debugger/graphics/graphics_surface.cpp index cbcd5dd5f..707747422 100644 --- a/src/yuzu/debugger/graphics/graphics_surface.cpp +++ b/src/yuzu/debugger/graphics/graphics_surface.cpp @@ -382,12 +382,13 @@ void GraphicsSurfaceWidget::OnUpdate() { // TODO: Implement a good way to visualize alpha components! QImage decoded_image(surface_width, surface_height, QImage::Format_ARGB32); - boost::optional<VAddr> address = gpu.MemoryManager().GpuToCpuAddress(surface_address); + std::optional<VAddr> address = gpu.MemoryManager().GpuToCpuAddress(surface_address); // TODO(bunnei): Will not work with BCn formats that swizzle 4x4 tiles. // Needs to be fixed if we plan to use this feature more, otherwise we may remove it. auto unswizzled_data = Tegra::Texture::UnswizzleTexture( - *address, 1, Tegra::Texture::BytesPerPixel(surface_format), surface_width, surface_height); + *address, 1, 1, Tegra::Texture::BytesPerPixel(surface_format), surface_width, + surface_height, 1U); auto texture_data = Tegra::Texture::DecodeTexture(unswizzled_data, surface_format, surface_width, surface_height); @@ -443,7 +444,7 @@ void GraphicsSurfaceWidget::SaveSurface() { pixmap->save(&file, "PNG"); } else if (selectedFilter == bin_filter) { auto& gpu = Core::System::GetInstance().GPU(); - boost::optional<VAddr> address = gpu.MemoryManager().GpuToCpuAddress(surface_address); + std::optional<VAddr> address = gpu.MemoryManager().GpuToCpuAddress(surface_address); const u8* buffer = Memory::GetPointer(*address); ASSERT_MSG(buffer != nullptr, "Memory not accessible"); diff --git a/src/yuzu/debugger/wait_tree.cpp b/src/yuzu/debugger/wait_tree.cpp index 4a09da685..0c831c9f4 100644 --- a/src/yuzu/debugger/wait_tree.cpp +++ b/src/yuzu/debugger/wait_tree.cpp @@ -9,8 +9,8 @@ #include "core/core.h" #include "core/hle/kernel/event.h" #include "core/hle/kernel/handle_table.h" -#include "core/hle/kernel/kernel.h" #include "core/hle/kernel/mutex.h" +#include "core/hle/kernel/process.h" #include "core/hle/kernel/scheduler.h" #include "core/hle/kernel/thread.h" #include "core/hle/kernel/timer.h" @@ -66,10 +66,11 @@ std::vector<std::unique_ptr<WaitTreeThread>> WaitTreeItem::MakeThreadItemList() } }; - add_threads(Core::System::GetInstance().Scheduler(0)->GetThreadList()); - add_threads(Core::System::GetInstance().Scheduler(1)->GetThreadList()); - add_threads(Core::System::GetInstance().Scheduler(2)->GetThreadList()); - add_threads(Core::System::GetInstance().Scheduler(3)->GetThreadList()); + const auto& system = Core::System::GetInstance(); + add_threads(system.Scheduler(0).GetThreadList()); + add_threads(system.Scheduler(1).GetThreadList()); + add_threads(system.Scheduler(2).GetThreadList()); + add_threads(system.Scheduler(3).GetThreadList()); return item_list; } @@ -82,7 +83,7 @@ QString WaitTreeText::GetText() const { } WaitTreeMutexInfo::WaitTreeMutexInfo(VAddr mutex_address) : mutex_address(mutex_address) { - auto& handle_table = Core::System::GetInstance().Kernel().HandleTable(); + const auto& handle_table = Core::CurrentProcess()->GetHandleTable(); mutex_value = Memory::Read32(mutex_address); owner_handle = static_cast<Kernel::Handle>(mutex_value & Kernel::Mutex::MutexOwnerMask); diff --git a/src/yuzu/debugger/wait_tree.h b/src/yuzu/debugger/wait_tree.h index defbf734f..331f89885 100644 --- a/src/yuzu/debugger/wait_tree.h +++ b/src/yuzu/debugger/wait_tree.h @@ -11,7 +11,6 @@ #include <QAbstractItemModel> #include <QDockWidget> #include <QTreeView> -#include <boost/container/flat_set.hpp> #include "common/common_types.h" #include "core/hle/kernel/object.h" diff --git a/src/yuzu/game_list.cpp b/src/yuzu/game_list.cpp index 67890455a..a5a4aa432 100644 --- a/src/yuzu/game_list.cpp +++ b/src/yuzu/game_list.cpp @@ -16,7 +16,6 @@ #include <fmt/format.h> #include "common/common_paths.h" #include "common/common_types.h" -#include "common/file_util.h" #include "common/logging/log.h" #include "core/file_sys/patch_manager.h" #include "yuzu/compatibility_list.h" @@ -217,11 +216,11 @@ GameList::GameList(FileSys::VirtualFilesystem vfs, GMainWindow* parent) tree_view->setContextMenuPolicy(Qt::CustomContextMenu); item_model->insertColumns(0, COLUMN_COUNT); - item_model->setHeaderData(COLUMN_NAME, Qt::Horizontal, "Name"); - item_model->setHeaderData(COLUMN_COMPATIBILITY, Qt::Horizontal, "Compatibility"); - item_model->setHeaderData(COLUMN_ADD_ONS, Qt::Horizontal, "Add-ons"); - item_model->setHeaderData(COLUMN_FILE_TYPE, Qt::Horizontal, "File type"); - item_model->setHeaderData(COLUMN_SIZE, Qt::Horizontal, "Size"); + item_model->setHeaderData(COLUMN_NAME, Qt::Horizontal, tr("Name")); + item_model->setHeaderData(COLUMN_COMPATIBILITY, Qt::Horizontal, tr("Compatibility")); + item_model->setHeaderData(COLUMN_ADD_ONS, Qt::Horizontal, tr("Add-ons")); + item_model->setHeaderData(COLUMN_FILE_TYPE, Qt::Horizontal, tr("File type")); + item_model->setHeaderData(COLUMN_SIZE, Qt::Horizontal, tr("Size")); connect(tree_view, &QTreeView::activated, this, &GameList::ValidateEntry); connect(tree_view, &QTreeView::customContextMenuRequested, this, &GameList::PopupContextMenu); @@ -387,9 +386,9 @@ void GameList::LoadCompatibilityList() { } void GameList::PopulateAsync(const QString& dir_path, bool deep_scan) { - if (!FileUtil::Exists(dir_path.toStdString()) || - !FileUtil::IsDirectory(dir_path.toStdString())) { - LOG_ERROR(Frontend, "Could not find game list folder at {}", dir_path.toLocal8Bit().data()); + const QFileInfo dir_info{dir_path}; + if (!dir_info.exists() || !dir_info.isDir()) { + LOG_ERROR(Frontend, "Could not find game list folder at {}", dir_path.toStdString()); search_field->setFilterResult(0, 0); return; } diff --git a/src/yuzu/game_list_worker.cpp b/src/yuzu/game_list_worker.cpp index 1947bdb93..3d865a12d 100644 --- a/src/yuzu/game_list_worker.cpp +++ b/src/yuzu/game_list_worker.cpp @@ -27,9 +27,8 @@ #include "yuzu/ui_settings.h" namespace { -void GetMetadataFromControlNCA(const FileSys::PatchManager& patch_manager, - const std::shared_ptr<FileSys::NCA>& nca, std::vector<u8>& icon, - std::string& name) { +void GetMetadataFromControlNCA(const FileSys::PatchManager& patch_manager, const FileSys::NCA& nca, + std::vector<u8>& icon, std::string& name) { auto [nacp, icon_file] = patch_manager.ParseControlNCA(nca); if (icon_file != nullptr) icon = icon_file->ReadAllBytes(); @@ -57,16 +56,30 @@ QString FormatGameName(const std::string& physical_name) { return physical_name_as_qstring; } -QString FormatPatchNameVersions(const FileSys::PatchManager& patch_manager, bool updatable = true) { +QString FormatPatchNameVersions(const FileSys::PatchManager& patch_manager, + Loader::AppLoader& loader, bool updatable = true) { QString out; - for (const auto& kv : patch_manager.GetPatchVersionNames()) { - if (!updatable && kv.first == "Update") + FileSys::VirtualFile update_raw; + loader.ReadUpdateRaw(update_raw); + for (const auto& kv : patch_manager.GetPatchVersionNames(update_raw)) { + const bool is_update = kv.first == "Update"; + if (!updatable && is_update) { continue; + } + + const QString type = QString::fromStdString(kv.first); if (kv.second.empty()) { - out.append(fmt::format("{}\n", kv.first).c_str()); + out.append(QStringLiteral("%1\n").arg(type)); } else { - out.append(fmt::format("{} ({})\n", kv.first, kv.second).c_str()); + auto ver = kv.second; + + // Display container name for packed updates + if (is_update && ver == "PACKED") { + ver = Loader::GetFileTypeString(loader.GetFileType()); + } + + out.append(QStringLiteral("%1 (%2)\n").arg(type, QString::fromStdString(ver))); } } @@ -88,7 +101,7 @@ void GameListWorker::AddInstalledTitlesToGameList() { FileSys::ContentRecordType::Program); for (const auto& game : installed_games) { - const auto& file = cache->GetEntryUnparsed(game); + const auto file = cache->GetEntryUnparsed(game); std::unique_ptr<Loader::AppLoader> loader = Loader::GetLoader(file); if (!loader) continue; @@ -99,9 +112,9 @@ void GameListWorker::AddInstalledTitlesToGameList() { loader->ReadProgramId(program_id); const FileSys::PatchManager patch{program_id}; - const auto& control = cache->GetEntry(game.title_id, FileSys::ContentRecordType::Control); + const auto control = cache->GetEntry(game.title_id, FileSys::ContentRecordType::Control); if (control != nullptr) - GetMetadataFromControlNCA(patch, control, icon, name); + GetMetadataFromControlNCA(patch, *control, icon, name); auto it = FindMatchingCompatibilityEntry(compatibility_list, program_id); @@ -116,7 +129,7 @@ void GameListWorker::AddInstalledTitlesToGameList() { QString::fromStdString(Loader::GetFileTypeString(loader->GetFileType())), program_id), new GameListItemCompat(compatibility), - new GameListItem(FormatPatchNameVersions(patch)), + new GameListItem(FormatPatchNameVersions(patch, *loader)), new GameListItem( QString::fromStdString(Loader::GetFileTypeString(loader->GetFileType()))), new GameListItemSize(file->GetSize()), @@ -127,9 +140,10 @@ void GameListWorker::AddInstalledTitlesToGameList() { FileSys::ContentRecordType::Control); for (const auto& entry : control_data) { - const auto nca = cache->GetEntry(entry); - if (nca != nullptr) - nca_control_map.insert_or_assign(entry.title_id, nca); + auto nca = cache->GetEntry(entry); + if (nca != nullptr) { + nca_control_map.insert_or_assign(entry.title_id, std::move(nca)); + } } } @@ -145,9 +159,11 @@ void GameListWorker::FillControlMap(const std::string& dir_path) { QFileInfo file_info(physical_name.c_str()); if (!is_dir && file_info.suffix().toStdString() == "nca") { auto nca = - std::make_shared<FileSys::NCA>(vfs->OpenFile(physical_name, FileSys::Mode::Read)); - if (nca->GetType() == FileSys::NCAContentType::Control) - nca_control_map.insert_or_assign(nca->GetTitleId(), nca); + std::make_unique<FileSys::NCA>(vfs->OpenFile(physical_name, FileSys::Mode::Read)); + if (nca->GetType() == FileSys::NCAContentType::Control) { + const u64 title_id = nca->GetTitleId(); + nca_control_map.insert_or_assign(title_id, std::move(nca)); + } } return true; }; @@ -188,8 +204,8 @@ void GameListWorker::AddFstEntriesToGameList(const std::string& dir_path, unsign res2 == Loader::ResultStatus::Success) { // Use from metadata pool. if (nca_control_map.find(program_id) != nca_control_map.end()) { - const auto nca = nca_control_map[program_id]; - GetMetadataFromControlNCA(patch, nca, icon, name); + const auto& nca = nca_control_map[program_id]; + GetMetadataFromControlNCA(patch, *nca, icon, name); } } @@ -206,7 +222,8 @@ void GameListWorker::AddFstEntriesToGameList(const std::string& dir_path, unsign QString::fromStdString(Loader::GetFileTypeString(loader->GetFileType())), program_id), new GameListItemCompat(compatibility), - new GameListItem(FormatPatchNameVersions(patch, loader->IsRomFSUpdatable())), + new GameListItem( + FormatPatchNameVersions(patch, *loader, loader->IsRomFSUpdatable())), new GameListItem( QString::fromStdString(Loader::GetFileTypeString(loader->GetFileType()))), new GameListItemSize(FileUtil::GetSize(physical_name)), diff --git a/src/yuzu/game_list_worker.h b/src/yuzu/game_list_worker.h index 09d20c42f..0e42d0bde 100644 --- a/src/yuzu/game_list_worker.h +++ b/src/yuzu/game_list_worker.h @@ -63,7 +63,7 @@ private: void AddFstEntriesToGameList(const std::string& dir_path, unsigned int recursion = 0); std::shared_ptr<FileSys::VfsFilesystem> vfs; - std::map<u64, std::shared_ptr<FileSys::NCA>> nca_control_map; + std::map<u64, std::unique_ptr<FileSys::NCA>> nca_control_map; QStringList watch_list; QString dir_path; bool deep_scan; diff --git a/src/yuzu/main.cpp b/src/yuzu/main.cpp index 1d06d6c95..74a44be37 100644 --- a/src/yuzu/main.cpp +++ b/src/yuzu/main.cpp @@ -10,6 +10,7 @@ // VFS includes must be before glad as they will conflict with Windows file api, which uses defines. #include "core/file_sys/vfs.h" #include "core/file_sys/vfs_real.h" +#include "core/hle/service/acc/profile_manager.h" // These are wrappers to avoid the calls to CreateDirectory and CreateFile becuase of the Windows // defines. @@ -29,8 +30,10 @@ static FileSys::VirtualFile VfsDirectoryCreateFileWrapper(const FileSys::Virtual #define QT_NO_OPENGL #include <QDesktopWidget> #include <QDialogButtonBox> +#include <QFile> #include <QFileDialog> #include <QMessageBox> +#include <QtConcurrent/QtConcurrent> #include <QtGui> #include <QtWidgets> #include <fmt/format.h> @@ -59,6 +62,8 @@ static FileSys::VirtualFile VfsDirectoryCreateFileWrapper(const FileSys::Virtual #include "core/hle/kernel/process.h" #include "core/hle/service/filesystem/filesystem.h" #include "core/hle/service/filesystem/fsp_ldr.h" +#include "core/hle/service/nfp/nfp.h" +#include "core/hle/service/sm/sm.h" #include "core/loader/loader.h" #include "core/perf_stats.h" #include "core/settings.h" @@ -99,6 +104,8 @@ __declspec(dllexport) int AmdPowerXpressRequestHighPerformance = 1; } #endif +constexpr u64 DLC_BASE_TITLE_ID_MASK = 0xFFFFFFFFFFFFE000; + /** * "Callouts" are one-time instructional messages shown to the user. In the config settings, there * is a bitfield "callout_flags" options, used to track if a message has already been shown to the @@ -174,8 +181,11 @@ GMainWindow::GMainWindow() .arg(Common::g_build_fullname, Common::g_scm_branch, Common::g_scm_desc)); show(); + // Gen keys if necessary + OnReinitializeKeys(ReinitializeKeyBehavior::NoWarning); + // Necessary to load titles from nand in gamelist. - Service::FileSystem::CreateFactories(vfs); + Service::FileSystem::CreateFactories(*vfs); game_list->LoadCompatibilityList(); game_list->PopulateAsync(UISettings::values.gamedir, UISettings::values.gamedir_deepscan); @@ -421,6 +431,7 @@ void GMainWindow::ConnectMenuEvents() { connect(ui.action_Select_SDMC_Directory, &QAction::triggered, this, [this] { OnMenuSelectEmulatedDirectory(EmulatedDirectoryTarget::SDMC); }); connect(ui.action_Exit, &QAction::triggered, this, &QMainWindow::close); + connect(ui.action_Load_Amiibo, &QAction::triggered, this, &GMainWindow::OnLoadAmiibo); // Emulation connect(ui.action_Start, &QAction::triggered, this, &GMainWindow::OnStartGame); @@ -446,6 +457,9 @@ void GMainWindow::ConnectMenuEvents() { connect(ui.action_Fullscreen, &QAction::triggered, this, &GMainWindow::ToggleFullscreen); // Help + connect(ui.action_Open_yuzu_Folder, &QAction::triggered, this, &GMainWindow::OnOpenYuzuFolder); + connect(ui.action_Rederive, &QAction::triggered, this, + std::bind(&GMainWindow::OnReinitializeKeys, this, ReinitializeKeyBehavior::Warning)); connect(ui.action_About, &QAction::triggered, this, &GMainWindow::OnAbout); } @@ -488,6 +502,8 @@ QStringList GMainWindow::GetUnsupportedGLExtensions() { unsupported_ext.append("ARB_texture_storage"); if (!GLAD_GL_ARB_multi_bind) unsupported_ext.append("ARB_multi_bind"); + if (!GLAD_GL_ARB_copy_image) + unsupported_ext.append("ARB_copy_image"); // Extensions required to support some texture formats. if (!GLAD_GL_EXT_texture_compression_s3tc) @@ -685,6 +701,7 @@ void GMainWindow::ShutdownGame() { ui.action_Stop->setEnabled(false); ui.action_Restart->setEnabled(false); ui.action_Report_Compatibility->setEnabled(false); + ui.action_Load_Amiibo->setEnabled(false); render_window->hide(); game_list->show(); game_list->setFilterFocus(); @@ -746,12 +763,43 @@ void GMainWindow::OnGameListOpenFolder(u64 program_id, GameListOpenTarget target open_target = "Save Data"; const std::string nand_dir = FileUtil::GetUserPath(FileUtil::UserPath::NANDDir); ASSERT(program_id != 0); - // TODO(tech4me): Update this to work with arbitrary user profile - // Refer to core/hle/service/acc/profile_manager.cpp ProfileManager constructor - constexpr u128 user_id = {1, 0}; + + Service::Account::ProfileManager manager{}; + const auto user_ids = manager.GetAllUsers(); + QStringList list; + for (const auto& user_id : user_ids) { + if (user_id == Service::Account::UUID{}) + continue; + Service::Account::ProfileBase base; + if (!manager.GetProfileBase(user_id, base)) + continue; + + list.push_back(QString::fromStdString(Common::StringFromFixedZeroTerminatedBuffer( + reinterpret_cast<const char*>(base.username.data()), base.username.size()))); + } + + bool ok = false; + const auto index_string = + QInputDialog::getItem(this, tr("Select User"), + tr("Please select the user's save data you would like to open."), + list, Settings::values.current_user, false, &ok); + if (!ok) + return; + + const auto index = list.indexOf(index_string); + ASSERT(index != -1 && index < 8); + + const auto user_id = manager.GetUser(index); + ASSERT(user_id); path = nand_dir + FileSys::SaveDataFactory::GetFullPath(FileSys::SaveDataSpaceId::NandUser, FileSys::SaveDataType::SaveData, - program_id, user_id, 0); + program_id, user_id->uuid, 0); + + if (!FileUtil::Exists(path)) { + FileUtil::CreateFullPath(path); + FileUtil::CreateDir(path); + } + break; } case GameListOpenTarget::ModData: { @@ -818,14 +866,10 @@ static bool RomFSRawCopy(QProgressDialog& dialog, const FileSys::VirtualDir& src } void GMainWindow::OnGameListDumpRomFS(u64 program_id, const std::string& game_path) { - const auto path = fmt::format("{}{:016X}/romfs", - FileUtil::GetUserPath(FileUtil::UserPath::DumpDir), program_id); - - const auto failed = [this, &path] { + const auto failed = [this] { QMessageBox::warning(this, tr("RomFS Extraction Failed!"), tr("There was an error copying the RomFS files or the user " "cancelled the operation.")); - vfs->DeleteDirectory(path); }; const auto loader = Loader::GetLoader(vfs->OpenFile(game_path, FileSys::Mode::Read)); @@ -840,10 +884,24 @@ void GMainWindow::OnGameListDumpRomFS(u64 program_id, const std::string& game_pa return; } - const auto romfs = - loader->IsRomFSUpdatable() - ? FileSys::PatchManager(program_id).PatchRomFS(file, loader->ReadRomFSIVFCOffset()) - : file; + const auto installed = Service::FileSystem::GetUnionContents(); + auto romfs_title_id = SelectRomFSDumpTarget(*installed, program_id); + + if (!romfs_title_id) { + failed(); + return; + } + + const auto path = fmt::format( + "{}{:016X}/romfs", FileUtil::GetUserPath(FileUtil::UserPath::DumpDir), *romfs_title_id); + + FileSys::VirtualFile romfs; + + if (*romfs_title_id == program_id) { + romfs = file; + } else { + romfs = installed->GetEntry(*romfs_title_id, FileSys::ContentRecordType::Data)->GetRomFS(); + } const auto extracted = FileSys::ExtractRomFS(romfs, FileSys::RomFSExtractionType::Full); if (extracted == nullptr) { @@ -855,6 +913,7 @@ void GMainWindow::OnGameListDumpRomFS(u64 program_id, const std::string& game_pa if (out == nullptr) { failed(); + vfs->DeleteDirectory(path); return; } @@ -865,8 +924,11 @@ void GMainWindow::OnGameListDumpRomFS(u64 program_id, const std::string& game_pa "files into the new directory while <br>skeleton will only create the directory " "structure."), {"Full", "Skeleton"}, 0, false, &ok); - if (!ok) + if (!ok) { failed(); + vfs->DeleteDirectory(path); + return; + } const auto full = res == "Full"; const auto entry_size = CalculateRomFSEntrySize(extracted, full); @@ -883,6 +945,7 @@ void GMainWindow::OnGameListDumpRomFS(u64 program_id, const std::string& game_pa } else { progress.close(); failed(); + vfs->DeleteDirectory(path); } } @@ -903,22 +966,20 @@ void GMainWindow::OnGameListNavigateToGamedbEntry(u64 program_id, } void GMainWindow::OnMenuLoadFile() { - QString extensions; - for (const auto& piece : game_list->supported_file_extensions) - extensions += "*." + piece + " "; - - extensions += "main "; - - QString file_filter = tr("Switch Executable") + " (" + extensions + ")"; - file_filter += ";;" + tr("All Files (*.*)"); - - QString filename = QFileDialog::getOpenFileName(this, tr("Load File"), - UISettings::values.roms_path, file_filter); - if (!filename.isEmpty()) { - UISettings::values.roms_path = QFileInfo(filename).path(); + const QString extensions = + QString("*.").append(GameList::supported_file_extensions.join(" *.")).append(" main"); + const QString file_filter = tr("Switch Executable (%1);;All Files (*.*)", + "%1 is an identifier for the Switch executable file extensions.") + .arg(extensions); + const QString filename = QFileDialog::getOpenFileName( + this, tr("Load File"), UISettings::values.roms_path, file_filter); - BootGame(filename); + if (filename.isEmpty()) { + return; } + + UISettings::values.roms_path = QFileInfo(filename).path(); + BootGame(filename); } void GMainWindow::OnMenuLoadFolder() { @@ -1134,7 +1195,7 @@ void GMainWindow::OnMenuSelectEmulatedDirectory(EmulatedDirectoryTarget target) FileUtil::GetUserPath(target == EmulatedDirectoryTarget::SDMC ? FileUtil::UserPath::SDMCDir : FileUtil::UserPath::NANDDir, dir_path.toStdString()); - Service::FileSystem::CreateFactories(vfs); + Service::FileSystem::CreateFactories(*vfs); game_list->PopulateAsync(UISettings::values.gamedir, UISettings::values.gamedir_deepscan); } } @@ -1171,6 +1232,7 @@ void GMainWindow::OnStartGame() { ui.action_Report_Compatibility->setEnabled(true); discord_rpc->Update(); + ui.action_Load_Amiibo->setEnabled(true); } void GMainWindow::OnPauseGame() { @@ -1275,6 +1337,52 @@ void GMainWindow::OnConfigure() { } } +void GMainWindow::OnLoadAmiibo() { + const QString extensions{"*.bin"}; + const QString file_filter = tr("Amiibo File (%1);; All Files (*.*)").arg(extensions); + const QString filename = QFileDialog::getOpenFileName(this, tr("Load Amiibo"), "", file_filter); + + if (filename.isEmpty()) { + return; + } + + Core::System& system{Core::System::GetInstance()}; + Service::SM::ServiceManager& sm = system.ServiceManager(); + auto nfc = sm.GetService<Service::NFP::Module::Interface>("nfp:user"); + if (nfc == nullptr) { + return; + } + + QFile nfc_file{filename}; + if (!nfc_file.open(QIODevice::ReadOnly)) { + QMessageBox::warning(this, tr("Error opening Amiibo data file"), + tr("Unable to open Amiibo file \"%1\" for reading.").arg(filename)); + return; + } + + const u64 nfc_file_size = nfc_file.size(); + std::vector<u8> buffer(nfc_file_size); + const u64 read_size = nfc_file.read(reinterpret_cast<char*>(buffer.data()), nfc_file_size); + if (nfc_file_size != read_size) { + QMessageBox::warning(this, tr("Error reading Amiibo data file"), + tr("Unable to fully read Amiibo data. Expected to read %1 bytes, but " + "was only able to read %2 bytes.") + .arg(nfc_file_size) + .arg(read_size)); + return; + } + + if (!nfc->LoadAmiibo(buffer)) { + QMessageBox::warning(this, tr("Error loading Amiibo data"), + tr("Unable to load Amiibo data.")); + } +} + +void GMainWindow::OnOpenYuzuFolder() { + QDesktopServices::openUrl(QUrl::fromLocalFile( + QString::fromStdString(FileUtil::GetUserPath(FileUtil::UserPath::UserDir)))); +} + void GMainWindow::OnAbout() { AboutDialog aboutDialog(this); aboutDialog.exec(); @@ -1315,15 +1423,17 @@ void GMainWindow::UpdateStatusBar() { void GMainWindow::OnCoreError(Core::System::ResultStatus result, std::string details) { QMessageBox::StandardButton answer; QString status_message; - const QString common_message = tr( - "The game you are trying to load requires additional files from your Switch to be dumped " - "before playing.<br/><br/>For more information on dumping these files, please see the " - "following wiki page: <a " - "href='https://yuzu-emu.org/wiki/" - "dumping-system-archives-and-the-shared-fonts-from-a-switch-console/'>Dumping System " - "Archives and the Shared Fonts from a Switch Console</a>.<br/><br/>Would you like to quit " - "back to the game list? Continuing emulation may result in crashes, corrupted save " - "data, or other bugs."); + const QString common_message = + tr("The game you are trying to load requires additional files from your Switch to be " + "dumped " + "before playing.<br/><br/>For more information on dumping these files, please see the " + "following wiki page: <a " + "href='https://yuzu-emu.org/wiki/" + "dumping-system-archives-and-the-shared-fonts-from-a-switch-console/'>Dumping System " + "Archives and the Shared Fonts from a Switch Console</a>.<br/><br/>Would you like to " + "quit " + "back to the game list? Continuing emulation may result in crashes, corrupted save " + "data, or other bugs."); switch (result) { case Core::System::ResultStatus::ErrorSystemFiles: { QString message = "yuzu was unable to locate a Switch system archive"; @@ -1354,9 +1464,12 @@ void GMainWindow::OnCoreError(Core::System::ResultStatus result, std::string det this, tr("Fatal Error"), tr("yuzu has encountered a fatal error, please see the log for more details. " "For more information on accessing the log, please see the following page: " - "<a href='https://community.citra-emu.org/t/how-to-upload-the-log-file/296'>How to " - "Upload the Log File</a>.<br/><br/>Would you like to quit back to the game list? " - "Continuing emulation may result in crashes, corrupted save data, or other bugs."), + "<a href='https://community.citra-emu.org/t/how-to-upload-the-log-file/296'>How " + "to " + "Upload the Log File</a>.<br/><br/>Would you like to quit back to the game " + "list? " + "Continuing emulation may result in crashes, corrupted save data, or other " + "bugs."), QMessageBox::Yes | QMessageBox::No, QMessageBox::No); status_message = "Fatal Error encountered"; break; @@ -1376,6 +1489,122 @@ void GMainWindow::OnCoreError(Core::System::ResultStatus result, std::string det } } +void GMainWindow::OnReinitializeKeys(ReinitializeKeyBehavior behavior) { + if (behavior == ReinitializeKeyBehavior::Warning) { + const auto res = QMessageBox::information( + this, tr("Confirm Key Rederivation"), + tr("You are about to force rederive all of your keys. \nIf you do not know what this " + "means or what you are doing, \nthis is a potentially destructive action. \nPlease " + "make " + "sure this is what you want \nand optionally make backups.\n\nThis will delete your " + "autogenerated key files and re-run the key derivation module."), + QMessageBox::StandardButtons{QMessageBox::Ok, QMessageBox::Cancel}); + + if (res == QMessageBox::Cancel) + return; + + FileUtil::Delete(FileUtil::GetUserPath(FileUtil::UserPath::KeysDir) + + "prod.keys_autogenerated"); + FileUtil::Delete(FileUtil::GetUserPath(FileUtil::UserPath::KeysDir) + + "console.keys_autogenerated"); + FileUtil::Delete(FileUtil::GetUserPath(FileUtil::UserPath::KeysDir) + + "title.keys_autogenerated"); + } + + Core::Crypto::KeyManager keys{}; + if (keys.BaseDeriveNecessary()) { + Core::Crypto::PartitionDataManager pdm{vfs->OpenDirectory( + FileUtil::GetUserPath(FileUtil::UserPath::SysDataDir), FileSys::Mode::Read)}; + + const auto function = [this, &keys, &pdm] { + keys.PopulateFromPartitionData(pdm); + Service::FileSystem::CreateFactories(*vfs); + keys.DeriveETicket(pdm); + }; + + QString errors; + + if (!pdm.HasFuses()) + errors += tr("- Missing fuses - Cannot derive SBK\n"); + if (!pdm.HasBoot0()) + errors += tr("- Missing BOOT0 - Cannot derive master keys\n"); + if (!pdm.HasPackage2()) + errors += tr("- Missing BCPKG2-1-Normal-Main - Cannot derive general keys\n"); + if (!pdm.HasProdInfo()) + errors += tr("- Missing PRODINFO - Cannot derive title keys\n"); + + if (!errors.isEmpty()) { + + QMessageBox::warning( + this, tr("Warning Missing Derivation Components"), + tr("The following are missing from your configuration that may hinder key " + "derivation. It will be attempted but may not complete.<br><br>") + + errors + + tr("<br><br>You can get all of these and dump all of your games easily by " + "following <a href='https://yuzu-emu.org/help/quickstart/'>the " + "quickstart guide</a>. Alternatively, you can use another method of dumping " + "to obtain all of your keys.")); + } + + QProgressDialog prog; + prog.setRange(0, 0); + prog.setLabelText(tr("Deriving keys...\nThis may take up to a minute depending \non your " + "system's performance.")); + prog.setWindowTitle(tr("Deriving Keys")); + + prog.show(); + + auto future = QtConcurrent::run(function); + while (!future.isFinished()) { + QCoreApplication::processEvents(); + } + + prog.close(); + } + + Service::FileSystem::CreateFactories(*vfs); + + if (behavior == ReinitializeKeyBehavior::Warning) { + game_list->PopulateAsync(UISettings::values.gamedir, UISettings::values.gamedir_deepscan); + } +} + +std::optional<u64> GMainWindow::SelectRomFSDumpTarget( + const FileSys::RegisteredCacheUnion& installed, u64 program_id) { + const auto dlc_entries = + installed.ListEntriesFilter(FileSys::TitleType::AOC, FileSys::ContentRecordType::Data); + std::vector<FileSys::RegisteredCacheEntry> dlc_match; + dlc_match.reserve(dlc_entries.size()); + std::copy_if(dlc_entries.begin(), dlc_entries.end(), std::back_inserter(dlc_match), + [&program_id, &installed](const FileSys::RegisteredCacheEntry& entry) { + return (entry.title_id & DLC_BASE_TITLE_ID_MASK) == program_id && + installed.GetEntry(entry)->GetStatus() == Loader::ResultStatus::Success; + }); + + std::vector<u64> romfs_tids; + romfs_tids.push_back(program_id); + for (const auto& entry : dlc_match) + romfs_tids.push_back(entry.title_id); + + if (romfs_tids.size() > 1) { + QStringList list{"Base"}; + for (std::size_t i = 1; i < romfs_tids.size(); ++i) + list.push_back(QStringLiteral("DLC %1").arg(romfs_tids[i] & 0x7FF)); + + bool ok; + const auto res = QInputDialog::getItem( + this, tr("Select RomFS Dump Target"), + tr("Please select which RomFS you would like to dump."), list, 0, false, &ok); + if (!ok) { + return {}; + } + + return romfs_tids[list.indexOf(res)]; + } + + return program_id; +} + bool GMainWindow::ConfirmClose() { if (emu_thread == nullptr || !UISettings::values.confirm_before_closing) return true; @@ -1484,7 +1713,7 @@ void GMainWindow::UpdateUITheme() { emit UpdateThemedIcons(); } -void GMainWindow::SetDiscordEnabled(bool state) { +void GMainWindow::SetDiscordEnabled([[maybe_unused]] bool state) { #ifdef USE_DISCORD_PRESENCE if (state) { discord_rpc = std::make_unique<DiscordRPC::DiscordImpl>(); diff --git a/src/yuzu/main.h b/src/yuzu/main.h index fe0e9a50a..929250e8c 100644 --- a/src/yuzu/main.h +++ b/src/yuzu/main.h @@ -5,6 +5,7 @@ #pragma once #include <memory> +#include <optional> #include <unordered_map> #include <QMainWindow> @@ -29,8 +30,9 @@ class WaitTreeWidget; enum class GameListOpenTarget; namespace FileSys { +class RegisteredCacheUnion; class VfsFilesystem; -} +} // namespace FileSys namespace Tegra { class DebugContext; @@ -41,6 +43,11 @@ enum class EmulatedDirectoryTarget { SDMC, }; +enum class ReinitializeKeyBehavior { + NoWarning, + Warning, +}; + namespace DiscordRPC { class DiscordInterface; } @@ -159,6 +166,8 @@ private slots: void OnMenuSelectEmulatedDirectory(EmulatedDirectoryTarget target); void OnMenuRecentFile(); void OnConfigure(); + void OnLoadAmiibo(); + void OnOpenYuzuFolder(); void OnAbout(); void OnToggleFilterBar(); void OnDisplayTitleBars(bool); @@ -167,8 +176,10 @@ private slots: void HideFullscreen(); void ToggleWindowMode(); void OnCoreError(Core::System::ResultStatus, std::string); + void OnReinitializeKeys(ReinitializeKeyBehavior behavior); private: + std::optional<u64> SelectRomFSDumpTarget(const FileSys::RegisteredCacheUnion&, u64 program_id); void UpdateStatusBar(); Ui::MainWindow ui; diff --git a/src/yuzu/main.ui b/src/yuzu/main.ui index cb1664b21..28cf269e7 100644 --- a/src/yuzu/main.ui +++ b/src/yuzu/main.ui @@ -57,8 +57,8 @@ <string>Recent Files</string> </property> </widget> - <addaction name="action_Install_File_NAND" /> - <addaction name="separator"/> + <addaction name="action_Install_File_NAND"/> + <addaction name="separator"/> <addaction name="action_Load_File"/> <addaction name="action_Load_Folder"/> <addaction name="separator"/> @@ -68,6 +68,8 @@ <addaction name="action_Select_NAND_Directory"/> <addaction name="action_Select_SDMC_Directory"/> <addaction name="separator"/> + <addaction name="action_Load_Amiibo"/> + <addaction name="separator"/> <addaction name="action_Exit"/> </widget> <widget class="QMenu" name="menu_Emulation"> @@ -97,24 +99,35 @@ <addaction name="action_Show_Status_Bar"/> <addaction name="menu_View_Debugging"/> </widget> + <widget class ="QMenu" name="menu_Tools"> + <property name="title"> + <string>Tools</string> + </property> + <addaction name="action_Rederive" /> + </widget> <widget class="QMenu" name="menu_Help"> <property name="title"> <string>&Help</string> </property> <addaction name="action_Report_Compatibility"/> + <addaction name="action_Open_yuzu_Folder" /> <addaction name="separator"/> <addaction name="action_About"/> </widget> <addaction name="menu_File"/> <addaction name="menu_Emulation"/> <addaction name="menu_View"/> + <addaction name="menu_Tools" /> <addaction name="menu_Help"/> </widget> - <action name="action_Install_File_NAND"> - <property name="text"> - <string>Install File to NAND...</string> - </property> - </action> + <action name="action_Install_File_NAND"> + <property name="enabled"> + <bool>true</bool> + </property> + <property name="text"> + <string>Install File to NAND...</string> + </property> + </action> <action name="action_Load_File"> <property name="text"> <string>Load File...</string> @@ -159,6 +172,11 @@ <string>&Stop</string> </property> </action> + <action name="action_Rederive"> + <property name="text"> + <string>Reinitialize keys...</string> + </property> + </action> <action name="action_About"> <property name="text"> <string>About yuzu</string> @@ -241,6 +259,14 @@ <string>Restart</string> </property> </action> + <action name="action_Load_Amiibo"> + <property name="enabled"> + <bool>false</bool> + </property> + <property name="text"> + <string>Load Amiibo...</string> + </property> + </action> <action name="action_Report_Compatibility"> <property name="enabled"> <bool>false</bool> @@ -252,6 +278,11 @@ <bool>false</bool> </property> </action> + <action name="action_Open_yuzu_Folder"> + <property name="text"> + <string>Open yuzu Folder</string> + </property> + </action> </widget> <resources/> <connections/> diff --git a/src/yuzu/util/limitable_input_dialog.cpp b/src/yuzu/util/limitable_input_dialog.cpp new file mode 100644 index 000000000..edd78e579 --- /dev/null +++ b/src/yuzu/util/limitable_input_dialog.cpp @@ -0,0 +1,59 @@ +// Copyright 2018 yuzu Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include <QDialogButtonBox> +#include <QLabel> +#include <QLineEdit> +#include <QPushButton> +#include <QVBoxLayout> +#include "yuzu/util/limitable_input_dialog.h" + +LimitableInputDialog::LimitableInputDialog(QWidget* parent) : QDialog{parent} { + CreateUI(); + ConnectEvents(); +} + +LimitableInputDialog::~LimitableInputDialog() = default; + +void LimitableInputDialog::CreateUI() { + setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); + + text_label = new QLabel(this); + text_entry = new QLineEdit(this); + buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this); + + auto* const layout = new QVBoxLayout; + layout->addWidget(text_label); + layout->addWidget(text_entry); + layout->addWidget(buttons); + + setLayout(layout); +} + +void LimitableInputDialog::ConnectEvents() { + connect(buttons, &QDialogButtonBox::accepted, this, &QDialog::accept); + connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject); +} + +QString LimitableInputDialog::GetText(QWidget* parent, const QString& title, const QString& text, + int min_character_limit, int max_character_limit) { + Q_ASSERT(min_character_limit <= max_character_limit); + + LimitableInputDialog dialog{parent}; + dialog.setWindowTitle(title); + dialog.text_label->setText(text); + dialog.text_entry->setMaxLength(max_character_limit); + + auto* const ok_button = dialog.buttons->button(QDialogButtonBox::Ok); + ok_button->setEnabled(false); + connect(dialog.text_entry, &QLineEdit::textEdited, [&](const QString& new_text) { + ok_button->setEnabled(new_text.length() >= min_character_limit); + }); + + if (dialog.exec() != QDialog::Accepted) { + return {}; + } + + return dialog.text_entry->text(); +} diff --git a/src/yuzu/util/limitable_input_dialog.h b/src/yuzu/util/limitable_input_dialog.h new file mode 100644 index 000000000..164ad7301 --- /dev/null +++ b/src/yuzu/util/limitable_input_dialog.h @@ -0,0 +1,31 @@ +// Copyright 2018 yuzu Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include <QDialog> + +class QDialogButtonBox; +class QLabel; +class QLineEdit; + +/// A QDialog that functions similarly to QInputDialog, however, it allows +/// restricting the minimum and total number of characters that can be entered. +class LimitableInputDialog final : public QDialog { + Q_OBJECT +public: + explicit LimitableInputDialog(QWidget* parent = nullptr); + ~LimitableInputDialog() override; + + static QString GetText(QWidget* parent, const QString& title, const QString& text, + int min_character_limit, int max_character_limit); + +private: + void CreateUI(); + void ConnectEvents(); + + QLabel* text_label; + QLineEdit* text_entry; + QDialogButtonBox* buttons; +}; |
