eden/src/yuzu/applets/qt_profile_select.cpp
crueter 08f3639c80
[desktop, fs] main_window separation; fix Ryujinx save data link issues (#2929)
Some genius decided to put the entire MainWindow class into main.h and
main.cpp, which is not only horrific practice but also completely
destroys clangd beyond repair. Please, just don't do this.

(this will probably merge conflict to hell and back)

Also, fixes a bunch of issues with Ryujinx save data link:
- Paths with spaces would cause mklink to fail
- Add support for portable directories
- Symlink detection was incorrect sometimes(????)
- Some other stuff I'm forgetting

Furthermore, when selecting "From Eden" and attempting to save in Ryujinx, Ryujinx would destroy the link for... some reason? So to get around this we just copy the Eden data to Ryujinx then treat it like a "From Ryujinx" op

Signed-off-by: crueter <crueter@eden-emu.dev>
Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/2929
Reviewed-by: Lizzie <lizzie@eden-emu.dev>
Reviewed-by: CamilleLaVey <camillelavey99@gmail.com>
2025-11-09 18:07:38 +01:00

260 lines
9.4 KiB
C++

// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <mutex>
#include <QApplication>
#include <QDialogButtonBox>
#include <QHeaderView>
#include <QLabel>
#include <QLineEdit>
#include <QScrollArea>
#include <QStandardItemModel>
#include <QTreeView>
#include <QVBoxLayout>
#include "common/fs/path_util.h"
#include "common/string_util.h"
#include "core/constants.h"
#include "core/core.h"
#include "core/hle/service/acc/profile_manager.h"
#include "yuzu/applets/qt_profile_select.h"
#include "yuzu/main_window.h"
#include "yuzu/util/controller_navigation.h"
namespace {
QString FormatUserEntryText(const QString& username, Common::UUID uuid) {
return QtProfileSelectionDialog::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.FormattedString()));
}
QString GetImagePath(Common::UUID uuid) {
const auto path =
Common::FS::GetEdenPath(Common::FS::EdenPath::NANDDir) /
fmt::format("system/save/8000000000000010/su/avators/{}.jpg", uuid.FormattedString());
return QString::fromStdString(Common::FS::PathToUTF8String(path));
}
QPixmap GetIcon(Common::UUID uuid) {
QPixmap icon{GetImagePath(uuid)};
if (!icon) {
icon.fill(Qt::black);
icon.loadFromData(Core::Constants::ACCOUNT_BACKUP_JPEG.data(),
static_cast<u32>(Core::Constants::ACCOUNT_BACKUP_JPEG.size()));
}
return icon.scaled(64, 64, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
}
} // Anonymous namespace
QtProfileSelectionDialog::QtProfileSelectionDialog(
Core::System& system, QWidget* parent,
const Core::Frontend::ProfileSelectParameters& parameters)
: QDialog(parent), profile_manager{system.GetProfileManager()} {
outer_layout = new QVBoxLayout(this);
instruction_label = new QLabel();
scroll_area = new QScrollArea;
buttons = new QDialogButtonBox(QDialogButtonBox::Cancel | QDialogButtonBox::Ok);
connect(buttons, &QDialogButtonBox::accepted, this, &QtProfileSelectionDialog::accept);
connect(buttons, &QDialogButtonBox::rejected, this, &QtProfileSelectionDialog::reject);
outer_layout->addWidget(instruction_label);
outer_layout->addWidget(scroll_area);
outer_layout->addWidget(buttons);
layout = new QVBoxLayout(scroll_area);
tree_view = new QTreeView;
item_model = new QStandardItemModel(tree_view);
tree_view->setModel(item_model);
controller_navigation = new ControllerNavigation(system.HIDCore(), this);
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, tr("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 umbrella of custom types.
qRegisterMetaType<QList<QStandardItem*>>("QList<QStandardItem*>");
layout->setContentsMargins(0, 0, 0, 0);
layout->setSpacing(0);
layout->addWidget(tree_view);
connect(tree_view, &QTreeView::clicked, this, &QtProfileSelectionDialog::SelectUser);
connect(tree_view, &QTreeView::doubleClicked, this, &QtProfileSelectionDialog::accept);
connect(controller_navigation, &ControllerNavigation::TriggerKeyboardEvent,
[this](Qt::Key key) {
if (!this->isActiveWindow()) {
return;
}
QKeyEvent* event = new QKeyEvent(QEvent::KeyPress, key, Qt::NoModifier);
QCoreApplication::postEvent(tree_view, event);
SelectUser(tree_view->currentIndex());
});
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);
SetWindowTitle(parameters);
SetDialogPurpose(parameters);
resize(550, 400);
}
QtProfileSelectionDialog::~QtProfileSelectionDialog() {
controller_navigation->UnloadController();
};
int QtProfileSelectionDialog::exec() {
// Skip profile selection when there's only one.
if (profile_manager.GetUserCount() == 1) {
user_index = 0;
return QDialog::Accepted;
}
return QDialog::exec();
}
void QtProfileSelectionDialog::accept() {
QDialog::accept();
}
void QtProfileSelectionDialog::reject() {
user_index = 0;
QDialog::reject();
}
int QtProfileSelectionDialog::GetIndex() const {
return user_index;
}
void QtProfileSelectionDialog::SelectUser(const QModelIndex& index) {
user_index = index.row();
}
void QtProfileSelectionDialog::SetWindowTitle(
const Core::Frontend::ProfileSelectParameters& parameters) {
using Service::AM::Frontend::UiMode;
switch (parameters.mode) {
case UiMode::UserCreator:
case UiMode::UserCreatorForStarter:
setWindowTitle(tr("Profile Creator"));
return;
case UiMode::EnsureNetworkServiceAccountAvailable:
setWindowTitle(tr("Profile Selector"));
return;
case UiMode::UserIconEditor:
setWindowTitle(tr("Profile Icon Editor"));
return;
case UiMode::UserNicknameEditor:
setWindowTitle(tr("Profile Nickname Editor"));
return;
case UiMode::NintendoAccountAuthorizationRequestContext:
case UiMode::IntroduceExternalNetworkServiceAccount:
case UiMode::IntroduceExternalNetworkServiceAccountForRegistration:
case UiMode::NintendoAccountNnidLinker:
case UiMode::LicenseRequirementsForNetworkService:
case UiMode::LicenseRequirementsForNetworkServiceWithUserContextImpl:
case UiMode::UserCreatorForImmediateNaLoginTest:
case UiMode::UserQualificationPromoter:
case UiMode::UserSelector:
default:
setWindowTitle(tr("Profile Selector"));
}
}
void QtProfileSelectionDialog::SetDialogPurpose(
const Core::Frontend::ProfileSelectParameters& parameters) {
using Service::AM::Frontend::UserSelectionPurpose;
switch (parameters.purpose) {
case UserSelectionPurpose::GameCardRegistration:
instruction_label->setText(tr("Who will receive the points?"));
return;
case UserSelectionPurpose::EShopLaunch:
instruction_label->setText(tr("Who is using Nintendo eShop?"));
return;
case UserSelectionPurpose::EShopItemShow:
instruction_label->setText(tr("Who is making this purchase?"));
return;
case UserSelectionPurpose::PicturePost:
instruction_label->setText(tr("Who is posting?"));
return;
case UserSelectionPurpose::NintendoAccountLinkage:
instruction_label->setText(tr("Select a user to link to a Nintendo Account."));
return;
case UserSelectionPurpose::SettingsUpdate:
instruction_label->setText(tr("Change settings for which user?"));
return;
case UserSelectionPurpose::SaveDataDeletion:
instruction_label->setText(tr("Format data for which user?"));
return;
case UserSelectionPurpose::UserMigration:
instruction_label->setText(tr("Which user will be transferred to another console?"));
return;
case UserSelectionPurpose::SaveDataTransfer:
instruction_label->setText(tr("Send save data for which user?"));
return;
case UserSelectionPurpose::General:
default:
instruction_label->setText(tr("Select a user:"));
return;
}
}
QtProfileSelector::QtProfileSelector(MainWindow& parent) {
connect(this, &QtProfileSelector::MainWindowSelectProfile, &parent,
&MainWindow::ProfileSelectorSelectProfile, Qt::QueuedConnection);
connect(this, &QtProfileSelector::MainWindowRequestExit, &parent,
&MainWindow::ProfileSelectorRequestExit, Qt::QueuedConnection);
connect(&parent, &MainWindow::ProfileSelectorFinishedSelection, this,
&QtProfileSelector::MainWindowFinishedSelection, Qt::DirectConnection);
}
QtProfileSelector::~QtProfileSelector() = default;
void QtProfileSelector::Close() const {
callback = {};
emit MainWindowRequestExit();
}
void QtProfileSelector::SelectProfile(
SelectProfileCallback callback_,
const Core::Frontend::ProfileSelectParameters& parameters) const {
callback = std::move(callback_);
emit MainWindowSelectProfile(parameters);
}
void QtProfileSelector::MainWindowFinishedSelection(std::optional<Common::UUID> uuid) {
if (callback) {
callback(uuid);
}
}