Browse Source
[desktop] refactor profile management and fix some misc bugs with it (#3415)
[desktop] refactor profile management and fix some misc bugs with it (#3415)
Adding and editing users is now done in a single dialog rather than all those other individual buttons and dialogs like before. Fixed some bugs with profile management too, and made edit/delete a right-click menu. Signed-off-by: crueter <crueter@eden-emu.dev> Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/3415pull/3417/head
No known key found for this signature in database
GPG Key ID: 425ACD2D4830EBC6
12 changed files with 955 additions and 465 deletions
-
3.ci/license-header.sh
-
3src/core/hle/service/acc/profile_manager.cpp
-
7src/frontend_common/data_manager.cpp
-
3src/yuzu/CMakeLists.txt
-
501src/yuzu/configuration/configure_profile_manager.cpp
-
34src/yuzu/configuration/configure_profile_manager.h
-
68src/yuzu/configuration/configure_profile_manager.ui
-
372src/yuzu/configuration/system/new_user_dialog.cpp
-
71src/yuzu/configuration/system/new_user_dialog.h
-
192src/yuzu/configuration/system/new_user_dialog.ui
-
136src/yuzu/configuration/system/profile_avatar_dialog.cpp
-
28src/yuzu/configuration/system/profile_avatar_dialog.h
@ -0,0 +1,372 @@ |
|||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
|||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|||
|
|||
#include <algorithm>
|
|||
#include "common/common_types.h"
|
|||
#include "common/fs/path_util.h"
|
|||
#include "common/uuid.h"
|
|||
#include "configuration/system/profile_avatar_dialog.h"
|
|||
#include "core/constants.h"
|
|||
#include "core/file_sys/content_archive.h"
|
|||
#include "core/file_sys/nca_metadata.h"
|
|||
#include "core/file_sys/romfs.h"
|
|||
#include "core/hle/service/filesystem/filesystem.h"
|
|||
#include "new_user_dialog.h"
|
|||
#include "qt_common/qt_common.h"
|
|||
#include "ui_new_user_dialog.h"
|
|||
|
|||
#include <QFileDialog>
|
|||
#include <QMessageBox>
|
|||
#include <QStyle>
|
|||
#include <fmt/format.h>
|
|||
#include <qnamespace.h>
|
|||
#include <qregularexpression.h>
|
|||
#include <qvalidator.h>
|
|||
|
|||
QPixmap NewUserDialog::DefaultAvatar() { |
|||
QPixmap 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); |
|||
} |
|||
|
|||
QString NewUserDialog::GetImagePath(const 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 NewUserDialog::GetIcon(const Common::UUID& uuid) { |
|||
QPixmap icon{GetImagePath(uuid)}; |
|||
|
|||
if (!icon) { |
|||
return DefaultAvatar(); |
|||
} |
|||
|
|||
return icon.scaled(64, 64, Qt::IgnoreAspectRatio, Qt::SmoothTransformation); |
|||
} |
|||
|
|||
NewUserDialog::NewUserDialog(Common::UUID uuid, const std::string& username, const QString& title, |
|||
QWidget* parent) |
|||
: QDialog(parent) { |
|||
m_editing = true; |
|||
setup(uuid, username, title); |
|||
} |
|||
|
|||
NewUserDialog::NewUserDialog(QWidget* parent) : QDialog(parent) { |
|||
setup(Common::UUID::MakeRandom(), "Eden", tr("New User")); |
|||
} |
|||
|
|||
void NewUserDialog::setup(Common::UUID uuid, const std::string& username, const QString& title) { |
|||
ui = new Ui::NewUserDialog; |
|||
ui->setupUi(this); |
|||
|
|||
setWindowTitle(title); |
|||
|
|||
m_scene = new QGraphicsScene; |
|||
ui->image->setScene(m_scene); |
|||
|
|||
// setup
|
|||
|
|||
// Byte reversing here is incredibly wack, but basically the way I've implemented it
|
|||
// is such that when you view it, it will match the folder name always.
|
|||
// TODO: Add UUID changing
|
|||
if (!m_editing) { |
|||
ui->uuid->setText(QString::fromStdString(uuid.RawString()).toUpper()); |
|||
} else { |
|||
QByteArray bytes = QByteArray::fromHex(QString::fromStdString(uuid.RawString()).toLatin1()); |
|||
std::reverse(bytes.begin(), bytes.end()); |
|||
QString txt = QString::fromLatin1(bytes.toHex().toUpper()); |
|||
|
|||
ui->uuid->setText(txt); |
|||
ui->uuid->setReadOnly(true); |
|||
} |
|||
|
|||
ui->username->setText(QString::fromStdString(username)); |
|||
verifyUser(); |
|||
|
|||
setImage(GetIcon(uuid)); |
|||
updateRevertButton(); |
|||
|
|||
// Validators
|
|||
QRegularExpressionValidator* username_validator = |
|||
new QRegularExpressionValidator(QRegularExpression(QStringLiteral(".{1,32}")), this); |
|||
ui->username->setValidator(username_validator); |
|||
|
|||
QRegularExpressionValidator* uuid_validator = new QRegularExpressionValidator( |
|||
QRegularExpression(QStringLiteral("[0-9a-fA-F]{32}")), this); |
|||
ui->uuid->setValidator(uuid_validator); |
|||
|
|||
// Connections
|
|||
connect(ui->uuid, &QLineEdit::textEdited, this, &::NewUserDialog::verifyUser); |
|||
connect(ui->username, &QLineEdit::textEdited, this, &::NewUserDialog::verifyUser); |
|||
|
|||
connect(ui->revert, &QAbstractButton::clicked, this, &NewUserDialog::revertImage); |
|||
connect(ui->selectImage, &QAbstractButton::clicked, this, &NewUserDialog::selectImage); |
|||
connect(ui->setAvatar, &QAbstractButton::clicked, this, &NewUserDialog::setAvatar); |
|||
connect(ui->generate, &QAbstractButton::clicked, this, &NewUserDialog::generateUUID); |
|||
|
|||
connect(this, &NewUserDialog::isDefaultAvatarChanged, this, &NewUserDialog::updateRevertButton); |
|||
connect(this, &QDialog::accepted, this, &NewUserDialog::dispatchUser); |
|||
|
|||
// dialog
|
|||
avatar_dialog = new ProfileAvatarDialog(this); |
|||
} |
|||
|
|||
NewUserDialog::~NewUserDialog() { |
|||
delete ui; |
|||
} |
|||
|
|||
bool NewUserDialog::isDefaultAvatar() const { |
|||
return m_isDefaultAvatar; |
|||
} |
|||
|
|||
void NewUserDialog::setIsDefaultAvatar(bool newIsDefaultAvatar) { |
|||
if (m_isDefaultAvatar == newIsDefaultAvatar) |
|||
return; |
|||
m_isDefaultAvatar = newIsDefaultAvatar; |
|||
emit isDefaultAvatarChanged(m_isDefaultAvatar); |
|||
} |
|||
|
|||
void NewUserDialog::selectImage() { |
|||
const auto file = QFileDialog::getOpenFileName(this, tr("Select User Image"), QString(), |
|||
tr("Image Formats (*.jpg *.jpeg *.png *.bmp)")); |
|||
if (file.isEmpty()) { |
|||
return; |
|||
} |
|||
|
|||
// Profile image must be 256x256
|
|||
QPixmap image(file); |
|||
if (image.width() != 256 || image.height() != 256) { |
|||
image = image.scaled(256, 256, Qt::KeepAspectRatioByExpanding, Qt::SmoothTransformation); |
|||
} |
|||
|
|||
setImage(image.scaled(64, 64, Qt::IgnoreAspectRatio)); |
|||
setIsDefaultAvatar(false); |
|||
} |
|||
|
|||
void NewUserDialog::setAvatar() { |
|||
if (!avatar_dialog->AreImagesLoaded()) { |
|||
if (!LoadAvatarData()) { |
|||
return; |
|||
} |
|||
} |
|||
if (avatar_dialog->exec() == QDialog::Accepted) { |
|||
setImage(avatar_dialog->GetSelectedAvatar().scaled(64, 64, Qt::IgnoreAspectRatio)); |
|||
} |
|||
} |
|||
|
|||
bool NewUserDialog::LoadAvatarData() { |
|||
constexpr u64 AvatarImageDataId = 0x010000000000080AULL; |
|||
|
|||
// Attempt to load avatar data archive from installed firmware
|
|||
auto* bis_system = QtCommon::system->GetFileSystemController().GetSystemNANDContents(); |
|||
if (!bis_system) { |
|||
QMessageBox::warning(this, tr("No firmware available"), |
|||
tr("Please install the firmware to use firmware avatars.")); |
|||
return false; |
|||
} |
|||
const auto nca = bis_system->GetEntry(AvatarImageDataId, FileSys::ContentRecordType::Data); |
|||
if (!nca) { |
|||
QMessageBox::warning(this, tr("Error loading archive"), |
|||
tr("Archive is not available. Please install/reinstall firmware.")); |
|||
return false; |
|||
} |
|||
const auto romfs = nca->GetRomFS(); |
|||
if (!romfs) { |
|||
QMessageBox::warning( |
|||
this, tr("Error loading archive"), |
|||
tr("Could not locate RomFS. Your file or decryption keys may be corrupted.")); |
|||
return false; |
|||
} |
|||
const auto extracted = FileSys::ExtractRomFS(romfs); |
|||
if (!extracted) { |
|||
QMessageBox::warning( |
|||
this, tr("Error extracting archive"), |
|||
tr("Could not extract RomFS. Your file or decryption keys may be corrupted.")); |
|||
return false; |
|||
} |
|||
const auto chara_dir = extracted->GetSubdirectory("chara"); |
|||
if (!chara_dir) { |
|||
QMessageBox::warning(this, tr("Error finding image directory"), |
|||
tr("Failed to find image directory in the archive.")); |
|||
return false; |
|||
} |
|||
|
|||
QVector<QPixmap> images; |
|||
for (const auto& item : chara_dir->GetFiles()) { |
|||
if (item->GetExtension() != "szs") { |
|||
continue; |
|||
} |
|||
|
|||
auto image_data = DecompressYaz0(item); |
|||
if (image_data.empty()) { |
|||
continue; |
|||
} |
|||
QImage image(reinterpret_cast<const uchar*>(image_data.data()), 256, 256, |
|||
QImage::Format_RGBA8888); |
|||
images.append(QPixmap::fromImage(image)); |
|||
} |
|||
|
|||
if (images.isEmpty()) { |
|||
QMessageBox::warning(this, tr("No images found"), |
|||
tr("No avatar images were found in the archive.")); |
|||
return false; |
|||
} |
|||
|
|||
// Load the image data into the dialog
|
|||
avatar_dialog->LoadImages(images); |
|||
return true; |
|||
} |
|||
|
|||
std::vector<uint8_t> NewUserDialog::DecompressYaz0(const FileSys::VirtualFile& file) { |
|||
if (!file) { |
|||
throw std::invalid_argument("Null file pointer passed to DecompressYaz0"); |
|||
} |
|||
|
|||
uint32_t magic{}; |
|||
file->ReadObject(&magic, 0); |
|||
if (magic != Common::MakeMagic('Y', 'a', 'z', '0')) { |
|||
return std::vector<uint8_t>(); |
|||
} |
|||
|
|||
uint32_t decoded_length{}; |
|||
file->ReadObject(&decoded_length, 4); |
|||
decoded_length = Common::swap32(decoded_length); |
|||
|
|||
std::size_t input_size = file->GetSize() - 16; |
|||
std::vector<uint8_t> input(input_size); |
|||
file->ReadBytes(input.data(), input_size, 16); |
|||
|
|||
uint32_t input_offset{}; |
|||
uint32_t output_offset{}; |
|||
std::vector<uint8_t> output(decoded_length); |
|||
|
|||
uint16_t mask{}; |
|||
uint8_t header{}; |
|||
|
|||
while (output_offset < decoded_length) { |
|||
if ((mask >>= 1) == 0) { |
|||
header = input[input_offset++]; |
|||
mask = 0x80; |
|||
} |
|||
|
|||
if ((header & mask) != 0) { |
|||
if (output_offset == output.size()) { |
|||
break; |
|||
} |
|||
output[output_offset++] = input[input_offset++]; |
|||
} else { |
|||
uint8_t byte1 = input[input_offset++]; |
|||
uint8_t byte2 = input[input_offset++]; |
|||
|
|||
uint32_t dist = ((byte1 & 0xF) << 8) | byte2; |
|||
uint32_t position = output_offset - (dist + 1); |
|||
|
|||
uint32_t length = byte1 >> 4; |
|||
if (length == 0) { |
|||
length = static_cast<uint32_t>(input[input_offset++]) + 0x12; |
|||
} else { |
|||
length += 2; |
|||
} |
|||
|
|||
uint32_t gap = output_offset - position; |
|||
uint32_t non_overlapping_length = length; |
|||
|
|||
if (non_overlapping_length > gap) { |
|||
non_overlapping_length = gap; |
|||
} |
|||
|
|||
std::memcpy(&output[output_offset], &output[position], non_overlapping_length); |
|||
output_offset += non_overlapping_length; |
|||
position += non_overlapping_length; |
|||
length -= non_overlapping_length; |
|||
|
|||
while (length-- > 0) { |
|||
output[output_offset++] = output[position++]; |
|||
} |
|||
} |
|||
} |
|||
|
|||
return output; |
|||
} |
|||
|
|||
void NewUserDialog::setImage(const QPixmap& pixmap) { |
|||
m_pixmap = pixmap; |
|||
m_scene->clear(); |
|||
m_scene->addPixmap(m_pixmap); |
|||
} |
|||
|
|||
void NewUserDialog::revertImage() { |
|||
setImage(DefaultAvatar()); |
|||
setIsDefaultAvatar(true); |
|||
} |
|||
|
|||
void NewUserDialog::updateRevertButton() { |
|||
if (isDefaultAvatar()) { |
|||
ui->revert->setIcon(QIcon{}); |
|||
} else { |
|||
QStyle* style = parentWidget()->style(); |
|||
QIcon icon(style->standardIcon(QStyle::SP_LineEditClearButton)); |
|||
ui->revert->setIcon(icon); |
|||
} |
|||
} |
|||
|
|||
void NewUserDialog::generateUUID() { |
|||
Common::UUID uuid = Common::UUID::MakeRandom(); |
|||
ui->uuid->setText(QString::fromStdString(uuid.RawString()).toUpper()); |
|||
} |
|||
|
|||
void NewUserDialog::verifyUser() { |
|||
const QPixmap checked = QIcon::fromTheme(QStringLiteral("checked")).pixmap(16); |
|||
const QPixmap failed = QIcon::fromTheme(QStringLiteral("failed")).pixmap(16); |
|||
|
|||
const bool uuid_good = ui->uuid->hasAcceptableInput(); |
|||
const bool username_good = ui->username->hasAcceptableInput(); |
|||
|
|||
auto ok_button = ui->buttonBox->button(QDialogButtonBox::Ok); |
|||
ok_button->setEnabled(username_good && uuid_good); |
|||
|
|||
if (uuid_good) { |
|||
ui->uuidVerified->setPixmap(checked); |
|||
ui->uuidVerified->setToolTip(tr("All Good", "Tooltip")); |
|||
} else { |
|||
ui->uuidVerified->setPixmap(failed); |
|||
ui->uuidVerified->setToolTip(tr("Must be 32 hex characters (0-9, a-f)", "Tooltip")); |
|||
} |
|||
|
|||
if (username_good) { |
|||
ui->usernameVerified->setPixmap(checked); |
|||
ui->usernameVerified->setToolTip(tr("All Good", "Tooltip")); |
|||
} else { |
|||
ui->usernameVerified->setPixmap(failed); |
|||
ui->usernameVerified->setToolTip(tr("Must be between 1 and 32 characters", "Tooltip")); |
|||
} |
|||
} |
|||
|
|||
// TODO: Move UUID
|
|||
void NewUserDialog::dispatchUser() { |
|||
QByteArray bytes = QByteArray::fromHex(ui->uuid->text().toLatin1()); |
|||
|
|||
// convert to 16 u8's
|
|||
std::array<u8, 16> uuid_arr; |
|||
std::copy_n(reinterpret_cast<const u8*>(bytes.constData()), 16, uuid_arr.begin()); |
|||
|
|||
if (!m_editing) { |
|||
std::ranges::reverse(uuid_arr); |
|||
} |
|||
|
|||
Common::UUID uuid(uuid_arr); |
|||
|
|||
User user{ |
|||
.username = ui->username->text(), |
|||
.uuid = uuid, |
|||
.pixmap = m_pixmap, |
|||
}; |
|||
|
|||
emit userAdded(user); |
|||
} |
|||
@ -0,0 +1,71 @@ |
|||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project |
|||
// SPDX-License-Identifier: GPL-3.0-or-later |
|||
|
|||
#pragma once |
|||
|
|||
#include <QDialog> |
|||
#include "common/uuid.h" |
|||
#include "core/file_sys/vfs/vfs_types.h" |
|||
|
|||
class QGraphicsScene; |
|||
class ProfileAvatarDialog; |
|||
|
|||
namespace Ui { |
|||
class NewUserDialog; |
|||
} |
|||
|
|||
struct User { |
|||
QString username; |
|||
Common::UUID uuid; |
|||
QPixmap pixmap; |
|||
}; |
|||
|
|||
class NewUserDialog : public QDialog |
|||
{ |
|||
Q_OBJECT |
|||
Q_PROPERTY(bool isDefaultAvatar READ isDefaultAvatar WRITE setIsDefaultAvatar NOTIFY |
|||
isDefaultAvatarChanged FINAL) |
|||
|
|||
public: |
|||
explicit NewUserDialog(QWidget *parent = nullptr); |
|||
explicit NewUserDialog(Common::UUID uuid, const std::string &username, const QString &title, QWidget *parent = nullptr); |
|||
~NewUserDialog(); |
|||
|
|||
bool isDefaultAvatar() const; |
|||
void setIsDefaultAvatar(bool newIsDefaultAvatar); |
|||
|
|||
static QString GetImagePath(const Common::UUID& uuid); |
|||
static QPixmap GetIcon(const Common::UUID& uuid); |
|||
static QPixmap DefaultAvatar(); |
|||
|
|||
private: |
|||
Ui::NewUserDialog *ui; |
|||
QGraphicsScene *m_scene; |
|||
QPixmap m_pixmap; |
|||
|
|||
ProfileAvatarDialog* avatar_dialog; |
|||
|
|||
bool m_isDefaultAvatar = true; |
|||
bool m_editing = false; |
|||
|
|||
void setup(Common::UUID uuid, const std::string &username, const QString &title); |
|||
bool LoadAvatarData(); |
|||
std::vector<uint8_t> DecompressYaz0(const FileSys::VirtualFile& file); |
|||
|
|||
public slots: |
|||
void setImage(const QPixmap &pixmap); |
|||
void selectImage(); |
|||
void setAvatar(); |
|||
|
|||
void revertImage(); |
|||
void updateRevertButton(); |
|||
|
|||
void generateUUID(); |
|||
void verifyUser(); |
|||
|
|||
void dispatchUser(); |
|||
|
|||
signals: |
|||
void isDefaultAvatarChanged(bool isDefaultAvatar); |
|||
void userAdded(User user); |
|||
}; |
|||
@ -0,0 +1,192 @@ |
|||
<?xml version="1.0" encoding="UTF-8"?> |
|||
<ui version="4.0"> |
|||
<class>NewUserDialog</class> |
|||
<widget class="QDialog" name="NewUserDialog"> |
|||
<property name="geometry"> |
|||
<rect> |
|||
<x>0</x> |
|||
<y>0</y> |
|||
<width>448</width> |
|||
<height>220</height> |
|||
</rect> |
|||
</property> |
|||
<property name="windowTitle"> |
|||
<string>New User</string> |
|||
</property> |
|||
<layout class="QGridLayout" name="gridLayout_2"> |
|||
<item row="2" column="1" colspan="3"> |
|||
<widget class="QDialogButtonBox" name="buttonBox"> |
|||
<property name="orientation"> |
|||
<enum>Qt::Orientation::Horizontal</enum> |
|||
</property> |
|||
<property name="standardButtons"> |
|||
<set>QDialogButtonBox::StandardButton::Cancel|QDialogButtonBox::StandardButton::Ok</set> |
|||
</property> |
|||
</widget> |
|||
</item> |
|||
<item row="1" column="2"> |
|||
<widget class="QPushButton" name="setAvatar"> |
|||
<property name="text"> |
|||
<string>Change Avatar</string> |
|||
</property> |
|||
</widget> |
|||
</item> |
|||
<item row="1" column="1"> |
|||
<widget class="QPushButton" name="selectImage"> |
|||
<property name="text"> |
|||
<string>Set Image</string> |
|||
</property> |
|||
</widget> |
|||
</item> |
|||
<item row="1" column="3"> |
|||
<widget class="QToolButton" name="revert"> |
|||
<property name="text"> |
|||
<string/> |
|||
</property> |
|||
</widget> |
|||
</item> |
|||
<item row="0" column="0" colspan="4"> |
|||
<layout class="QGridLayout" name="gridLayout"> |
|||
<item row="1" column="0"> |
|||
<widget class="QLabel" name="label"> |
|||
<property name="text"> |
|||
<string>UUID</string> |
|||
</property> |
|||
</widget> |
|||
</item> |
|||
<item row="0" column="1" colspan="2"> |
|||
<widget class="QLineEdit" name="username"> |
|||
<property name="text"> |
|||
<string>Eden</string> |
|||
</property> |
|||
</widget> |
|||
</item> |
|||
<item row="1" column="1" colspan="2"> |
|||
<widget class="QLineEdit" name="uuid"/> |
|||
</item> |
|||
<item row="0" column="0"> |
|||
<widget class="QLabel" name="label_2"> |
|||
<property name="text"> |
|||
<string>Username</string> |
|||
</property> |
|||
</widget> |
|||
</item> |
|||
<item row="2" column="1"> |
|||
<widget class="QLabel" name="label_3"> |
|||
<property name="font"> |
|||
<font> |
|||
<italic>true</italic> |
|||
</font> |
|||
</property> |
|||
<property name="text"> |
|||
<string>UUID must be 32 hex characters (0-9, A-F)</string> |
|||
</property> |
|||
</widget> |
|||
</item> |
|||
<item row="2" column="2"> |
|||
<widget class="QPushButton" name="generate"> |
|||
<property name="text"> |
|||
<string>Generate</string> |
|||
</property> |
|||
</widget> |
|||
</item> |
|||
<item row="1" column="3"> |
|||
<widget class="QLabel" name="uuidVerified"> |
|||
<property name="sizePolicy"> |
|||
<sizepolicy hsizetype="Fixed" vsizetype="Preferred"> |
|||
<horstretch>0</horstretch> |
|||
<verstretch>0</verstretch> |
|||
</sizepolicy> |
|||
</property> |
|||
<property name="minimumSize"> |
|||
<size> |
|||
<width>24</width> |
|||
<height>0</height> |
|||
</size> |
|||
</property> |
|||
</widget> |
|||
</item> |
|||
<item row="0" column="3"> |
|||
<widget class="QLabel" name="usernameVerified"> |
|||
<property name="text"> |
|||
<string/> |
|||
</property> |
|||
</widget> |
|||
</item> |
|||
</layout> |
|||
</item> |
|||
<item row="1" column="0"> |
|||
<widget class="QGraphicsView" name="image"> |
|||
<property name="sizePolicy"> |
|||
<sizepolicy hsizetype="Expanding" vsizetype="Expanding"> |
|||
<horstretch>0</horstretch> |
|||
<verstretch>0</verstretch> |
|||
</sizepolicy> |
|||
</property> |
|||
<property name="minimumSize"> |
|||
<size> |
|||
<width>64</width> |
|||
<height>64</height> |
|||
</size> |
|||
</property> |
|||
<property name="maximumSize"> |
|||
<size> |
|||
<width>64</width> |
|||
<height>64</height> |
|||
</size> |
|||
</property> |
|||
<property name="frameShape"> |
|||
<enum>QFrame::Shape::NoFrame</enum> |
|||
</property> |
|||
<property name="frameShadow"> |
|||
<enum>QFrame::Shadow::Plain</enum> |
|||
</property> |
|||
<property name="verticalScrollBarPolicy"> |
|||
<enum>Qt::ScrollBarPolicy::ScrollBarAlwaysOff</enum> |
|||
</property> |
|||
<property name="horizontalScrollBarPolicy"> |
|||
<enum>Qt::ScrollBarPolicy::ScrollBarAlwaysOff</enum> |
|||
</property> |
|||
<property name="interactive"> |
|||
<bool>false</bool> |
|||
</property> |
|||
</widget> |
|||
</item> |
|||
</layout> |
|||
</widget> |
|||
<resources/> |
|||
<connections> |
|||
<connection> |
|||
<sender>buttonBox</sender> |
|||
<signal>accepted()</signal> |
|||
<receiver>NewUserDialog</receiver> |
|||
<slot>accept()</slot> |
|||
<hints> |
|||
<hint type="sourcelabel"> |
|||
<x>248</x> |
|||
<y>254</y> |
|||
</hint> |
|||
<hint type="destinationlabel"> |
|||
<x>157</x> |
|||
<y>274</y> |
|||
</hint> |
|||
</hints> |
|||
</connection> |
|||
<connection> |
|||
<sender>buttonBox</sender> |
|||
<signal>rejected()</signal> |
|||
<receiver>NewUserDialog</receiver> |
|||
<slot>reject()</slot> |
|||
<hints> |
|||
<hint type="sourcelabel"> |
|||
<x>316</x> |
|||
<y>260</y> |
|||
</hint> |
|||
<hint type="destinationlabel"> |
|||
<x>286</x> |
|||
<y>274</y> |
|||
</hint> |
|||
</hints> |
|||
</connection> |
|||
</connections> |
|||
</ui> |
|||
@ -0,0 +1,136 @@ |
|||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
|||
// SPDX-License-Identifier: GPL-3.0-or-later
|
|||
|
|||
#include "profile_avatar_dialog.h"
|
|||
|
|||
#include <QBoxLayout>
|
|||
#include <QColorDialog>
|
|||
#include <QLabel>
|
|||
#include <QListWidget>
|
|||
#include <QPainter>
|
|||
#include <QPushButton>
|
|||
|
|||
// TODO: Move this to a .ui def
|
|||
ProfileAvatarDialog::ProfileAvatarDialog(QWidget* parent) |
|||
: QDialog{parent}, avatar_list{new QListWidget(this)}, bg_color_button{new QPushButton(this)} { |
|||
auto* main_layout = new QVBoxLayout(this); |
|||
auto* button_layout = new QHBoxLayout(); |
|||
auto* select_button = new QPushButton(tr("Select"), this); |
|||
auto* cancel_button = new QPushButton(tr("Cancel"), this); |
|||
auto* bg_color_label = new QLabel(tr("Background Color"), this); |
|||
|
|||
SetBackgroundColor(Qt::white); |
|||
|
|||
avatar_list->setViewMode(QListView::IconMode); |
|||
avatar_list->setIconSize(QSize(64, 64)); |
|||
avatar_list->setSpacing(4); |
|||
avatar_list->setResizeMode(QListView::Adjust); |
|||
avatar_list->setSelectionMode(QAbstractItemView::SingleSelection); |
|||
avatar_list->setEditTriggers(QAbstractItemView::NoEditTriggers); |
|||
avatar_list->setDragDropMode(QAbstractItemView::NoDragDrop); |
|||
avatar_list->setDragEnabled(false); |
|||
avatar_list->setDropIndicatorShown(false); |
|||
avatar_list->setAcceptDrops(false); |
|||
|
|||
button_layout->addWidget(bg_color_button); |
|||
button_layout->addWidget(bg_color_label); |
|||
button_layout->addStretch(); |
|||
button_layout->addWidget(select_button); |
|||
button_layout->addWidget(cancel_button); |
|||
|
|||
this->setWindowTitle(tr("Select Firmware Avatar")); |
|||
main_layout->addWidget(avatar_list); |
|||
main_layout->addLayout(button_layout); |
|||
|
|||
connect(bg_color_button, &QPushButton::clicked, this, [this]() { |
|||
const auto new_color = QColorDialog::getColor(avatar_bg_color); |
|||
if (new_color.isValid()) { |
|||
SetBackgroundColor(new_color); |
|||
RefreshAvatars(); |
|||
} |
|||
}); |
|||
connect(select_button, &QPushButton::clicked, this, [this]() { accept(); }); |
|||
connect(cancel_button, &QPushButton::clicked, this, [this]() { reject(); }); |
|||
} |
|||
|
|||
ProfileAvatarDialog::~ProfileAvatarDialog() = default; |
|||
|
|||
void ProfileAvatarDialog::SetBackgroundColor(const QColor& color) { |
|||
avatar_bg_color = color; |
|||
|
|||
bg_color_button->setStyleSheet( |
|||
QStringLiteral("background-color: %1; min-width: 60px;").arg(avatar_bg_color.name())); |
|||
} |
|||
|
|||
QPixmap ProfileAvatarDialog::CreateAvatar(const QPixmap& avatar) { |
|||
QPixmap output(avatar.size()); |
|||
output.fill(avatar_bg_color); |
|||
|
|||
// Scale the image and fill it black to become our shadow
|
|||
QPixmap shadow_pixmap = avatar.transformed(QTransform::fromScale(1.04, 1.04)); |
|||
QPainter shadow_painter(&shadow_pixmap); |
|||
shadow_painter.setCompositionMode(QPainter::CompositionMode_SourceIn); |
|||
shadow_painter.fillRect(shadow_pixmap.rect(), Qt::black); |
|||
shadow_painter.end(); |
|||
|
|||
QPainter painter(&output); |
|||
painter.setOpacity(0.10); |
|||
painter.drawPixmap(0, 0, shadow_pixmap); |
|||
painter.setOpacity(1.0); |
|||
painter.drawPixmap(0, 0, avatar); |
|||
painter.end(); |
|||
|
|||
return output; |
|||
} |
|||
|
|||
void ProfileAvatarDialog::RefreshAvatars() { |
|||
if (avatar_list->count() != avatar_image_store.size()) { |
|||
return; |
|||
} |
|||
for (int i = 0; i < avatar_image_store.size(); ++i) { |
|||
const auto icon = |
|||
QIcon(CreateAvatar(avatar_image_store[i]) |
|||
.scaled(64, 64, Qt::IgnoreAspectRatio, Qt::SmoothTransformation)); |
|||
avatar_list->item(i)->setIcon(icon); |
|||
} |
|||
} |
|||
|
|||
void ProfileAvatarDialog::LoadImages(const QVector<QPixmap>& avatar_images) { |
|||
avatar_image_store = avatar_images; |
|||
avatar_list->clear(); |
|||
|
|||
for (int i = 0; i < avatar_image_store.size(); ++i) { |
|||
avatar_list->addItem(new QListWidgetItem); |
|||
} |
|||
RefreshAvatars(); |
|||
|
|||
// Determine window size now that avatars are loaded into the grid
|
|||
// There is probably a better way to handle this that I'm unaware of
|
|||
const auto* style = avatar_list->style(); |
|||
|
|||
const int icon_size = avatar_list->iconSize().width(); |
|||
const int icon_spacing = avatar_list->spacing() * 2; |
|||
const int icon_margin = style->pixelMetric(QStyle::PM_FocusFrameHMargin); |
|||
const int icon_full_size = icon_size + icon_spacing + icon_margin; |
|||
|
|||
const int horizontal_margin = style->pixelMetric(QStyle::PM_LayoutLeftMargin) + |
|||
style->pixelMetric(QStyle::PM_LayoutRightMargin) + |
|||
style->pixelMetric(QStyle::PM_ScrollBarExtent); |
|||
const int vertical_margin = style->pixelMetric(QStyle::PM_LayoutTopMargin) + |
|||
style->pixelMetric(QStyle::PM_LayoutBottomMargin); |
|||
|
|||
// Set default list size so that it is 6 icons wide and 4.5 tall
|
|||
const int columns = 6; |
|||
const double rows = 4.5; |
|||
const int total_width = icon_full_size * columns + horizontal_margin; |
|||
const int total_height = icon_full_size * rows + vertical_margin; |
|||
avatar_list->setMinimumSize(total_width, total_height); |
|||
} |
|||
|
|||
bool ProfileAvatarDialog::AreImagesLoaded() const { |
|||
return !avatar_image_store.isEmpty(); |
|||
} |
|||
|
|||
QPixmap ProfileAvatarDialog::GetSelectedAvatar() { |
|||
return CreateAvatar(avatar_image_store[avatar_list->currentRow()]); |
|||
} |
|||
@ -0,0 +1,28 @@ |
|||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project |
|||
// SPDX-License-Identifier: GPL-3.0-or-later |
|||
|
|||
#pragma once |
|||
|
|||
#include <QDialog> |
|||
|
|||
class QListWidget; |
|||
|
|||
class ProfileAvatarDialog : public QDialog { |
|||
public: |
|||
explicit ProfileAvatarDialog(QWidget* parent); |
|||
~ProfileAvatarDialog(); |
|||
|
|||
void LoadImages(const QVector<QPixmap>& avatar_images); |
|||
bool AreImagesLoaded() const; |
|||
QPixmap GetSelectedAvatar(); |
|||
|
|||
private: |
|||
void SetBackgroundColor(const QColor& color); |
|||
QPixmap CreateAvatar(const QPixmap& avatar); |
|||
void RefreshAvatars(); |
|||
|
|||
QVector<QPixmap> avatar_image_store; |
|||
QListWidget* avatar_list; |
|||
QColor avatar_bg_color; |
|||
QPushButton* bg_color_button; |
|||
}; |
|||
Write
Preview
Loading…
Cancel
Save
Reference in new issue