duckstation/src/duckstation-qt/gamecheatsettingswidget.cpp
2025-01-13 22:45:59 +10:00

1075 lines
36 KiB
C++

// SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin <stenzek@gmail.com>
// SPDX-License-Identifier: CC-BY-NC-ND-4.0
#include "gamecheatsettingswidget.h"
#include "mainwindow.h"
#include "qthost.h"
#include "qtutils.h"
#include "settingswindow.h"
#include "settingwidgetbinder.h"
#include "core/cheats.h"
#include "common/error.h"
#include "common/log.h"
#include "common/string_util.h"
#include "fmt/format.h"
#include <QtCore/QSignalBlocker>
#include <QtGui/QPainter>
#include <QtGui/QStandardItem>
#include <QtGui/QStandardItemModel>
#include <QtWidgets/QInputDialog>
#include <QtWidgets/QStyledItemDelegate>
LOG_CHANNEL(Cheats);
namespace {
class CheatListOptionDelegate : public QStyledItemDelegate
{
public:
CheatListOptionDelegate(GameCheatSettingsWidget* parent, QTreeView* treeview);
QWidget* createEditor(QWidget* parent, const QStyleOptionViewItem& option, const QModelIndex& index) const override;
void setEditorData(QWidget* editor, const QModelIndex& index) const override;
void setModelData(QWidget* editor, QAbstractItemModel* model, const QModelIndex& index) const override;
void paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const override;
private:
std::string getCodeNameForRow(const QModelIndex& index) const;
const Cheats::CodeInfo* getCodeInfoForRow(const QModelIndex& index) const;
GameCheatSettingsWidget* m_parent;
QTreeView* m_treeview;
};
}; // namespace
CheatListOptionDelegate::CheatListOptionDelegate(GameCheatSettingsWidget* parent, QTreeView* treeview)
: QStyledItemDelegate(parent), m_parent(parent), m_treeview(treeview)
{
}
std::string CheatListOptionDelegate::getCodeNameForRow(const QModelIndex& index) const
{
return index.siblingAtColumn(0).data(Qt::UserRole).toString().toStdString();
}
const Cheats::CodeInfo* CheatListOptionDelegate::getCodeInfoForRow(const QModelIndex& index) const
{
return m_parent->getCodeInfo(getCodeNameForRow(index));
}
QWidget* CheatListOptionDelegate::createEditor(QWidget* parent, const QStyleOptionViewItem& option,
const QModelIndex& index) const
{
// only edit the value, don't want the title becoming editable
if (index.column() != 1)
return nullptr;
const QVariant data = index.data(Qt::UserRole);
if (data.isNull())
return nullptr;
// if it's a uint, it's a range, otherwise string => combobox
if (data.typeId() == QMetaType::QString)
return new QComboBox(parent);
else if (data.typeId() == QMetaType::UInt)
return new QSpinBox(parent);
else
return QStyledItemDelegate::createEditor(parent, option, index);
}
void CheatListOptionDelegate::setEditorData(QWidget* editor, const QModelIndex& index) const
{
if (index.column() != 1)
return;
if (QComboBox* cb = qobject_cast<QComboBox*>(editor))
{
const Cheats::CodeInfo* ci = getCodeInfoForRow(index);
if (ci)
{
int current_index = 0;
const QString selected_name = index.data(Qt::UserRole).toString();
for (const Cheats::CodeOption& opt : ci->options)
{
const QString name = QString::fromStdString(opt.first);
cb->addItem(name, QVariant(static_cast<uint>(opt.second)));
if (name == selected_name)
cb->setCurrentIndex(current_index);
current_index++;
}
}
}
else if (QSpinBox* sb = qobject_cast<QSpinBox*>(editor))
{
const Cheats::CodeInfo* ci = getCodeInfoForRow(index);
if (ci)
{
sb->setMinimum(ci->option_range_start);
sb->setMaximum(ci->option_range_end);
sb->setValue(index.data(Qt::UserRole).toUInt());
}
}
else
{
return QStyledItemDelegate::setEditorData(editor, index);
}
}
void CheatListOptionDelegate::setModelData(QWidget* editor, QAbstractItemModel* model, const QModelIndex& index) const
{
if (index.column() != 1)
return;
if (QComboBox* cb = qobject_cast<QComboBox*>(editor))
{
const QString value = cb->currentText();
const Cheats::CodeInfo* ci = getCodeInfoForRow(index);
if (ci)
{
m_parent->setCodeOption(ci->name, ci->MapOptionNameToValue(value.toStdString()));
model->setData(index, value, Qt::UserRole);
}
}
else if (QSpinBox* sb = qobject_cast<QSpinBox*>(editor))
{
const u32 value = static_cast<u32>(sb->value());
m_parent->setCodeOption(getCodeNameForRow(index), value);
model->setData(index, static_cast<uint>(value), Qt::UserRole);
}
else
{
return QStyledItemDelegate::setModelData(editor, model, index);
}
}
void CheatListOptionDelegate::paint(QPainter* painter, const QStyleOptionViewItem& option,
const QModelIndex& index) const
{
if (index.column() == 0)
{
// skip for editable rows
if (index.data(Qt::UserRole + 1).toBool())
return QStyledItemDelegate::paint(painter, option, index);
// expand the width to full for those without options
QStyleOptionViewItem option_copy(option);
option_copy.rect.setWidth(option_copy.rect.width() + m_treeview->columnWidth(1));
return QStyledItemDelegate::paint(painter, option_copy, index);
}
else
{
if (!(index.flags() & Qt::ItemIsEditable))
{
// disable painting this column, so we can expand it (see above)
return;
}
// just draw the number or label as a string
const QVariant data = index.data(Qt::UserRole);
painter->drawText(option.rect, 0, data.toString());
}
}
GameCheatSettingsWidget::GameCheatSettingsWidget(SettingsWindow* dialog, QWidget* parent) : m_dialog(dialog)
{
SettingsInterface* sif = m_dialog->getSettingsInterface();
const bool sorting_enabled = sif->GetBoolValue("Cheats", "SortList", false);
m_ui.setupUi(this);
m_codes_model = new QStandardItemModel(this);
m_sort_model = new QSortFilterProxyModel(m_codes_model);
m_sort_model->setSourceModel(m_codes_model);
m_sort_model->setFilterCaseSensitivity(Qt::CaseInsensitive);
m_sort_model->setRecursiveFilteringEnabled(true);
m_sort_model->setAutoAcceptChildRows(true);
m_sort_model->sort(sorting_enabled ? 0 : -1, Qt::AscendingOrder);
m_ui.cheatList->setModel(m_sort_model);
m_ui.cheatList->setItemDelegate(new CheatListOptionDelegate(this, m_ui.cheatList));
reloadList();
// We don't use the binder here, because they're binary - either enabled, or not in the file.
m_ui.enableCheats->setChecked(sif->GetBoolValue("Cheats", "EnableCheats", false));
m_ui.loadDatabaseCheats->setChecked(sif->GetBoolValue("Cheats", "LoadCheatsFromDatabase", true));
m_ui.sortCheats->setChecked(sorting_enabled);
connect(m_ui.enableCheats, &QCheckBox::checkStateChanged, this, &GameCheatSettingsWidget::onEnableCheatsChanged);
connect(m_ui.sortCheats, &QPushButton::toggled, this, &GameCheatSettingsWidget::onSortCheatsToggled);
connect(m_ui.search, &QLineEdit::textChanged, this, &GameCheatSettingsWidget::onSearchFilterChanged);
connect(m_ui.loadDatabaseCheats, &QCheckBox::checkStateChanged, this,
&GameCheatSettingsWidget::onLoadDatabaseCheatsChanged);
connect(m_ui.cheatList, &QTreeView::doubleClicked, this, &GameCheatSettingsWidget::onCheatListItemDoubleClicked);
connect(m_ui.cheatList, &QTreeView::customContextMenuRequested, this,
&GameCheatSettingsWidget::onCheatListContextMenuRequested);
connect(m_codes_model, &QStandardItemModel::itemChanged, this, &GameCheatSettingsWidget::onCheatListItemChanged);
connect(m_ui.add, &QToolButton::clicked, this, &GameCheatSettingsWidget::newCode);
connect(m_ui.remove, &QToolButton::clicked, this, &GameCheatSettingsWidget::onRemoveCodeClicked);
connect(m_ui.disableAll, &QToolButton::clicked, this, &GameCheatSettingsWidget::disableAllCheats);
connect(m_ui.reloadCheats, &QToolButton::clicked, this, &GameCheatSettingsWidget::onReloadClicked);
connect(m_ui.importCheats, &QPushButton::clicked, this, &GameCheatSettingsWidget::onImportClicked);
connect(m_ui.exportCheats, &QPushButton::clicked, this, &GameCheatSettingsWidget::onExportClicked);
connect(m_ui.clearCheats, &QPushButton::clicked, this, &GameCheatSettingsWidget::onClearClicked);
}
GameCheatSettingsWidget::~GameCheatSettingsWidget() = default;
const Cheats::CodeInfo* GameCheatSettingsWidget::getCodeInfo(const std::string_view name) const
{
return Cheats::FindCodeInInfoList(m_codes, name);
}
void GameCheatSettingsWidget::setCodeOption(const std::string_view name, u32 value)
{
const Cheats::CodeInfo* info = getCodeInfo(name);
if (!info)
return;
m_dialog->getSettingsInterface()->SetUIntValue("Cheats", info->name.c_str(), value);
m_dialog->saveAndReloadGameSettings();
}
std::string GameCheatSettingsWidget::getPathForSavingCheats() const
{
// Check for the path without the hash first. If we have one of those, keep using it.
std::string path = Cheats::GetChtFilename(m_dialog->getGameSerial(), std::nullopt, true);
if (!FileSystem::FileExists(path.c_str()))
path = Cheats::GetChtFilename(m_dialog->getGameSerial(), m_dialog->getGameHash(), true);
return path;
}
QStringList GameCheatSettingsWidget::getGroupNames() const
{
std::vector<std::string_view> unique_prefixes = Cheats::GetCodeListUniquePrefixes(m_codes, false);
QStringList ret;
if (!unique_prefixes.empty())
{
ret.reserve(unique_prefixes.size());
for (const std::string_view& prefix : unique_prefixes)
ret.push_back(QtUtils::StringViewToQString(prefix));
}
return ret;
}
bool GameCheatSettingsWidget::hasCodeWithName(const std::string_view name) const
{
return (Cheats::FindCodeInInfoList(m_codes, name) != nullptr);
}
void GameCheatSettingsWidget::onEnableCheatsChanged(Qt::CheckState state)
{
if (state == Qt::Checked)
m_dialog->getSettingsInterface()->SetBoolValue("Cheats", "EnableCheats", true);
else
m_dialog->getSettingsInterface()->DeleteValue("Cheats", "EnableCheats");
m_dialog->saveAndReloadGameSettings();
}
void GameCheatSettingsWidget::onSortCheatsToggled(bool checked)
{
m_sort_model->sort(checked ? 0 : -1, Qt::AscendingOrder);
if (checked)
m_dialog->getSettingsInterface()->SetBoolValue("Cheats", "SortList", true);
else
m_dialog->getSettingsInterface()->DeleteValue("Cheats", "SortList");
m_dialog->saveAndReloadGameSettings();
}
void GameCheatSettingsWidget::onSearchFilterChanged(const QString& text)
{
m_sort_model->setFilterFixedString(text);
// if we're clearing search, re-expand everything, since sorting collapses them
if (text.isEmpty())
expandAllItems();
}
void GameCheatSettingsWidget::onLoadDatabaseCheatsChanged(Qt::CheckState state)
{
// Default is enabled.
if (state == Qt::Checked)
m_dialog->getSettingsInterface()->DeleteValue("Cheats", "LoadCheatsFromDatabase");
else
m_dialog->getSettingsInterface()->SetBoolValue("Cheats", "LoadCheatsFromDatabase", false);
m_dialog->saveAndReloadGameSettings();
reloadList();
}
void GameCheatSettingsWidget::onCheatListItemDoubleClicked(const QModelIndex& index)
{
const QModelIndex col0 = m_sort_model->mapToSource(index.siblingAtColumn(0));
if (!col0.isValid())
return;
const QStandardItem* item = m_codes_model->itemFromIndex(col0);
if (!item)
return;
const QVariant item_data = item->data(Qt::UserRole);
if (!item_data.isValid())
return;
editCode(item_data.toString().toStdString());
}
void GameCheatSettingsWidget::onCheatListItemChanged(QStandardItem* item)
{
const QVariant item_data = item->data(Qt::UserRole);
if (!item_data.isValid())
return;
std::string cheat_name = item_data.toString().toStdString();
const bool current_enabled =
(std::find(m_enabled_codes.begin(), m_enabled_codes.end(), cheat_name) != m_enabled_codes.end());
const bool current_checked = (item->checkState() == Qt::Checked);
if (current_enabled == current_checked)
return;
if (current_checked)
checkForMasterDisable();
setCheatEnabled(std::move(cheat_name), current_checked, true);
}
void GameCheatSettingsWidget::onCheatListContextMenuRequested(const QPoint& pos)
{
Cheats::CodeInfo* selected = getSelectedCode();
const std::string selected_code = selected ? selected->name : std::string();
QMenu context_menu(m_ui.cheatList);
QAction* add = context_menu.addAction(QIcon::fromTheme("add-line"), tr("Add Cheat..."));
connect(add, &QAction::triggered, this, &GameCheatSettingsWidget::newCode);
QAction* edit = context_menu.addAction(QIcon::fromTheme("mag-line"), tr("Edit Cheat..."));
edit->setEnabled(selected != nullptr);
connect(edit, &QAction::triggered, this, [this, &selected_code]() { editCode(selected_code); });
QAction* remove = context_menu.addAction(QIcon::fromTheme("minus-line"), tr("Remove Cheat"));
remove->setEnabled(selected != nullptr);
connect(remove, &QAction::triggered, this, [this, &selected_code]() { removeCode(selected_code, true); });
context_menu.addSeparator();
QAction* disable_all = context_menu.addAction(QIcon::fromTheme("chat-off-line"), tr("Disable All Cheats"));
connect(disable_all, &QAction::triggered, this, &GameCheatSettingsWidget::disableAllCheats);
QAction* reload = context_menu.addAction(QIcon::fromTheme("refresh-line"), tr("Reload Cheats"));
connect(reload, &QAction::triggered, this, &GameCheatSettingsWidget::onReloadClicked);
context_menu.exec(m_ui.cheatList->mapToGlobal(pos));
}
void GameCheatSettingsWidget::onRemoveCodeClicked()
{
Cheats::CodeInfo* selected = getSelectedCode();
if (!selected)
return;
removeCode(selected->name, true);
}
void GameCheatSettingsWidget::onReloadClicked()
{
reloadList();
g_emu_thread->reloadCheats(true, false, true, true);
}
bool GameCheatSettingsWidget::shouldLoadFromDatabase() const
{
return m_dialog->getSettingsInterface()->GetBoolValue("Cheats", "LoadCheatsFromDatabase", true);
}
void GameCheatSettingsWidget::checkForMasterDisable()
{
if (m_dialog->getSettingsInterface()->GetBoolValue("Cheats", "EnableCheats", false) || m_master_enable_ignored)
return;
QMessageBox mbox(this);
mbox.setIcon(QMessageBox::Warning);
mbox.setWindowTitle(tr("Confirm Cheat Enable"));
mbox.setWindowIcon(QtHost::GetAppIcon());
mbox.setTextFormat(Qt::RichText);
mbox.setText(tr("<h3>Cheats are not currently enabled for this game.</h3><p>Enabling this cheat will not have any "
"effect until cheats are enabled for this game. Do you want to do this now?"));
mbox.addButton(QMessageBox::Yes);
mbox.addButton(QMessageBox::No);
QCheckBox* cb = new QCheckBox(&mbox);
cb->setText(tr("Do not show again"));
mbox.setCheckBox(cb);
const int res = mbox.exec();
if (res == QMessageBox::No)
m_master_enable_ignored = cb->isChecked();
else
m_ui.enableCheats->setChecked(true);
}
Cheats::CodeInfo* GameCheatSettingsWidget::getSelectedCode()
{
const QList<QModelIndex> selected = m_ui.cheatList->selectionModel()->selectedRows();
if (selected.size() != 1)
return nullptr;
const QStandardItem* item = m_codes_model->itemFromIndex(m_sort_model->mapToSource(selected[0]));
if (!item)
return nullptr;
const QVariant item_data = item->data(Qt::UserRole);
if (!item_data.isValid())
return nullptr;
return Cheats::FindCodeInInfoList(m_codes, item_data.toString().toStdString());
}
void GameCheatSettingsWidget::disableAllCheats()
{
setStateForAll(false);
}
void GameCheatSettingsWidget::resizeEvent(QResizeEvent* event)
{
QWidget::resizeEvent(event);
QtUtils::ResizeColumnsForTreeView(m_ui.cheatList, {-1, 150});
}
void GameCheatSettingsWidget::setCheatEnabled(std::string name, bool enabled, bool save_and_reload_settings)
{
SettingsInterface* si = m_dialog->getSettingsInterface();
const auto it = std::find(m_enabled_codes.begin(), m_enabled_codes.end(), name);
if (enabled)
{
si->AddToStringList(Cheats::CHEATS_CONFIG_SECTION, Cheats::PATCH_ENABLE_CONFIG_KEY, name.c_str());
if (it == m_enabled_codes.end())
m_enabled_codes.push_back(std::move(name));
}
else
{
si->RemoveFromStringList(Cheats::CHEATS_CONFIG_SECTION, Cheats::PATCH_ENABLE_CONFIG_KEY, name.c_str());
if (it != m_enabled_codes.end())
m_enabled_codes.erase(it);
}
if (save_and_reload_settings)
m_dialog->saveAndReloadGameSettings();
}
void GameCheatSettingsWidget::setStateForAll(bool enabled)
{
setStateRecursively(m_codes_model->invisibleRootItem(), enabled);
m_dialog->saveAndReloadGameSettings();
}
void GameCheatSettingsWidget::setStateRecursively(QStandardItem* parent, bool enabled)
{
const int count = parent->rowCount();
for (int i = 0; i < count; i++)
{
QStandardItem* child = parent->child(i);
if (child->hasChildren())
{
setStateRecursively(child, enabled);
continue;
}
// found a code to toggle
const QVariant item_data = child->data(Qt::UserRole);
if (item_data.isValid())
{
if ((child->checkState() == Qt::Checked) != enabled)
{
// set state first, so the signal doesn't change it
// can't use a signal blocker here, because otherwise the view doesn't update
setCheatEnabled(item_data.toString().toStdString(), enabled, false);
child->setCheckState(enabled ? Qt::Checked : Qt::Unchecked);
}
}
}
}
void GameCheatSettingsWidget::reloadList()
{
// Show all hashes, since the ini is shared.
m_codes = Cheats::GetCodeInfoList(m_dialog->getGameSerial(), std::nullopt, true, shouldLoadFromDatabase(), false);
m_enabled_codes =
m_dialog->getSettingsInterface()->GetStringList(Cheats::CHEATS_CONFIG_SECTION, Cheats::PATCH_ENABLE_CONFIG_KEY);
m_parent_map.clear();
m_codes_model->clear();
m_codes_model->invisibleRootItem()->setColumnCount(2);
for (const Cheats::CodeInfo& ci : m_codes)
{
const bool enabled = (std::find(m_enabled_codes.begin(), m_enabled_codes.end(), ci.name) != m_enabled_codes.end());
const std::string_view parent_part = ci.GetNameParentPart();
QStandardItem* parent = getTreeWidgetParent(parent_part);
populateTreeWidgetItem(parent, ci, enabled);
}
// Hide root indicator when there's no groups, frees up some whitespace.
m_ui.cheatList->setRootIsDecorated(!m_parent_map.empty());
// Expand all items.
expandAllItems();
}
void GameCheatSettingsWidget::expandAllItems()
{
for (const auto& it : m_parent_map)
m_ui.cheatList->setExpanded(m_sort_model->mapFromSource(it.second->index()), true);
}
void GameCheatSettingsWidget::onImportClicked()
{
QMenu menu(this);
connect(menu.addAction(tr("From File...")), &QAction::triggered, this,
&GameCheatSettingsWidget::onImportFromFileTriggered);
connect(menu.addAction(tr("From Text...")), &QAction::triggered, this,
&GameCheatSettingsWidget::onImportFromTextTriggered);
menu.exec(QCursor::pos());
}
void GameCheatSettingsWidget::onImportFromFileTriggered()
{
const QString filter(tr("PCSXR/Libretro Cheat Files (*.cht *.txt);;All Files (*.*)"));
const QString filename =
QDir::toNativeSeparators(QFileDialog::getOpenFileName(this, tr("Import Cheats"), QString(), filter));
if (filename.isEmpty())
return;
Error error;
const std::optional<std::string> file_contents = FileSystem::ReadFileToString(filename.toStdString().c_str(), &error);
if (!file_contents.has_value())
{
QMessageBox::critical(this, tr("Error"),
tr("Failed to read file:\n%1").arg(QString::fromStdString(error.GetDescription())));
return;
}
importCodes(file_contents.value());
}
void GameCheatSettingsWidget::onImportFromTextTriggered()
{
const QString text = QInputDialog::getMultiLineText(this, tr("Import Cheats"), tr("Cheat File Text:"));
if (text.isEmpty())
return;
importCodes(text.toStdString());
}
void GameCheatSettingsWidget::importCodes(const std::string& file_contents)
{
Error error;
Cheats::CodeInfoList new_codes;
if (!Cheats::ImportCodesFromString(&new_codes, file_contents, Cheats::FileFormat::Unknown, true, &error))
{
QMessageBox::critical(this, tr("Error"),
tr("Failed to parse file:\n%1").arg(QString::fromStdString(error.GetDescription())));
return;
}
if (!Cheats::SaveCodesToFile(getPathForSavingCheats().c_str(), new_codes, &error))
{
QMessageBox::critical(this, tr("Error"),
tr("Failed to save file:\n%1").arg(QString::fromStdString(error.GetDescription())));
}
reloadList();
g_emu_thread->reloadCheats(true, false, false, true);
}
void GameCheatSettingsWidget::newCode()
{
Cheats::CodeInfo new_code;
CheatCodeEditorDialog dlg(this, &new_code, getGroupNames());
if (!dlg.exec())
{
// cancelled
return;
}
// no need to reload cheats yet, it's not active. just refresh the list
reloadList();
g_emu_thread->reloadCheats(true, false, false, true);
}
void GameCheatSettingsWidget::editCode(const std::string_view code_name)
{
Cheats::CodeInfo* code = Cheats::FindCodeInInfoList(m_codes, code_name);
if (!code)
return;
CheatCodeEditorDialog dlg(this, code, getGroupNames());
if (!dlg.exec())
{
// no changes
return;
}
reloadList();
g_emu_thread->reloadCheats(true, true, false, true);
}
void GameCheatSettingsWidget::removeCode(const std::string_view code_name, bool confirm)
{
Cheats::CodeInfo* code = Cheats::FindCodeInInfoList(m_codes, code_name);
if (!code)
return;
if (code->from_database)
{
QMessageBox::critical(this, tr("Error"),
tr("This code is from the built-in cheat database, and cannot be removed. To hide this code, "
"uncheck the \"Load Database Cheats\" option."));
return;
}
if (QMessageBox::question(this, tr("Confirm Removal"),
tr("You are removing the code named '%1'. You cannot undo this action, are you sure you "
"wish to delete this code?")
.arg(QtUtils::StringViewToQString(code_name))) != QMessageBox::Yes)
{
return;
}
Error error;
if (!Cheats::UpdateCodeInFile(getPathForSavingCheats().c_str(), code->name, nullptr, &error))
{
QMessageBox::critical(this, tr("Error"),
tr("Failed to save file:\n%1").arg(QString::fromStdString(error.GetDescription())));
return;
}
reloadList();
g_emu_thread->reloadCheats(true, true, false, true);
}
void GameCheatSettingsWidget::onExportClicked()
{
const QString filter(tr("PCSXR Cheat Files (*.cht);;All Files (*.*)"));
const QString filename =
QDir::toNativeSeparators(QFileDialog::getSaveFileName(this, tr("Export Cheats"), QString(), filter));
if (filename.isEmpty())
return;
Error error;
if (!Cheats::ExportCodesToFile(filename.toStdString(), m_codes, &error))
{
QMessageBox::critical(this, tr("Error"),
tr("Failed to save cheat file:\n%1").arg(QString::fromStdString(error.GetDescription())));
}
}
void GameCheatSettingsWidget::onClearClicked()
{
if (QMessageBox::question(this, tr("Confirm Removal"),
tr("You are removing all cheats manually added for this game. This action cannot be "
"reversed.\n\nAny database cheats will still be loaded and present unless you uncheck "
"the \"Load Database Cheats\" option.\n\nAre you sure you want to continue?")) !=
QMessageBox::Yes)
{
return;
}
disableAllCheats();
Cheats::RemoveAllCodes(m_dialog->getGameSerial(), m_dialog->getGameTitle(), m_dialog->getGameHash());
reloadList();
}
QStandardItem* GameCheatSettingsWidget::getTreeWidgetParent(const std::string_view parent)
{
if (parent.empty())
return m_codes_model->invisibleRootItem();
auto it = m_parent_map.find(parent);
if (it != m_parent_map.end())
return it->second;
std::string_view this_part = parent;
QStandardItem* parent_to_this = nullptr;
const std::string_view::size_type pos = parent.rfind('\\');
if (pos != std::string::npos && pos != (parent.size() - 1))
{
// go up the chain until we find the real parent, then back down
parent_to_this = getTreeWidgetParent(parent.substr(0, pos));
this_part = parent.substr(pos + 1);
}
else
{
parent_to_this = m_codes_model->invisibleRootItem();
}
QStandardItem* item = new QStandardItem();
item->setText(QString::fromUtf8(this_part.data(), this_part.length()));
item->setFlags(Qt::ItemIsEnabled | Qt::ItemIsSelectable);
parent_to_this->appendRow(item);
m_parent_map.emplace(parent, item);
return item;
}
void GameCheatSettingsWidget::populateTreeWidgetItem(QStandardItem* parent, const Cheats::CodeInfo& pi, bool enabled)
{
const std::string_view name_part = pi.GetNamePart();
QStandardItem* label = new QStandardItem();
label->setFlags(Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsUserCheckable | Qt::ItemNeverHasChildren);
label->setCheckState(enabled ? Qt::Checked : Qt::Unchecked);
label->setData(QString::fromStdString(pi.name), Qt::UserRole);
// Why?
if (!pi.description.empty())
label->setToolTip(QString::fromStdString(pi.description));
if (!name_part.empty())
label->setText(QtUtils::StringViewToQString(name_part));
const int index = parent->rowCount();
parent->appendRow(label);
if (pi.HasOptionChoices() || pi.HasOptionRange())
{
QStandardItem* value_col = new QStandardItem();
value_col->setFlags(Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsEditable);
if (pi.HasOptionChoices())
{
// need to resolve the value back to a name
const std::string_view option_name =
pi.MapOptionValueToName(m_dialog->getSettingsInterface()->GetTinyStringValue("Cheats", pi.name.c_str()));
value_col->setData(QtUtils::StringViewToQString(option_name), Qt::UserRole);
}
else if (pi.HasOptionRange())
{
const u32 value =
m_dialog->getSettingsInterface()->GetUIntValue("Cheats", pi.name.c_str(), pi.option_range_start);
value_col->setData(static_cast<uint>(value), Qt::UserRole);
}
parent->setChild(index, 1, value_col);
// Why are we doing this on the label item? Qt seems to return these oddball QStandardItem values
// for columns that don't exist that have all flags set, so we can't use it in the drawing delegate
// to determine whether the label should span the entire width or not.
label->setData(true, Qt::UserRole + 1);
}
}
CheatCodeEditorDialog::CheatCodeEditorDialog(GameCheatSettingsWidget* parent, Cheats::CodeInfo* code,
const QStringList& group_names)
: QDialog(parent), m_parent(parent), m_code(code)
{
m_ui.setupUi(this);
setupAdditionalUi(group_names);
fillUi();
connect(m_ui.group, &QComboBox::currentIndexChanged, this, &CheatCodeEditorDialog::onGroupSelectedIndexChanged);
connect(m_ui.optionsType, &QComboBox::currentIndexChanged, this, &CheatCodeEditorDialog::onOptionTypeChanged);
connect(m_ui.rangeMin, &QSpinBox::valueChanged, this, &CheatCodeEditorDialog::onRangeMinChanged);
connect(m_ui.rangeMax, &QSpinBox::valueChanged, this, &CheatCodeEditorDialog::onRangeMaxChanged);
connect(m_ui.editChoice, &QPushButton::clicked, this, &CheatCodeEditorDialog::onEditChoiceClicked);
connect(m_ui.buttonBox, &QDialogButtonBox::accepted, this, &CheatCodeEditorDialog::saveClicked);
connect(m_ui.buttonBox, &QDialogButtonBox::rejected, this, &CheatCodeEditorDialog::cancelClicked);
}
CheatCodeEditorDialog::~CheatCodeEditorDialog() = default;
void CheatCodeEditorDialog::onGroupSelectedIndexChanged(int index)
{
if (index != (m_ui.group->count() - 1))
return;
// new item...
const QString text = QInputDialog::getText(
this, tr("Enter Group Name"), tr("Enter name for the code group. Using backslashes (\\) will create sub-trees."));
// don't want this re-triggering
QSignalBlocker sb(m_ui.group);
if (text.isEmpty())
{
// cancelled...
m_ui.group->setCurrentIndex(0);
return;
}
const int existing_index = m_ui.group->findText(text);
if (existing_index >= 0)
{
m_ui.group->setCurrentIndex(existing_index);
return;
}
m_ui.group->insertItem(index, text);
m_ui.group->setCurrentIndex(index);
}
void CheatCodeEditorDialog::saveClicked()
{
std::string new_name = m_ui.name->text().toStdString();
if (new_name.empty())
{
QMessageBox::critical(this, tr("Error"), tr("Name cannot be empty."));
return;
}
std::string new_body = m_ui.instructions->toPlainText().toStdString();
if (new_body.empty())
{
QMessageBox::critical(this, tr("Error"), tr("Instructions cannot be empty."));
return;
}
// name actually includes the prefix
if (const int index = m_ui.group->currentIndex(); index != 0)
{
const std::string prefix = m_ui.group->currentText().toStdString();
if (!prefix.empty())
new_name = fmt::format("{}\\{}", prefix, new_name);
}
// if the name has changed, then we need to make sure it hasn't already been used
if (new_name != m_code->name && m_parent->hasCodeWithName(new_name))
{
QMessageBox::critical(this, tr("Error"),
tr("A code with the name '%1' already exists.").arg(QString::fromStdString(new_name)));
return;
}
std::string old_name = std::move(m_code->name);
// cheats coming from the database need to be copied into the user's file
if (m_code->from_database)
{
m_code->from_database = false;
old_name.clear();
}
m_code->name = std::move(new_name);
m_code->description = m_ui.description->toPlainText().replace('\n', ' ').toStdString();
m_code->type = static_cast<Cheats::CodeType>(m_ui.type->currentIndex());
m_code->activation = static_cast<Cheats::CodeActivation>(m_ui.activation->currentIndex());
m_code->body = std::move(new_body);
m_code->option_range_start = 0;
m_code->option_range_end = 0;
m_code->options = {};
if (m_ui.optionsType->currentIndex() == 1)
{
// choices
m_code->options = std::move(m_new_options);
}
else if (m_ui.optionsType->currentIndex() == 2)
{
// range
m_code->option_range_start = static_cast<u16>(m_ui.rangeMin->value());
m_code->option_range_end = static_cast<u16>(m_ui.rangeMax->value());
}
std::string path = m_parent->getPathForSavingCheats();
Error error;
if (!Cheats::UpdateCodeInFile(path.c_str(), old_name, m_code, &error))
{
QMessageBox::critical(this, tr("Error"),
tr("Failed to save cheat code:\n%1").arg(QString::fromStdString(error.GetDescription())));
}
done(1);
}
void CheatCodeEditorDialog::cancelClicked()
{
done(0);
}
void CheatCodeEditorDialog::onOptionTypeChanged(int index)
{
m_ui.editChoice->setVisible(index == 1);
m_ui.rangeMin->setVisible(index == 2);
m_ui.rangeMax->setVisible(index == 2);
}
void CheatCodeEditorDialog::onRangeMinChanged(int value)
{
m_ui.rangeMax->setValue(std::max(m_ui.rangeMax->value(), value));
}
void CheatCodeEditorDialog::onRangeMaxChanged(int value)
{
m_ui.rangeMin->setValue(std::min(m_ui.rangeMin->value(), value));
}
void CheatCodeEditorDialog::onEditChoiceClicked()
{
GameCheatCodeChoiceEditorDialog dlg(this, m_new_options);
if (dlg.exec())
m_new_options = dlg.getNewOptions();
}
void CheatCodeEditorDialog::setupAdditionalUi(const QStringList& group_names)
{
for (u32 i = 0; i < static_cast<u32>(Cheats::CodeType::Count); i++)
m_ui.type->addItem(Cheats::GetTypeDisplayName(static_cast<Cheats::CodeType>(i)));
for (u32 i = 0; i < static_cast<u32>(Cheats::CodeActivation::Count); i++)
m_ui.activation->addItem(Cheats::GetActivationDisplayName(static_cast<Cheats::CodeActivation>(i)));
m_ui.group->addItem(tr("Ungrouped"));
if (!group_names.isEmpty())
m_ui.group->addItems(group_names);
m_ui.group->addItem(tr("New..."));
}
void CheatCodeEditorDialog::fillUi()
{
m_ui.name->setText(QtUtils::StringViewToQString(m_code->GetNamePart()));
m_ui.description->setPlainText(QString::fromStdString(m_code->description));
const std::string_view group = m_code->GetNameParentPart();
if (group.empty())
{
// ungrouped is always first
m_ui.group->setCurrentIndex(0);
}
else
{
const QString group_qstr(QtUtils::StringViewToQString(group));
int index = m_ui.group->findText(group_qstr);
if (index < 0)
{
// shouldn't happen...
index = m_ui.group->count() - 1;
m_ui.group->insertItem(index, group_qstr);
}
m_ui.group->setCurrentIndex(index);
}
m_ui.type->setCurrentIndex(static_cast<int>(m_code->type));
m_ui.activation->setCurrentIndex(static_cast<int>(m_code->activation));
m_ui.instructions->setPlainText(QString::fromStdString(m_code->body));
m_ui.rangeMin->setValue(static_cast<int>(m_code->option_range_start));
m_ui.rangeMax->setValue(static_cast<int>(m_code->option_range_end));
m_new_options = m_code->options;
m_ui.optionsType->setCurrentIndex(m_code->HasOptionRange() ? 2 : (m_code->HasOptionChoices() ? 1 : 0));
onOptionTypeChanged(m_ui.optionsType->currentIndex());
}
GameCheatCodeChoiceEditorDialog::GameCheatCodeChoiceEditorDialog(QWidget* parent, const Cheats::CodeOptionList& options)
: QDialog(parent)
{
m_ui.setupUi(this);
connect(m_ui.add, &QToolButton::clicked, this, &GameCheatCodeChoiceEditorDialog::onAddClicked);
connect(m_ui.remove, &QToolButton::clicked, this, &GameCheatCodeChoiceEditorDialog::onRemoveClicked);
connect(m_ui.buttonBox, &QDialogButtonBox::accepted, this, &GameCheatCodeChoiceEditorDialog::onSaveClicked);
connect(m_ui.buttonBox, &QDialogButtonBox::rejected, this, &CheatCodeEditorDialog::reject);
m_ui.optionList->setRootIsDecorated(false);
for (const Cheats::CodeOption& opt : options)
{
QTreeWidgetItem* item = new QTreeWidgetItem();
item->setFlags(item->flags() | Qt::ItemIsEditable);
item->setText(0, QString::fromStdString(opt.first));
item->setText(1, QString::number(opt.second));
m_ui.optionList->addTopLevelItem(item);
}
}
GameCheatCodeChoiceEditorDialog::~GameCheatCodeChoiceEditorDialog() = default;
void GameCheatCodeChoiceEditorDialog::resizeEvent(QResizeEvent* event)
{
QDialog::resizeEvent(event);
QtUtils::ResizeColumnsForTreeView(m_ui.optionList, {-1, 150});
}
void GameCheatCodeChoiceEditorDialog::onAddClicked()
{
QTreeWidgetItem* item = new QTreeWidgetItem();
item->setFlags(item->flags() | Qt::ItemIsEditable);
item->setText(0, QStringLiteral("Option %1").arg(m_ui.optionList->topLevelItemCount()));
item->setText(1, QStringLiteral("0"));
m_ui.optionList->addTopLevelItem(item);
}
void GameCheatCodeChoiceEditorDialog::onRemoveClicked()
{
const QList<QTreeWidgetItem*> items = m_ui.optionList->selectedItems();
for (QTreeWidgetItem* item : items)
{
const int index = m_ui.optionList->indexOfTopLevelItem(item);
if (index >= 0)
delete m_ui.optionList->takeTopLevelItem(index);
}
}
void GameCheatCodeChoiceEditorDialog::onSaveClicked()
{
// validate the data
const int count = m_ui.optionList->topLevelItemCount();
if (count == 0)
{
QMessageBox::critical(this, tr("Error"), tr("At least one option must be defined."));
return;
}
for (int i = 0; i < count; i++)
{
const QTreeWidgetItem* it = m_ui.optionList->topLevelItem(i);
const QString this_name = it->text(0);
for (int j = 0; j < count; j++)
{
if (i == j)
continue;
if (m_ui.optionList->topLevelItem(j)->text(0) == this_name)
{
QMessageBox::critical(this, tr("Error"), tr("The option '%1' is defined twice.").arg(this_name));
return;
}
}
// should be a parseable number
const QString this_value = it->text(1);
if (bool ok; this_value.toUInt(&ok), !ok)
{
QMessageBox::critical(this, tr("Error"),
tr("The option '%1' does not have a valid value. It must be a number.").arg(this_name));
return;
}
}
accept();
}
Cheats::CodeOptionList GameCheatCodeChoiceEditorDialog::getNewOptions() const
{
Cheats::CodeOptionList ret;
const int count = m_ui.optionList->topLevelItemCount();
ret.reserve(static_cast<size_t>(count));
for (int i = 0; i < count; i++)
{
const QTreeWidgetItem* it = m_ui.optionList->topLevelItem(i);
ret.emplace_back(it->text(0).toStdString(), it->text(1).toUInt());
}
return ret;
}