summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/yuzu/CMakeLists.txt3
-rw-r--r--src/yuzu/configuration/configure.ui11
-rw-r--r--src/yuzu/configuration/configure_dialog.cpp17
-rw-r--r--src/yuzu/configuration/configure_profile_manager.cpp300
-rw-r--r--src/yuzu/configuration/configure_profile_manager.h57
-rw-r--r--src/yuzu/configuration/configure_profile_manager.ui172
-rw-r--r--src/yuzu/configuration/configure_system.cpp261
-rw-r--r--src/yuzu/configuration/configure_system.h27
-rw-r--r--src/yuzu/configuration/configure_system.ui144
9 files changed, 565 insertions, 427 deletions
diff --git a/src/yuzu/CMakeLists.txt b/src/yuzu/CMakeLists.txt
index 17ecaafde..5446be6be 100644
--- a/src/yuzu/CMakeLists.txt
+++ b/src/yuzu/CMakeLists.txt
@@ -37,6 +37,8 @@ add_executable(yuzu
37 configuration/configure_input_simple.h 37 configuration/configure_input_simple.h
38 configuration/configure_mouse_advanced.cpp 38 configuration/configure_mouse_advanced.cpp
39 configuration/configure_mouse_advanced.h 39 configuration/configure_mouse_advanced.h
40 configuration/configure_profile_manager.cpp
41 configuration/configure_profile_manager.h
40 configuration/configure_system.cpp 42 configuration/configure_system.cpp
41 configuration/configure_system.h 43 configuration/configure_system.h
42 configuration/configure_per_general.cpp 44 configuration/configure_per_general.cpp
@@ -94,6 +96,7 @@ set(UIS
94 configuration/configure_input_simple.ui 96 configuration/configure_input_simple.ui
95 configuration/configure_mouse_advanced.ui 97 configuration/configure_mouse_advanced.ui
96 configuration/configure_per_general.ui 98 configuration/configure_per_general.ui
99 configuration/configure_profile_manager.ui
97 configuration/configure_system.ui 100 configuration/configure_system.ui
98 configuration/configure_touchscreen_advanced.ui 101 configuration/configure_touchscreen_advanced.ui
99 configuration/configure_web.ui 102 configuration/configure_web.ui
diff --git a/src/yuzu/configuration/configure.ui b/src/yuzu/configuration/configure.ui
index ce833b6c8..3f03f0b77 100644
--- a/src/yuzu/configuration/configure.ui
+++ b/src/yuzu/configuration/configure.ui
@@ -52,6 +52,11 @@
52 <string>System</string> 52 <string>System</string>
53 </attribute> 53 </attribute>
54 </widget> 54 </widget>
55 <widget class="ConfigureProfileManager" name="profileManagerTab">
56 <attribute name="title">
57 <string>Profiles</string>
58 </attribute>
59 </widget>
55 <widget class="ConfigureInputSimple" name="inputTab"> 60 <widget class="ConfigureInputSimple" name="inputTab">
56 <attribute name="title"> 61 <attribute name="title">
57 <string>Input</string> 62 <string>Input</string>
@@ -104,6 +109,12 @@
104 <container>1</container> 109 <container>1</container>
105 </customwidget> 110 </customwidget>
106 <customwidget> 111 <customwidget>
112 <class>ConfigureProfileManager</class>
113 <extends>QWidget</extends>
114 <header>configuration/configure_profile_manager.h</header>
115 <container>1</container>
116 </customwidget>
117 <customwidget>
107 <class>ConfigureAudio</class> 118 <class>ConfigureAudio</class>
108 <extends>QWidget</extends> 119 <extends>QWidget</extends>
109 <header>configuration/configure_audio.h</header> 120 <header>configuration/configure_audio.h</header>
diff --git a/src/yuzu/configuration/configure_dialog.cpp b/src/yuzu/configuration/configure_dialog.cpp
index 90d7c6372..d802443d0 100644
--- a/src/yuzu/configuration/configure_dialog.cpp
+++ b/src/yuzu/configuration/configure_dialog.cpp
@@ -32,6 +32,7 @@ void ConfigureDialog::applyConfiguration() {
32 ui->generalTab->applyConfiguration(); 32 ui->generalTab->applyConfiguration();
33 ui->gameListTab->applyConfiguration(); 33 ui->gameListTab->applyConfiguration();
34 ui->systemTab->applyConfiguration(); 34 ui->systemTab->applyConfiguration();
35 ui->profileManagerTab->applyConfiguration();
35 ui->inputTab->applyConfiguration(); 36 ui->inputTab->applyConfiguration();
36 ui->graphicsTab->applyConfiguration(); 37 ui->graphicsTab->applyConfiguration();
37 ui->audioTab->applyConfiguration(); 38 ui->audioTab->applyConfiguration();
@@ -43,7 +44,7 @@ void ConfigureDialog::applyConfiguration() {
43void ConfigureDialog::PopulateSelectionList() { 44void ConfigureDialog::PopulateSelectionList() {
44 const std::array<std::pair<QString, QStringList>, 4> items{ 45 const std::array<std::pair<QString, QStringList>, 4> items{
45 {{tr("General"), {tr("General"), tr("Web"), tr("Debug"), tr("Game List")}}, 46 {{tr("General"), {tr("General"), tr("Web"), tr("Debug"), tr("Game List")}},
46 {tr("System"), {tr("System"), tr("Audio")}}, 47 {tr("System"), {tr("System"), tr("Profiles"), tr("Audio")}},
47 {tr("Graphics"), {tr("Graphics")}}, 48 {tr("Graphics"), {tr("Graphics")}},
48 {tr("Controls"), {tr("Input")}}}}; 49 {tr("Controls"), {tr("Input")}}}};
49 50
@@ -60,11 +61,15 @@ void ConfigureDialog::UpdateVisibleTabs() {
60 if (items.isEmpty()) 61 if (items.isEmpty())
61 return; 62 return;
62 63
63 const std::map<QString, QWidget*> widgets = { 64 const std::map<QString, QWidget*> widgets = {{tr("General"), ui->generalTab},
64 {tr("General"), ui->generalTab}, {tr("System"), ui->systemTab}, 65 {tr("System"), ui->systemTab},
65 {tr("Input"), ui->inputTab}, {tr("Graphics"), ui->graphicsTab}, 66 {tr("Profiles"), ui->profileManagerTab},
66 {tr("Audio"), ui->audioTab}, {tr("Debug"), ui->debugTab}, 67 {tr("Input"), ui->inputTab},
67 {tr("Web"), ui->webTab}, {tr("Game List"), ui->gameListTab}}; 68 {tr("Graphics"), ui->graphicsTab},
69 {tr("Audio"), ui->audioTab},
70 {tr("Debug"), ui->debugTab},
71 {tr("Web"), ui->webTab},
72 {tr("Game List"), ui->gameListTab}};
68 73
69 ui->tabWidget->clear(); 74 ui->tabWidget->clear();
70 75
diff --git a/src/yuzu/configuration/configure_profile_manager.cpp b/src/yuzu/configuration/configure_profile_manager.cpp
new file mode 100644
index 000000000..41663e39a
--- /dev/null
+++ b/src/yuzu/configuration/configure_profile_manager.cpp
@@ -0,0 +1,300 @@
1// Copyright 2016 Citra Emulator Project
2// Licensed under GPLv2 or any later version
3// Refer to the license.txt file included.
4
5#include <algorithm>
6#include <QFileDialog>
7#include <QGraphicsItem>
8#include <QGraphicsScene>
9#include <QHeaderView>
10#include <QMessageBox>
11#include <QStandardItemModel>
12#include <QTreeView>
13#include <QVBoxLayout>
14#include "common/assert.h"
15#include "common/file_util.h"
16#include "common/string_util.h"
17#include "core/core.h"
18#include "core/hle/service/acc/profile_manager.h"
19#include "core/settings.h"
20#include "ui_configure_profile_manager.h"
21#include "yuzu/configuration/configure_profile_manager.h"
22#include "yuzu/util/limitable_input_dialog.h"
23
24namespace {
25// Same backup JPEG used by acc IProfile::GetImage if no jpeg found
26constexpr std::array<u8, 107> backup_jpeg{
27 0xff, 0xd8, 0xff, 0xdb, 0x00, 0x43, 0x00, 0x03, 0x02, 0x02, 0x02, 0x02, 0x02, 0x03, 0x02, 0x02,
28 0x02, 0x03, 0x03, 0x03, 0x03, 0x04, 0x06, 0x04, 0x04, 0x04, 0x04, 0x04, 0x08, 0x06, 0x06, 0x05,
29 0x06, 0x09, 0x08, 0x0a, 0x0a, 0x09, 0x08, 0x09, 0x09, 0x0a, 0x0c, 0x0f, 0x0c, 0x0a, 0x0b, 0x0e,
30 0x0b, 0x09, 0x09, 0x0d, 0x11, 0x0d, 0x0e, 0x0f, 0x10, 0x10, 0x11, 0x10, 0x0a, 0x0c, 0x12, 0x13,
31 0x12, 0x10, 0x13, 0x0f, 0x10, 0x10, 0x10, 0xff, 0xc9, 0x00, 0x0b, 0x08, 0x00, 0x01, 0x00, 0x01,
32 0x01, 0x01, 0x11, 0x00, 0xff, 0xcc, 0x00, 0x06, 0x00, 0x10, 0x10, 0x05, 0xff, 0xda, 0x00, 0x08,
33 0x01, 0x01, 0x00, 0x00, 0x3f, 0x00, 0xd2, 0xcf, 0x20, 0xff, 0xd9,
34};
35
36QString GetImagePath(Service::Account::UUID uuid) {
37 const auto path = FileUtil::GetUserPath(FileUtil::UserPath::NANDDir) +
38 "/system/save/8000000000000010/su/avators/" + uuid.FormatSwitch() + ".jpg";
39 return QString::fromStdString(path);
40}
41
42QString GetAccountUsername(const Service::Account::ProfileManager& manager,
43 Service::Account::UUID uuid) {
44 Service::Account::ProfileBase profile;
45 if (!manager.GetProfileBase(uuid, profile)) {
46 return {};
47 }
48
49 const auto text = Common::StringFromFixedZeroTerminatedBuffer(
50 reinterpret_cast<const char*>(profile.username.data()), profile.username.size());
51 return QString::fromStdString(text);
52}
53
54QString FormatUserEntryText(const QString& username, Service::Account::UUID uuid) {
55 return ConfigureProfileManager::tr("%1\n%2",
56 "%1 is the profile username, %2 is the formatted UUID (e.g. "
57 "00112233-4455-6677-8899-AABBCCDDEEFF))")
58 .arg(username, QString::fromStdString(uuid.FormatSwitch()));
59}
60
61QPixmap GetIcon(Service::Account::UUID uuid) {
62 QPixmap icon{GetImagePath(uuid)};
63
64 if (!icon) {
65 icon.fill(Qt::black);
66 icon.loadFromData(backup_jpeg.data(), static_cast<u32>(backup_jpeg.size()));
67 }
68
69 return icon.scaled(64, 64, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
70}
71
72QString GetProfileUsernameFromUser(QWidget* parent, const QString& description_text) {
73 return LimitableInputDialog::GetText(parent, ConfigureProfileManager::tr("Enter Username"),
74 description_text, 1,
75 static_cast<int>(Service::Account::profile_username_size));
76}
77} // Anonymous namespace
78
79ConfigureProfileManager ::ConfigureProfileManager(QWidget* parent)
80 : QWidget(parent), ui(new Ui::ConfigureProfileManager),
81 profile_manager(std::make_unique<Service::Account::ProfileManager>()) {
82 ui->setupUi(this);
83
84 layout = new QVBoxLayout;
85 tree_view = new QTreeView;
86 item_model = new QStandardItemModel(tree_view);
87 tree_view->setModel(item_model);
88
89 tree_view->setAlternatingRowColors(true);
90 tree_view->setSelectionMode(QHeaderView::SingleSelection);
91 tree_view->setSelectionBehavior(QHeaderView::SelectRows);
92 tree_view->setVerticalScrollMode(QHeaderView::ScrollPerPixel);
93 tree_view->setHorizontalScrollMode(QHeaderView::ScrollPerPixel);
94 tree_view->setSortingEnabled(true);
95 tree_view->setEditTriggers(QHeaderView::NoEditTriggers);
96 tree_view->setUniformRowHeights(true);
97 tree_view->setIconSize({64, 64});
98 tree_view->setContextMenuPolicy(Qt::NoContextMenu);
99
100 item_model->insertColumns(0, 1);
101 item_model->setHeaderData(0, Qt::Horizontal, "Users");
102
103 // We must register all custom types with the Qt Automoc system so that we are able to use it
104 // with signals/slots. In this case, QList falls under the umbrells of custom types.
105 qRegisterMetaType<QList<QStandardItem*>>("QList<QStandardItem*>");
106
107 layout->setContentsMargins(0, 0, 0, 0);
108 layout->setSpacing(0);
109 layout->addWidget(tree_view);
110
111 ui->scrollArea->setLayout(layout);
112
113 connect(tree_view, &QTreeView::clicked, this, &ConfigureProfileManager::SelectUser);
114
115 connect(ui->pm_add, &QPushButton::pressed, this, &ConfigureProfileManager::AddUser);
116 connect(ui->pm_rename, &QPushButton::pressed, this, &ConfigureProfileManager::RenameUser);
117 connect(ui->pm_remove, &QPushButton::pressed, this, &ConfigureProfileManager::DeleteUser);
118 connect(ui->pm_set_image, &QPushButton::pressed, this, &ConfigureProfileManager::SetUserImage);
119
120 scene = new QGraphicsScene;
121 ui->current_user_icon->setScene(scene);
122
123 this->setConfiguration();
124}
125
126ConfigureProfileManager::~ConfigureProfileManager() = default;
127
128void ConfigureProfileManager::setConfiguration() {
129 enabled = !Core::System::GetInstance().IsPoweredOn();
130 item_model->removeRows(0, item_model->rowCount());
131 list_items.clear();
132
133 PopulateUserList();
134 UpdateCurrentUser();
135}
136
137void ConfigureProfileManager::PopulateUserList() {
138 const auto& profiles = profile_manager->GetAllUsers();
139 for (const auto& user : profiles) {
140 Service::Account::ProfileBase profile;
141 if (!profile_manager->GetProfileBase(user, profile))
142 continue;
143
144 const auto username = Common::StringFromFixedZeroTerminatedBuffer(
145 reinterpret_cast<const char*>(profile.username.data()), profile.username.size());
146
147 list_items.push_back(QList<QStandardItem*>{new QStandardItem{
148 GetIcon(user), FormatUserEntryText(QString::fromStdString(username), user)}});
149 }
150
151 for (const auto& item : list_items)
152 item_model->appendRow(item);
153}
154
155void ConfigureProfileManager::UpdateCurrentUser() {
156 ui->pm_add->setEnabled(profile_manager->GetUserCount() < Service::Account::MAX_USERS);
157
158 const auto& current_user = profile_manager->GetUser(Settings::values.current_user);
159 ASSERT(current_user);
160 const auto username = GetAccountUsername(*profile_manager, *current_user);
161
162 scene->clear();
163 scene->addPixmap(
164 GetIcon(*current_user).scaled(48, 48, Qt::IgnoreAspectRatio, Qt::SmoothTransformation));
165 ui->current_user_username->setText(username);
166}
167
168void ConfigureProfileManager::applyConfiguration() {
169 if (!enabled)
170 return;
171
172 Settings::Apply();
173}
174
175void ConfigureProfileManager::SelectUser(const QModelIndex& index) {
176 Settings::values.current_user =
177 std::clamp<s32>(index.row(), 0, static_cast<s32>(profile_manager->GetUserCount() - 1));
178
179 UpdateCurrentUser();
180
181 ui->pm_remove->setEnabled(profile_manager->GetUserCount() >= 2);
182 ui->pm_rename->setEnabled(true);
183 ui->pm_set_image->setEnabled(true);
184}
185
186void ConfigureProfileManager::AddUser() {
187 const auto username =
188 GetProfileUsernameFromUser(this, tr("Enter a username for the new user:"));
189 if (username.isEmpty()) {
190 return;
191 }
192
193 const auto uuid = Service::Account::UUID::Generate();
194 profile_manager->CreateNewUser(uuid, username.toStdString());
195
196 item_model->appendRow(new QStandardItem{GetIcon(uuid), FormatUserEntryText(username, uuid)});
197}
198
199void ConfigureProfileManager::RenameUser() {
200 const auto user = tree_view->currentIndex().row();
201 const auto uuid = profile_manager->GetUser(user);
202 ASSERT(uuid);
203
204 Service::Account::ProfileBase profile;
205 if (!profile_manager->GetProfileBase(*uuid, profile))
206 return;
207
208 const auto new_username = GetProfileUsernameFromUser(this, tr("Enter a new username:"));
209 if (new_username.isEmpty()) {
210 return;
211 }
212
213 const auto username_std = new_username.toStdString();
214 std::fill(profile.username.begin(), profile.username.end(), '\0');
215 std::copy(username_std.begin(), username_std.end(), profile.username.begin());
216
217 profile_manager->SetProfileBase(*uuid, profile);
218
219 item_model->setItem(
220 user, 0,
221 new QStandardItem{GetIcon(*uuid),
222 FormatUserEntryText(QString::fromStdString(username_std), *uuid)});
223 UpdateCurrentUser();
224}
225
226void ConfigureProfileManager::DeleteUser() {
227 const auto index = tree_view->currentIndex().row();
228 const auto uuid = profile_manager->GetUser(index);
229 ASSERT(uuid);
230 const auto username = GetAccountUsername(*profile_manager, *uuid);
231
232 const auto confirm = QMessageBox::question(
233 this, tr("Confirm Delete"),
234 tr("You are about to delete user with name \"%1\". Are you sure?").arg(username));
235
236 if (confirm == QMessageBox::No)
237 return;
238
239 if (Settings::values.current_user == tree_view->currentIndex().row())
240 Settings::values.current_user = 0;
241 UpdateCurrentUser();
242
243 if (!profile_manager->RemoveUser(*uuid))
244 return;
245
246 item_model->removeRows(tree_view->currentIndex().row(), 1);
247 tree_view->clearSelection();
248
249 ui->pm_remove->setEnabled(false);
250 ui->pm_rename->setEnabled(false);
251}
252
253void ConfigureProfileManager::SetUserImage() {
254 const auto index = tree_view->currentIndex().row();
255 const auto uuid = profile_manager->GetUser(index);
256 ASSERT(uuid);
257
258 const auto file = QFileDialog::getOpenFileName(this, tr("Select User Image"), QString(),
259 tr("JPEG Images (*.jpg *.jpeg)"));
260
261 if (file.isEmpty()) {
262 return;
263 }
264
265 const auto image_path = GetImagePath(*uuid);
266 if (QFile::exists(image_path) && !QFile::remove(image_path)) {
267 QMessageBox::warning(
268 this, tr("Error deleting image"),
269 tr("Error occurred attempting to overwrite previous image at: %1.").arg(image_path));
270 return;
271 }
272
273 const auto raw_path = QString::fromStdString(
274 FileUtil::GetUserPath(FileUtil::UserPath::NANDDir) + "/system/save/8000000000000010");
275 const QFileInfo raw_info{raw_path};
276 if (raw_info.exists() && !raw_info.isDir() && !QFile::remove(raw_path)) {
277 QMessageBox::warning(this, tr("Error deleting file"),
278 tr("Unable to delete existing file: %1.").arg(raw_path));
279 return;
280 }
281
282 const QString absolute_dst_path = QFileInfo{image_path}.absolutePath();
283 if (!QDir{raw_path}.mkpath(absolute_dst_path)) {
284 QMessageBox::warning(
285 this, tr("Error creating user image directory"),
286 tr("Unable to create directory %1 for storing user images.").arg(absolute_dst_path));
287 return;
288 }
289
290 if (!QFile::copy(file, image_path)) {
291 QMessageBox::warning(this, tr("Error copying user image"),
292 tr("Unable to copy image from %1 to %2").arg(file, image_path));
293 return;
294 }
295
296 const auto username = GetAccountUsername(*profile_manager, *uuid);
297 item_model->setItem(index, 0,
298 new QStandardItem{GetIcon(*uuid), FormatUserEntryText(username, *uuid)});
299 UpdateCurrentUser();
300}
diff --git a/src/yuzu/configuration/configure_profile_manager.h b/src/yuzu/configuration/configure_profile_manager.h
new file mode 100644
index 000000000..7fe95a2a8
--- /dev/null
+++ b/src/yuzu/configuration/configure_profile_manager.h
@@ -0,0 +1,57 @@
1// Copyright 2016 Citra Emulator Project
2// Licensed under GPLv2 or any later version
3// Refer to the license.txt file included.
4
5#pragma once
6
7#include <memory>
8
9#include <QList>
10#include <QWidget>
11
12class QGraphicsScene;
13class QStandardItem;
14class QStandardItemModel;
15class QTreeView;
16class QVBoxLayout;
17
18namespace Service::Account {
19class ProfileManager;
20}
21
22namespace Ui {
23class ConfigureProfileManager;
24}
25
26class ConfigureProfileManager : public QWidget {
27 Q_OBJECT
28
29public:
30 explicit ConfigureProfileManager(QWidget* parent = nullptr);
31 ~ConfigureProfileManager() override;
32
33 void applyConfiguration();
34 void setConfiguration();
35
36private:
37 void PopulateUserList();
38 void UpdateCurrentUser();
39
40 void SelectUser(const QModelIndex& index);
41 void AddUser();
42 void RenameUser();
43 void DeleteUser();
44 void SetUserImage();
45
46 QVBoxLayout* layout;
47 QTreeView* tree_view;
48 QStandardItemModel* item_model;
49 QGraphicsScene* scene;
50
51 std::vector<QList<QStandardItem*>> list_items;
52
53 std::unique_ptr<Ui::ConfigureProfileManager> ui;
54 bool enabled = false;
55
56 std::unique_ptr<Service::Account::ProfileManager> profile_manager;
57};
diff --git a/src/yuzu/configuration/configure_profile_manager.ui b/src/yuzu/configuration/configure_profile_manager.ui
new file mode 100644
index 000000000..dedba4998
--- /dev/null
+++ b/src/yuzu/configuration/configure_profile_manager.ui
@@ -0,0 +1,172 @@
1<?xml version="1.0" encoding="UTF-8"?>
2<ui version="4.0">
3 <class>ConfigureProfileManager</class>
4 <widget class="QWidget" name="ConfigureProfileManager">
5 <property name="geometry">
6 <rect>
7 <x>0</x>
8 <y>0</y>
9 <width>366</width>
10 <height>483</height>
11 </rect>
12 </property>
13 <property name="windowTitle">
14 <string>Form</string>
15 </property>
16 <layout class="QHBoxLayout" name="horizontalLayout">
17 <item>
18 <layout class="QVBoxLayout" name="verticalLayout">
19 <item>
20 <widget class="QGroupBox" name="gridGroupBox">
21 <property name="title">
22 <string>Profile Manager</string>
23 </property>
24 <layout class="QGridLayout" name="gridLayout_2">
25 <property name="sizeConstraint">
26 <enum>QLayout::SetNoConstraint</enum>
27 </property>
28 <item row="0" column="0">
29 <layout class="QHBoxLayout" name="horizontalLayout_2">
30 <item>
31 <widget class="QLabel" name="label">
32 <property name="sizePolicy">
33 <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
34 <horstretch>0</horstretch>
35 <verstretch>0</verstretch>
36 </sizepolicy>
37 </property>
38 <property name="text">
39 <string>Current User</string>
40 </property>
41 </widget>
42 </item>
43 <item>
44 <widget class="QGraphicsView" name="current_user_icon">
45 <property name="minimumSize">
46 <size>
47 <width>48</width>
48 <height>48</height>
49 </size>
50 </property>
51 <property name="maximumSize">
52 <size>
53 <width>48</width>
54 <height>48</height>
55 </size>
56 </property>
57 <property name="verticalScrollBarPolicy">
58 <enum>Qt::ScrollBarAlwaysOff</enum>
59 </property>
60 <property name="horizontalScrollBarPolicy">
61 <enum>Qt::ScrollBarAlwaysOff</enum>
62 </property>
63 <property name="interactive">
64 <bool>false</bool>
65 </property>
66 </widget>
67 </item>
68 <item>
69 <widget class="QLabel" name="current_user_username">
70 <property name="sizePolicy">
71 <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
72 <horstretch>0</horstretch>
73 <verstretch>0</verstretch>
74 </sizepolicy>
75 </property>
76 <property name="text">
77 <string>Username</string>
78 </property>
79 </widget>
80 </item>
81 </layout>
82 </item>
83 <item row="1" column="0">
84 <widget class="QScrollArea" name="scrollArea">
85 <property name="sizePolicy">
86 <sizepolicy hsizetype="Expanding" vsizetype="Expanding">
87 <horstretch>0</horstretch>
88 <verstretch>0</verstretch>
89 </sizepolicy>
90 </property>
91 <property name="frameShape">
92 <enum>QFrame::StyledPanel</enum>
93 </property>
94 <property name="widgetResizable">
95 <bool>false</bool>
96 </property>
97 </widget>
98 </item>
99 <item row="2" column="0">
100 <layout class="QHBoxLayout" name="horizontalLayout_3">
101 <item>
102 <widget class="QPushButton" name="pm_set_image">
103 <property name="enabled">
104 <bool>false</bool>
105 </property>
106 <property name="text">
107 <string>Set Image</string>
108 </property>
109 </widget>
110 </item>
111 <item>
112 <spacer name="horizontalSpacer">
113 <property name="orientation">
114 <enum>Qt::Horizontal</enum>
115 </property>
116 <property name="sizeHint" stdset="0">
117 <size>
118 <width>40</width>
119 <height>20</height>
120 </size>
121 </property>
122 </spacer>
123 </item>
124 <item>
125 <widget class="QPushButton" name="pm_add">
126 <property name="text">
127 <string>Add</string>
128 </property>
129 </widget>
130 </item>
131 <item>
132 <widget class="QPushButton" name="pm_rename">
133 <property name="enabled">
134 <bool>false</bool>
135 </property>
136 <property name="text">
137 <string>Rename</string>
138 </property>
139 </widget>
140 </item>
141 <item>
142 <widget class="QPushButton" name="pm_remove">
143 <property name="enabled">
144 <bool>false</bool>
145 </property>
146 <property name="text">
147 <string>Remove</string>
148 </property>
149 </widget>
150 </item>
151 </layout>
152 </item>
153 </layout>
154 </widget>
155 </item>
156 <item>
157 <widget class="QLabel" name="label_disable_info">
158 <property name="text">
159 <string>Profile management is available only when game is not running.</string>
160 </property>
161 <property name="wordWrap">
162 <bool>true</bool>
163 </property>
164 </widget>
165 </item>
166 </layout>
167 </item>
168 </layout>
169 </widget>
170 <resources/>
171 <connections/>
172</ui>
diff --git a/src/yuzu/configuration/configure_system.cpp b/src/yuzu/configuration/configure_system.cpp
index ab5d46492..445d01ca0 100644
--- a/src/yuzu/configuration/configure_system.cpp
+++ b/src/yuzu/configuration/configure_system.cpp
@@ -15,7 +15,6 @@
15#include "common/file_util.h" 15#include "common/file_util.h"
16#include "common/string_util.h" 16#include "common/string_util.h"
17#include "core/core.h" 17#include "core/core.h"
18#include "core/hle/service/acc/profile_manager.h"
19#include "core/settings.h" 18#include "core/settings.h"
20#include "ui_configure_system.h" 19#include "ui_configure_system.h"
21#include "yuzu/configuration/configure_system.h" 20#include "yuzu/configuration/configure_system.h"
@@ -36,64 +35,9 @@ constexpr std::array<int, 12> days_in_month = {{
36 30, 35 30,
37 31, 36 31,
38}}; 37}};
39
40// Same backup JPEG used by acc IProfile::GetImage if no jpeg found
41constexpr std::array<u8, 107> backup_jpeg{
42 0xff, 0xd8, 0xff, 0xdb, 0x00, 0x43, 0x00, 0x03, 0x02, 0x02, 0x02, 0x02, 0x02, 0x03, 0x02, 0x02,
43 0x02, 0x03, 0x03, 0x03, 0x03, 0x04, 0x06, 0x04, 0x04, 0x04, 0x04, 0x04, 0x08, 0x06, 0x06, 0x05,
44 0x06, 0x09, 0x08, 0x0a, 0x0a, 0x09, 0x08, 0x09, 0x09, 0x0a, 0x0c, 0x0f, 0x0c, 0x0a, 0x0b, 0x0e,
45 0x0b, 0x09, 0x09, 0x0d, 0x11, 0x0d, 0x0e, 0x0f, 0x10, 0x10, 0x11, 0x10, 0x0a, 0x0c, 0x12, 0x13,
46 0x12, 0x10, 0x13, 0x0f, 0x10, 0x10, 0x10, 0xff, 0xc9, 0x00, 0x0b, 0x08, 0x00, 0x01, 0x00, 0x01,
47 0x01, 0x01, 0x11, 0x00, 0xff, 0xcc, 0x00, 0x06, 0x00, 0x10, 0x10, 0x05, 0xff, 0xda, 0x00, 0x08,
48 0x01, 0x01, 0x00, 0x00, 0x3f, 0x00, 0xd2, 0xcf, 0x20, 0xff, 0xd9,
49};
50
51QString GetImagePath(Service::Account::UUID uuid) {
52 const auto path = FileUtil::GetUserPath(FileUtil::UserPath::NANDDir) +
53 "/system/save/8000000000000010/su/avators/" + uuid.FormatSwitch() + ".jpg";
54 return QString::fromStdString(path);
55}
56
57QString GetAccountUsername(const Service::Account::ProfileManager& manager,
58 Service::Account::UUID uuid) {
59 Service::Account::ProfileBase profile;
60 if (!manager.GetProfileBase(uuid, profile)) {
61 return {};
62 }
63
64 const auto text = Common::StringFromFixedZeroTerminatedBuffer(
65 reinterpret_cast<const char*>(profile.username.data()), profile.username.size());
66 return QString::fromStdString(text);
67}
68
69QString FormatUserEntryText(const QString& username, Service::Account::UUID uuid) {
70 return ConfigureSystem::tr("%1\n%2",
71 "%1 is the profile username, %2 is the formatted UUID (e.g. "
72 "00112233-4455-6677-8899-AABBCCDDEEFF))")
73 .arg(username, QString::fromStdString(uuid.FormatSwitch()));
74}
75
76QPixmap GetIcon(Service::Account::UUID uuid) {
77 QPixmap icon{GetImagePath(uuid)};
78
79 if (!icon) {
80 icon.fill(Qt::black);
81 icon.loadFromData(backup_jpeg.data(), static_cast<u32>(backup_jpeg.size()));
82 }
83
84 return icon.scaled(64, 64, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
85}
86
87QString GetProfileUsernameFromUser(QWidget* parent, const QString& description_text) {
88 return LimitableInputDialog::GetText(parent, ConfigureSystem::tr("Enter Username"),
89 description_text, 1,
90 static_cast<int>(Service::Account::profile_username_size));
91}
92} // Anonymous namespace 38} // Anonymous namespace
93 39
94ConfigureSystem::ConfigureSystem(QWidget* parent) 40ConfigureSystem::ConfigureSystem(QWidget* parent) : QWidget(parent), ui(new Ui::ConfigureSystem) {
95 : QWidget(parent), ui(new Ui::ConfigureSystem),
96 profile_manager(std::make_unique<Service::Account::ProfileManager>()) {
97 ui->setupUi(this); 41 ui->setupUi(this);
98 connect(ui->combo_birthmonth, 42 connect(ui->combo_birthmonth,
99 static_cast<void (QComboBox::*)(int)>(&QComboBox::currentIndexChanged), this, 43 static_cast<void (QComboBox::*)(int)>(&QComboBox::currentIndexChanged), this,
@@ -101,51 +45,12 @@ ConfigureSystem::ConfigureSystem(QWidget* parent)
101 connect(ui->button_regenerate_console_id, &QPushButton::clicked, this, 45 connect(ui->button_regenerate_console_id, &QPushButton::clicked, this,
102 &ConfigureSystem::RefreshConsoleID); 46 &ConfigureSystem::RefreshConsoleID);
103 47
104 layout = new QVBoxLayout;
105 tree_view = new QTreeView;
106 item_model = new QStandardItemModel(tree_view);
107 tree_view->setModel(item_model);
108
109 tree_view->setAlternatingRowColors(true);
110 tree_view->setSelectionMode(QHeaderView::SingleSelection);
111 tree_view->setSelectionBehavior(QHeaderView::SelectRows);
112 tree_view->setVerticalScrollMode(QHeaderView::ScrollPerPixel);
113 tree_view->setHorizontalScrollMode(QHeaderView::ScrollPerPixel);
114 tree_view->setSortingEnabled(true);
115 tree_view->setEditTriggers(QHeaderView::NoEditTriggers);
116 tree_view->setUniformRowHeights(true);
117 tree_view->setIconSize({64, 64});
118 tree_view->setContextMenuPolicy(Qt::NoContextMenu);
119
120 item_model->insertColumns(0, 1);
121 item_model->setHeaderData(0, Qt::Horizontal, "Users");
122
123 // We must register all custom types with the Qt Automoc system so that we are able to use it
124 // with signals/slots. In this case, QList falls under the umbrells of custom types.
125 qRegisterMetaType<QList<QStandardItem*>>("QList<QStandardItem*>");
126
127 layout->setContentsMargins(0, 0, 0, 0);
128 layout->setSpacing(0);
129 layout->addWidget(tree_view);
130
131 ui->scrollArea->setLayout(layout);
132
133 connect(tree_view, &QTreeView::clicked, this, &ConfigureSystem::SelectUser);
134
135 connect(ui->pm_add, &QPushButton::pressed, this, &ConfigureSystem::AddUser);
136 connect(ui->pm_rename, &QPushButton::pressed, this, &ConfigureSystem::RenameUser);
137 connect(ui->pm_remove, &QPushButton::pressed, this, &ConfigureSystem::DeleteUser);
138 connect(ui->pm_set_image, &QPushButton::pressed, this, &ConfigureSystem::SetUserImage);
139
140 connect(ui->rng_seed_checkbox, &QCheckBox::stateChanged, this, [this](bool checked) { 48 connect(ui->rng_seed_checkbox, &QCheckBox::stateChanged, this, [this](bool checked) {
141 ui->rng_seed_edit->setEnabled(checked); 49 ui->rng_seed_edit->setEnabled(checked);
142 if (!checked) 50 if (!checked)
143 ui->rng_seed_edit->setText(QStringLiteral("00000000")); 51 ui->rng_seed_edit->setText(QStringLiteral("00000000"));
144 }); 52 });
145 53
146 scene = new QGraphicsScene;
147 ui->current_user_icon->setScene(scene);
148
149 this->setConfiguration(); 54 this->setConfiguration();
150} 55}
151 56
@@ -156,12 +61,6 @@ void ConfigureSystem::setConfiguration() {
156 61
157 ui->combo_language->setCurrentIndex(Settings::values.language_index); 62 ui->combo_language->setCurrentIndex(Settings::values.language_index);
158 63
159 item_model->removeRows(0, item_model->rowCount());
160 list_items.clear();
161
162 PopulateUserList();
163 UpdateCurrentUser();
164
165 ui->rng_seed_checkbox->setChecked(Settings::values.rng_seed.has_value()); 64 ui->rng_seed_checkbox->setChecked(Settings::values.rng_seed.has_value());
166 ui->rng_seed_edit->setEnabled(Settings::values.rng_seed.has_value()); 65 ui->rng_seed_edit->setEnabled(Settings::values.rng_seed.has_value());
167 66
@@ -170,37 +69,6 @@ void ConfigureSystem::setConfiguration() {
170 ui->rng_seed_edit->setText(rng_seed); 69 ui->rng_seed_edit->setText(rng_seed);
171} 70}
172 71
173void ConfigureSystem::PopulateUserList() {
174 const auto& profiles = profile_manager->GetAllUsers();
175 for (const auto& user : profiles) {
176 Service::Account::ProfileBase profile;
177 if (!profile_manager->GetProfileBase(user, profile))
178 continue;
179
180 const auto username = Common::StringFromFixedZeroTerminatedBuffer(
181 reinterpret_cast<const char*>(profile.username.data()), profile.username.size());
182
183 list_items.push_back(QList<QStandardItem*>{new QStandardItem{
184 GetIcon(user), FormatUserEntryText(QString::fromStdString(username), user)}});
185 }
186
187 for (const auto& item : list_items)
188 item_model->appendRow(item);
189}
190
191void ConfigureSystem::UpdateCurrentUser() {
192 ui->pm_add->setEnabled(profile_manager->GetUserCount() < Service::Account::MAX_USERS);
193
194 const auto& current_user = profile_manager->GetUser(Settings::values.current_user);
195 ASSERT(current_user);
196 const auto username = GetAccountUsername(*profile_manager, *current_user);
197
198 scene->clear();
199 scene->addPixmap(
200 GetIcon(*current_user).scaled(48, 48, Qt::IgnoreAspectRatio, Qt::SmoothTransformation));
201 ui->current_user_username->setText(username);
202}
203
204void ConfigureSystem::ReadSystemSettings() {} 72void ConfigureSystem::ReadSystemSettings() {}
205 73
206void ConfigureSystem::applyConfiguration() { 74void ConfigureSystem::applyConfiguration() {
@@ -256,130 +124,3 @@ void ConfigureSystem::RefreshConsoleID() {
256 ui->label_console_id->setText( 124 ui->label_console_id->setText(
257 tr("Console ID: 0x%1").arg(QString::number(console_id, 16).toUpper())); 125 tr("Console ID: 0x%1").arg(QString::number(console_id, 16).toUpper()));
258} 126}
259
260void ConfigureSystem::SelectUser(const QModelIndex& index) {
261 Settings::values.current_user =
262 std::clamp<s32>(index.row(), 0, static_cast<s32>(profile_manager->GetUserCount() - 1));
263
264 UpdateCurrentUser();
265
266 ui->pm_remove->setEnabled(profile_manager->GetUserCount() >= 2);
267 ui->pm_rename->setEnabled(true);
268 ui->pm_set_image->setEnabled(true);
269}
270
271void ConfigureSystem::AddUser() {
272 const auto username =
273 GetProfileUsernameFromUser(this, tr("Enter a username for the new user:"));
274 if (username.isEmpty()) {
275 return;
276 }
277
278 const auto uuid = Service::Account::UUID::Generate();
279 profile_manager->CreateNewUser(uuid, username.toStdString());
280
281 item_model->appendRow(new QStandardItem{GetIcon(uuid), FormatUserEntryText(username, uuid)});
282}
283
284void ConfigureSystem::RenameUser() {
285 const auto user = tree_view->currentIndex().row();
286 const auto uuid = profile_manager->GetUser(user);
287 ASSERT(uuid);
288
289 Service::Account::ProfileBase profile;
290 if (!profile_manager->GetProfileBase(*uuid, profile))
291 return;
292
293 const auto new_username = GetProfileUsernameFromUser(this, tr("Enter a new username:"));
294 if (new_username.isEmpty()) {
295 return;
296 }
297
298 const auto username_std = new_username.toStdString();
299 std::fill(profile.username.begin(), profile.username.end(), '\0');
300 std::copy(username_std.begin(), username_std.end(), profile.username.begin());
301
302 profile_manager->SetProfileBase(*uuid, profile);
303
304 item_model->setItem(
305 user, 0,
306 new QStandardItem{GetIcon(*uuid),
307 FormatUserEntryText(QString::fromStdString(username_std), *uuid)});
308 UpdateCurrentUser();
309}
310
311void ConfigureSystem::DeleteUser() {
312 const auto index = tree_view->currentIndex().row();
313 const auto uuid = profile_manager->GetUser(index);
314 ASSERT(uuid);
315 const auto username = GetAccountUsername(*profile_manager, *uuid);
316
317 const auto confirm = QMessageBox::question(
318 this, tr("Confirm Delete"),
319 tr("You are about to delete user with name \"%1\". Are you sure?").arg(username));
320
321 if (confirm == QMessageBox::No)
322 return;
323
324 if (Settings::values.current_user == tree_view->currentIndex().row())
325 Settings::values.current_user = 0;
326 UpdateCurrentUser();
327
328 if (!profile_manager->RemoveUser(*uuid))
329 return;
330
331 item_model->removeRows(tree_view->currentIndex().row(), 1);
332 tree_view->clearSelection();
333
334 ui->pm_remove->setEnabled(false);
335 ui->pm_rename->setEnabled(false);
336}
337
338void ConfigureSystem::SetUserImage() {
339 const auto index = tree_view->currentIndex().row();
340 const auto uuid = profile_manager->GetUser(index);
341 ASSERT(uuid);
342
343 const auto file = QFileDialog::getOpenFileName(this, tr("Select User Image"), QString(),
344 tr("JPEG Images (*.jpg *.jpeg)"));
345
346 if (file.isEmpty()) {
347 return;
348 }
349
350 const auto image_path = GetImagePath(*uuid);
351 if (QFile::exists(image_path) && !QFile::remove(image_path)) {
352 QMessageBox::warning(
353 this, tr("Error deleting image"),
354 tr("Error occurred attempting to overwrite previous image at: %1.").arg(image_path));
355 return;
356 }
357
358 const auto raw_path = QString::fromStdString(
359 FileUtil::GetUserPath(FileUtil::UserPath::NANDDir) + "/system/save/8000000000000010");
360 const QFileInfo raw_info{raw_path};
361 if (raw_info.exists() && !raw_info.isDir() && !QFile::remove(raw_path)) {
362 QMessageBox::warning(this, tr("Error deleting file"),
363 tr("Unable to delete existing file: %1.").arg(raw_path));
364 return;
365 }
366
367 const QString absolute_dst_path = QFileInfo{image_path}.absolutePath();
368 if (!QDir{raw_path}.mkpath(absolute_dst_path)) {
369 QMessageBox::warning(
370 this, tr("Error creating user image directory"),
371 tr("Unable to create directory %1 for storing user images.").arg(absolute_dst_path));
372 return;
373 }
374
375 if (!QFile::copy(file, image_path)) {
376 QMessageBox::warning(this, tr("Error copying user image"),
377 tr("Unable to copy image from %1 to %2").arg(file, image_path));
378 return;
379 }
380
381 const auto username = GetAccountUsername(*profile_manager, *uuid);
382 item_model->setItem(index, 0,
383 new QStandardItem{GetIcon(*uuid), FormatUserEntryText(username, *uuid)});
384 UpdateCurrentUser();
385}
diff --git a/src/yuzu/configuration/configure_system.h b/src/yuzu/configuration/configure_system.h
index 07764e1f7..cf1e54de5 100644
--- a/src/yuzu/configuration/configure_system.h
+++ b/src/yuzu/configuration/configure_system.h
@@ -9,16 +9,6 @@
9#include <QList> 9#include <QList>
10#include <QWidget> 10#include <QWidget>
11 11
12class QGraphicsScene;
13class QStandardItem;
14class QStandardItemModel;
15class QTreeView;
16class QVBoxLayout;
17
18namespace Service::Account {
19class ProfileManager;
20}
21
22namespace Ui { 12namespace Ui {
23class ConfigureSystem; 13class ConfigureSystem;
24} 14}
@@ -39,21 +29,6 @@ private:
39 void UpdateBirthdayComboBox(int birthmonth_index); 29 void UpdateBirthdayComboBox(int birthmonth_index);
40 void RefreshConsoleID(); 30 void RefreshConsoleID();
41 31
42 void PopulateUserList();
43 void UpdateCurrentUser();
44 void SelectUser(const QModelIndex& index);
45 void AddUser();
46 void RenameUser();
47 void DeleteUser();
48 void SetUserImage();
49
50 QVBoxLayout* layout;
51 QTreeView* tree_view;
52 QStandardItemModel* item_model;
53 QGraphicsScene* scene;
54
55 std::vector<QList<QStandardItem*>> list_items;
56
57 std::unique_ptr<Ui::ConfigureSystem> ui; 32 std::unique_ptr<Ui::ConfigureSystem> ui;
58 bool enabled = false; 33 bool enabled = false;
59 34
@@ -61,6 +36,4 @@ private:
61 int birthday = 0; 36 int birthday = 0;
62 int language_index = 0; 37 int language_index = 0;
63 int sound_index = 0; 38 int sound_index = 0;
64
65 std::unique_ptr<Service::Account::ProfileManager> profile_manager;
66}; 39};
diff --git a/src/yuzu/configuration/configure_system.ui b/src/yuzu/configuration/configure_system.ui
index a91580893..74e800c2a 100644
--- a/src/yuzu/configuration/configure_system.ui
+++ b/src/yuzu/configuration/configure_system.ui
@@ -280,141 +280,17 @@
280 </widget> 280 </widget>
281 </item> 281 </item>
282 <item> 282 <item>
283 <widget class="QGroupBox" name="gridGroupBox"> 283 <spacer name="verticalSpacer">
284 <property name="title"> 284 <property name="orientation">
285 <string>Profile Manager</string> 285 <enum>Qt::Vertical</enum>
286 </property> 286 </property>
287 <layout class="QGridLayout" name="gridLayout_2"> 287 <property name="sizeHint" stdset="0">
288 <property name="sizeConstraint"> 288 <size>
289 <enum>QLayout::SetNoConstraint</enum> 289 <width>20</width>
290 </property> 290 <height>40</height>
291 <item row="0" column="0"> 291 </size>
292 <layout class="QHBoxLayout" name="horizontalLayout_2"> 292 </property>
293 <item> 293 </spacer>
294 <widget class="QLabel" name="label">
295 <property name="sizePolicy">
296 <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
297 <horstretch>0</horstretch>
298 <verstretch>0</verstretch>
299 </sizepolicy>
300 </property>
301 <property name="text">
302 <string>Current User</string>
303 </property>
304 </widget>
305 </item>
306 <item>
307 <widget class="QGraphicsView" name="current_user_icon">
308 <property name="minimumSize">
309 <size>
310 <width>48</width>
311 <height>48</height>
312 </size>
313 </property>
314 <property name="maximumSize">
315 <size>
316 <width>48</width>
317 <height>48</height>
318 </size>
319 </property>
320 <property name="verticalScrollBarPolicy">
321 <enum>Qt::ScrollBarAlwaysOff</enum>
322 </property>
323 <property name="horizontalScrollBarPolicy">
324 <enum>Qt::ScrollBarAlwaysOff</enum>
325 </property>
326 <property name="interactive">
327 <bool>false</bool>
328 </property>
329 </widget>
330 </item>
331 <item>
332 <widget class="QLabel" name="current_user_username">
333 <property name="sizePolicy">
334 <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
335 <horstretch>0</horstretch>
336 <verstretch>0</verstretch>
337 </sizepolicy>
338 </property>
339 <property name="text">
340 <string>Username</string>
341 </property>
342 </widget>
343 </item>
344 </layout>
345 </item>
346 <item row="1" column="0">
347 <widget class="QScrollArea" name="scrollArea">
348 <property name="sizePolicy">
349 <sizepolicy hsizetype="Expanding" vsizetype="Expanding">
350 <horstretch>0</horstretch>
351 <verstretch>0</verstretch>
352 </sizepolicy>
353 </property>
354 <property name="frameShape">
355 <enum>QFrame::StyledPanel</enum>
356 </property>
357 <property name="widgetResizable">
358 <bool>false</bool>
359 </property>
360 </widget>
361 </item>
362 <item row="2" column="0">
363 <layout class="QHBoxLayout" name="horizontalLayout_3">
364 <item>
365 <widget class="QPushButton" name="pm_set_image">
366 <property name="enabled">
367 <bool>false</bool>
368 </property>
369 <property name="text">
370 <string>Set Image</string>
371 </property>
372 </widget>
373 </item>
374 <item>
375 <spacer name="horizontalSpacer">
376 <property name="orientation">
377 <enum>Qt::Horizontal</enum>
378 </property>
379 <property name="sizeHint" stdset="0">
380 <size>
381 <width>40</width>
382 <height>20</height>
383 </size>
384 </property>
385 </spacer>
386 </item>
387 <item>
388 <widget class="QPushButton" name="pm_add">
389 <property name="text">
390 <string>Add</string>
391 </property>
392 </widget>
393 </item>
394 <item>
395 <widget class="QPushButton" name="pm_rename">
396 <property name="enabled">
397 <bool>false</bool>
398 </property>
399 <property name="text">
400 <string>Rename</string>
401 </property>
402 </widget>
403 </item>
404 <item>
405 <widget class="QPushButton" name="pm_remove">
406 <property name="enabled">
407 <bool>false</bool>
408 </property>
409 <property name="text">
410 <string>Remove</string>
411 </property>
412 </widget>
413 </item>
414 </layout>
415 </item>
416 </layout>
417 </widget>
418 </item> 294 </item>
419 <item> 295 <item>
420 <widget class="QLabel" name="label_disable_info"> 296 <widget class="QLabel" name="label_disable_info">