// SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin // 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 #include #include #include #include #include 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(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(opt.second))); if (name == selected_name) cb->setCurrentIndex(current_index); current_index++; } } } else if (QSpinBox* sb = qobject_cast(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(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(editor)) { const u32 value = static_cast(sb->value()); m_parent->setCodeOption(getCodeNameForRow(index), value); model->setData(index, static_cast(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 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("

Cheats are not currently enabled for this game.

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 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 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(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(m_ui.type->currentIndex()); m_code->activation = static_cast(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(m_ui.rangeMin->value()); m_code->option_range_end = static_cast(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(Cheats::CodeType::Count); i++) m_ui.type->addItem(Cheats::GetTypeDisplayName(static_cast(i))); for (u32 i = 0; i < static_cast(Cheats::CodeActivation::Count); i++) m_ui.activation->addItem(Cheats::GetActivationDisplayName(static_cast(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(m_code->type)); m_ui.activation->setCurrentIndex(static_cast(m_code->activation)); m_ui.instructions->setPlainText(QString::fromStdString(m_code->body)); m_ui.rangeMin->setValue(static_cast(m_code->option_range_start)); m_ui.rangeMax->setValue(static_cast(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 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(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; }