diff --git a/src/duckstation-qt/CMakeLists.txt b/src/duckstation-qt/CMakeLists.txt index c587687b6..c5e886363 100644 --- a/src/duckstation-qt/CMakeLists.txt +++ b/src/duckstation-qt/CMakeLists.txt @@ -32,9 +32,9 @@ set(SRCS cheatcodeeditordialog.cpp cheatcodeeditordialog.h cheatcodeeditordialog.ui - cheatmanagerdialog.cpp - cheatmanagerdialog.h - cheatmanagerdialog.ui + cheatmanagerwindow.cpp + cheatmanagerwindow.h + cheatmanagerwindow.ui collapsiblewidget.cpp collapsiblewidget.h colorpickerbutton.cpp @@ -121,6 +121,9 @@ set(SRCS memorycardeditorwindow.ui memorycardsettingswidget.cpp memorycardsettingswidget.h + memoryscannerwindow.cpp + memoryscannerwindow.h + memoryscannerwindow.ui memoryviewwidget.cpp memoryviewwidget.h postprocessingsettingswidget.cpp diff --git a/src/duckstation-qt/cheatmanagerdialog.cpp b/src/duckstation-qt/cheatmanagerdialog.cpp deleted file mode 100644 index e2167636d..000000000 --- a/src/duckstation-qt/cheatmanagerdialog.cpp +++ /dev/null @@ -1,1057 +0,0 @@ -// SPDX-FileCopyrightText: 2019-2022 Connor McLaughlin -// SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0) - -#include "cheatmanagerdialog.h" -#include "cheatcodeeditordialog.h" -#include "common/assert.h" -#include "common/string_util.h" -#include "core/bus.h" -#include "core/cpu_core.h" -#include "core/host.h" -#include "core/system.h" -#include "qthost.h" -#include "qtutils.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include - -static constexpr std::array s_size_strings = { - {TRANSLATE_NOOP("CheatManagerDialog", "Byte"), TRANSLATE_NOOP("CheatManagerDialog", "Halfword"), - TRANSLATE_NOOP("CheatManagerDialog", "Word"), TRANSLATE_NOOP("CheatManagerDialog", "Signed Byte"), - TRANSLATE_NOOP("CheatManagerDialog", "Signed Halfword"), TRANSLATE_NOOP("CheatManagerDialog", "Signed Word")}}; - -static QString formatHexValue(u32 value, u8 size) -{ - return QStringLiteral("0x%1").arg(static_cast(value), size, 16, QChar('0')); -} - -static QString formatHexAndDecValue(u32 value, u8 size, bool is_signed) -{ - - if (is_signed) - { - u32 value_raw = value; - if (size == 2) - value_raw &= 0xFF; - else if (size == 4) - value_raw &= 0xFFFF; - return QStringLiteral("0x%1 (%2)") - .arg(static_cast(value_raw), size, 16, QChar('0')) - .arg(static_cast(value)); - } - else - return QStringLiteral("0x%1 (%2)").arg(static_cast(value), size, 16, QChar('0')).arg(static_cast(value)); -} - -static QString formatCheatCode(u32 address, u32 value, const MemoryAccessSize size) -{ - - if (size == MemoryAccessSize::Byte && address <= 0x00200000) - return QStringLiteral("CHEAT CODE: %1 %2") - .arg(static_cast(address) + 0x30000000, 8, 16, QChar('0')) - .toUpper() - .arg(static_cast(value), 4, 16, QChar('0')) - .toUpper(); - else if (size == MemoryAccessSize::HalfWord && address <= 0x001FFFFE) - return QStringLiteral("CHEAT CODE: %1 %2") - .arg(static_cast(address) + 0x80000000, 8, 16, QChar('0')) - .toUpper() - .arg(static_cast(value), 4, 16, QChar('0')) - .toUpper(); - else if (size == MemoryAccessSize::Word && address <= 0x001FFFFC) - return QStringLiteral("CHEAT CODE: %1 %2") - .arg(static_cast(address) + 0x90000000, 8, 16, QChar('0')) - .toUpper() - .arg(static_cast(value), 8, 16, QChar('0')) - .toUpper(); - else - return QStringLiteral("OUTSIDE RAM RANGE. POKE %1 with %2") - .arg(static_cast(address), 8, 16, QChar('0')) - .toUpper() - .arg(static_cast(value), 8, 16, QChar('0')) - .toUpper(); -} - -static QString formatValue(u32 value, bool is_signed) -{ - if (is_signed) - return QString::number(static_cast(value)); - else - return QString::number(static_cast(value)); -} - -CheatManagerDialog::CheatManagerDialog(QWidget* parent) : QDialog(parent) -{ - m_ui.setupUi(this); - - setupAdditionalUi(); - connectUi(); - - updateCheatList(); -} - -CheatManagerDialog::~CheatManagerDialog() = default; - -void CheatManagerDialog::setupAdditionalUi() -{ - m_ui.scanStartAddress->setText(formatHexValue(m_scanner.GetStartAddress(), 8)); - m_ui.scanEndAddress->setText(formatHexValue(m_scanner.GetEndAddress(), 8)); -} - -void CheatManagerDialog::connectUi() -{ - connect(m_ui.tabWidget, &QTabWidget::currentChanged, [this](int index) { - resizeColumns(); - setUpdateTimerEnabled(index == 1); - }); - connect(m_ui.cheatList, &QTreeWidget::currentItemChanged, this, &CheatManagerDialog::cheatListCurrentItemChanged); - connect(m_ui.cheatList, &QTreeWidget::itemActivated, this, &CheatManagerDialog::cheatListItemActivated); - connect(m_ui.cheatList, &QTreeWidget::itemChanged, this, &CheatManagerDialog::cheatListItemChanged); - connect(m_ui.cheatListNewCategory, &QPushButton::clicked, this, &CheatManagerDialog::newCategoryClicked); - connect(m_ui.cheatListAdd, &QPushButton::clicked, this, &CheatManagerDialog::addCodeClicked); - connect(m_ui.cheatListEdit, &QPushButton::clicked, this, &CheatManagerDialog::editCodeClicked); - connect(m_ui.cheatListRemove, &QPushButton::clicked, this, &CheatManagerDialog::deleteCodeClicked); - connect(m_ui.cheatListActivate, &QPushButton::clicked, this, &CheatManagerDialog::activateCodeClicked); - connect(m_ui.cheatListImport, &QPushButton::clicked, this, &CheatManagerDialog::importClicked); - connect(m_ui.cheatListExport, &QPushButton::clicked, this, &CheatManagerDialog::exportClicked); - connect(m_ui.cheatListClear, &QPushButton::clicked, this, &CheatManagerDialog::clearClicked); - connect(m_ui.cheatListReset, &QPushButton::clicked, this, &CheatManagerDialog::resetClicked); - - connect(m_ui.scanValue, &QLineEdit::textChanged, this, &CheatManagerDialog::updateScanValue); - connect(m_ui.scanValueBase, QOverload::of(&QComboBox::currentIndexChanged), - [this](int index) { updateScanValue(); }); - connect(m_ui.scanSize, QOverload::of(&QComboBox::currentIndexChanged), [this](int index) { - m_scanner.SetSize(static_cast(index)); - m_scanner.ResetSearch(); - updateResults(); - }); - connect(m_ui.scanValueSigned, QOverload::of(&QComboBox::currentIndexChanged), [this](int index) { - m_scanner.SetValueSigned(index == 0); - m_scanner.ResetSearch(); - updateResults(); - }); - connect(m_ui.scanOperator, QOverload::of(&QComboBox::currentIndexChanged), - [this](int index) { m_scanner.SetOperator(static_cast(index)); }); - connect(m_ui.scanStartAddress, &QLineEdit::textChanged, [this](const QString& value) { - uint address; - if (value.startsWith(QStringLiteral("0x")) && value.length() > 2) - address = value.mid(2).toUInt(nullptr, 16); - else - address = value.toUInt(nullptr, 16); - m_scanner.SetStartAddress(static_cast(address)); - }); - connect(m_ui.scanEndAddress, &QLineEdit::textChanged, [this](const QString& value) { - uint address; - if (value.startsWith(QStringLiteral("0x")) && value.length() > 2) - address = value.mid(2).toUInt(nullptr, 16); - else - address = value.toUInt(nullptr, 16); - m_scanner.SetEndAddress(static_cast(address)); - }); - connect(m_ui.scanPresetRange, QOverload::of(&QComboBox::currentIndexChanged), [this](int index) { - if (index == 0) - { - m_ui.scanStartAddress->setText(formatHexValue(0, 8)); - m_ui.scanEndAddress->setText(formatHexValue(Bus::g_ram_size, 8)); - } - else if (index == 1) - { - m_ui.scanStartAddress->setText(formatHexValue(CPU::SCRATCHPAD_ADDR, 8)); - m_ui.scanEndAddress->setText(formatHexValue(CPU::SCRATCHPAD_ADDR + CPU::SCRATCHPAD_SIZE, 8)); - } - else - { - m_ui.scanStartAddress->setText(formatHexValue(Bus::BIOS_BASE, 8)); - m_ui.scanEndAddress->setText(formatHexValue(Bus::BIOS_BASE + Bus::BIOS_SIZE, 8)); - } - }); - connect(m_ui.scanNewSearch, &QPushButton::clicked, [this]() { - m_scanner.Search(); - updateResults(); - }); - connect(m_ui.scanSearchAgain, &QPushButton::clicked, [this]() { - m_scanner.SearchAgain(); - updateResults(); - }); - connect(m_ui.scanResetSearch, &QPushButton::clicked, [this]() { - m_scanner.ResetSearch(); - updateResults(); - }); - connect(m_ui.scanAddWatch, &QPushButton::clicked, this, &CheatManagerDialog::addToWatchClicked); - connect(m_ui.scanAddManualAddress, &QPushButton::clicked, this, &CheatManagerDialog::addManualWatchAddressClicked); - connect(m_ui.scanRemoveWatch, &QPushButton::clicked, this, &CheatManagerDialog::removeWatchClicked); - connect(m_ui.scanTable, &QTableWidget::currentItemChanged, this, &CheatManagerDialog::scanCurrentItemChanged); - connect(m_ui.watchTable, &QTableWidget::currentItemChanged, this, &CheatManagerDialog::watchCurrentItemChanged); - connect(m_ui.scanTable, &QTableWidget::itemChanged, this, &CheatManagerDialog::scanItemChanged); - connect(m_ui.watchTable, &QTableWidget::itemChanged, this, &CheatManagerDialog::watchItemChanged); - - connect(g_emu_thread, &EmuThread::cheatEnabled, this, &CheatManagerDialog::setCheatCheckState); -} - -void CheatManagerDialog::showEvent(QShowEvent* event) -{ - QDialog::showEvent(event); - resizeColumns(); -} - -void CheatManagerDialog::resizeEvent(QResizeEvent* event) -{ - QDialog::resizeEvent(event); - resizeColumns(); -} - -void CheatManagerDialog::resizeColumns() -{ - QtUtils::ResizeColumnsForTableView(m_ui.scanTable, {-1, 130, 130}); - QtUtils::ResizeColumnsForTableView(m_ui.watchTable, {-1, 100, 100, 100, 40}); - QtUtils::ResizeColumnsForTreeView(m_ui.cheatList, {-1, 100, 150, 100}); -} - -void CheatManagerDialog::setUpdateTimerEnabled(bool enabled) -{ - if ((!m_update_timer && !enabled) && m_update_timer->isActive() == enabled) - return; - - if (!m_update_timer) - { - m_update_timer = new QTimer(this); - connect(m_update_timer, &QTimer::timeout, this, &CheatManagerDialog::updateScanUi); - } - - if (enabled) - m_update_timer->start(100); - else - m_update_timer->stop(); -} - -int CheatManagerDialog::getSelectedResultIndexFirst() const -{ - QList sel = m_ui.scanTable->selectedRanges(); - if (sel.isEmpty()) - return -1; - - return sel.front().topRow(); -} - -int CheatManagerDialog::getSelectedResultIndexLast() const -{ - QList sel = m_ui.scanTable->selectedRanges(); - if (sel.isEmpty()) - return -1; - - return sel.front().bottomRow(); -} - -int CheatManagerDialog::getSelectedWatchIndexFirst() const -{ - QList sel = m_ui.watchTable->selectedRanges(); - if (sel.isEmpty()) - return -1; - - return sel.front().topRow(); -} - -int CheatManagerDialog::getSelectedWatchIndexLast() const -{ - QList sel = m_ui.watchTable->selectedRanges(); - if (sel.isEmpty()) - return -1; - - return sel.front().bottomRow(); -} - -QTreeWidgetItem* CheatManagerDialog::getItemForCheatIndex(u32 index) const -{ - QTreeWidgetItemIterator iter(m_ui.cheatList); - while (*iter) - { - QTreeWidgetItem* item = *iter; - const QVariant item_data(item->data(0, Qt::UserRole)); - if (item_data.isValid() && item_data.toUInt() == index) - return item; - - ++iter; - } - - return nullptr; -} - -QTreeWidgetItem* CheatManagerDialog::getItemForCheatGroup(const QString& group_name) const -{ - const int count = m_ui.cheatList->topLevelItemCount(); - for (int i = 0; i < count; i++) - { - QTreeWidgetItem* item = m_ui.cheatList->topLevelItem(i); - if (item->text(0) == group_name) - return item; - } - - return nullptr; -} - -QTreeWidgetItem* CheatManagerDialog::createItemForCheatGroup(const QString& group_name) const -{ - QTreeWidgetItem* group = new QTreeWidgetItem(); - group->setFlags(group->flags() | Qt::ItemIsUserCheckable); - group->setText(0, group_name); - m_ui.cheatList->addTopLevelItem(group); - return group; -} - -QStringList CheatManagerDialog::getCheatGroupNames() const -{ - QStringList group_names; - - const int count = m_ui.cheatList->topLevelItemCount(); - for (int i = 0; i < count; i++) - { - QTreeWidgetItem* item = m_ui.cheatList->topLevelItem(i); - group_names.push_back(item->text(0)); - } - - return group_names; -} - -static int getCheatIndexFromItem(QTreeWidgetItem* item) -{ - QVariant item_data(item->data(0, Qt::UserRole)); - if (!item_data.isValid()) - return -1; - - return static_cast(item_data.toUInt()); -} - -int CheatManagerDialog::getSelectedCheatIndex() const -{ - QList sel = m_ui.cheatList->selectedItems(); - if (sel.isEmpty()) - return -1; - - return static_cast(getCheatIndexFromItem(sel.first())); -} - -CheatList* CheatManagerDialog::getCheatList() const -{ - Assert(System::IsValid()); - - CheatList* list = System::GetCheatList(); - if (!list) - { - System::LoadCheatList(); - list = System::GetCheatList(); - } - if (!list) - { - System::LoadCheatListFromDatabase(); - list = System::GetCheatList(); - } - if (!list) - { - Host::RunOnCPUThread([]() { System::SetCheatList(std::make_unique()); }, true); - list = System::GetCheatList(); - } - - return list; -} - -void CheatManagerDialog::updateCheatList() -{ - QSignalBlocker sb(m_ui.cheatList); - - CheatList* list = getCheatList(); - while (m_ui.cheatList->topLevelItemCount() > 0) - delete m_ui.cheatList->takeTopLevelItem(0); - - const std::vector groups = list->GetCodeGroups(); - for (const std::string& group_name : groups) - { - QTreeWidgetItem* group = createItemForCheatGroup(QString::fromStdString(group_name)); - - const u32 count = list->GetCodeCount(); - bool all_enabled = true; - for (u32 i = 0; i < count; i++) - { - const CheatCode& code = list->GetCode(i); - if (code.group != group_name) - continue; - - QTreeWidgetItem* item = new QTreeWidgetItem(group); - fillItemForCheatCode(item, i, code); - - all_enabled &= code.enabled; - } - - group->setCheckState(0, all_enabled ? Qt::Checked : Qt::Unchecked); - group->setExpanded(true); - } - - m_ui.cheatListEdit->setEnabled(false); - m_ui.cheatListRemove->setEnabled(false); - m_ui.cheatListActivate->setText(tr("Activate")); - m_ui.cheatListActivate->setEnabled(false); - m_ui.cheatListExport->setEnabled(list->GetCodeCount() > 0); -} - -void CheatManagerDialog::fillItemForCheatCode(QTreeWidgetItem* item, u32 index, const CheatCode& code) -{ - item->setData(0, Qt::UserRole, QVariant(static_cast(index))); - if (code.IsManuallyActivated()) - { - item->setFlags(item->flags() & ~(Qt::ItemIsUserCheckable)); - } - else - { - item->setFlags(item->flags() | Qt::ItemIsUserCheckable); - item->setCheckState(0, code.enabled ? Qt::Checked : Qt::Unchecked); - } - item->setText(0, QString::fromStdString(code.description)); - item->setText(1, qApp->translate("Cheats", CheatCode::GetTypeDisplayName(code.type))); - item->setText(2, qApp->translate("Cheats", CheatCode::GetActivationDisplayName(code.activation))); - item->setText(3, QString::number(static_cast(code.instructions.size()))); -} - -void CheatManagerDialog::saveCheatList() -{ - Host::RunOnCPUThread([]() { System::SaveCheatList(); }); -} - -void CheatManagerDialog::cheatListCurrentItemChanged(QTreeWidgetItem* current, QTreeWidgetItem* previous) -{ - const int cheat_index = current ? getCheatIndexFromItem(current) : -1; - const bool has_current = (cheat_index >= 0); - m_ui.cheatListEdit->setEnabled(has_current); - m_ui.cheatListRemove->setEnabled(has_current); - m_ui.cheatListActivate->setEnabled(has_current); - - if (!has_current) - { - m_ui.cheatListActivate->setText(tr("Activate")); - } - else - { - const bool manual_activation = getCheatList()->GetCode(static_cast(cheat_index)).IsManuallyActivated(); - m_ui.cheatListActivate->setText(manual_activation ? tr("Activate") : tr("Toggle")); - } -} - -void CheatManagerDialog::cheatListItemActivated(QTreeWidgetItem* item) -{ - if (!item) - return; - - const int index = getCheatIndexFromItem(item); - if (index >= 0) - activateCheat(static_cast(index)); -} - -void CheatManagerDialog::cheatListItemChanged(QTreeWidgetItem* item, int column) -{ - if (!item || column != 0) - return; - - CheatList* list = getCheatList(); - - const int index = getCheatIndexFromItem(item); - if (index < 0) - { - // we're probably a parent/group node - const int child_count = item->childCount(); - const Qt::CheckState cs = item->checkState(0); - for (int i = 0; i < child_count; i++) - item->child(i)->setCheckState(0, cs); - - return; - } - - if (static_cast(index) >= list->GetCodeCount()) - return; - - CheatCode& cc = list->GetCode(static_cast(index)); - if (cc.IsManuallyActivated()) - return; - - const bool new_enabled = (item->checkState(0) == Qt::Checked); - if (cc.enabled == new_enabled) - return; - - Host::RunOnCPUThread([index, new_enabled]() { - System::GetCheatList()->SetCodeEnabled(static_cast(index), new_enabled); - System::SaveCheatList(); - }); -} - -void CheatManagerDialog::activateCheat(u32 index) -{ - CheatList* list = getCheatList(); - if (index >= list->GetCodeCount()) - return; - - CheatCode& cc = list->GetCode(index); - if (cc.IsManuallyActivated()) - { - g_emu_thread->applyCheat(index); - return; - } - - const bool new_enabled = !cc.enabled; - setCheatCheckState(index, new_enabled); - - Host::RunOnCPUThread([index, new_enabled]() { - System::GetCheatList()->SetCodeEnabled(index, new_enabled); - System::SaveCheatList(); - }); -} - -void CheatManagerDialog::setCheatCheckState(u32 index, bool checked) -{ - QTreeWidgetItem* item = getItemForCheatIndex(index); - if (item) - { - QSignalBlocker sb(m_ui.cheatList); - item->setCheckState(0, checked ? Qt::Checked : Qt::Unchecked); - } -} - -void CheatManagerDialog::newCategoryClicked() -{ - QString group_name = QInputDialog::getText(this, tr("Add Group"), tr("Group Name:")); - if (group_name.isEmpty()) - return; - - if (getItemForCheatGroup(group_name) != nullptr) - { - QMessageBox::critical(this, tr("Error"), tr("This group name already exists.")); - return; - } - - createItemForCheatGroup(group_name); -} - -void CheatManagerDialog::addCodeClicked() -{ - CheatList* list = getCheatList(); - - CheatCode new_code; - new_code.group = "Ungrouped"; - - CheatCodeEditorDialog editor(getCheatGroupNames(), &new_code, this); - if (editor.exec() > 0) - { - const QString group_name_qstr(QString::fromStdString(new_code.group)); - QTreeWidgetItem* group_item = getItemForCheatGroup(group_name_qstr); - if (!group_item) - group_item = createItemForCheatGroup(group_name_qstr); - - QTreeWidgetItem* item = new QTreeWidgetItem(group_item); - fillItemForCheatCode(item, list->GetCodeCount(), new_code); - group_item->setExpanded(true); - - Host::RunOnCPUThread( - [&new_code]() { - System::GetCheatList()->AddCode(std::move(new_code)); - System::SaveCheatList(); - }, - true); - } -} - -void CheatManagerDialog::editCodeClicked() -{ - int index = getSelectedCheatIndex(); - if (index < 0) - return; - - CheatList* list = getCheatList(); - if (static_cast(index) >= list->GetCodeCount()) - return; - - CheatCode new_code = list->GetCode(static_cast(index)); - CheatCodeEditorDialog editor(getCheatGroupNames(), &new_code, this); - if (editor.exec() > 0) - { - QTreeWidgetItem* item = getItemForCheatIndex(static_cast(index)); - if (item) - { - if (new_code.group != list->GetCode(static_cast(index)).group) - { - item = item->parent()->takeChild(item->parent()->indexOfChild(item)); - - const QString group_name_qstr(QString::fromStdString(new_code.group)); - QTreeWidgetItem* group_item = getItemForCheatGroup(group_name_qstr); - if (!group_item) - group_item = createItemForCheatGroup(group_name_qstr); - group_item->addChild(item); - group_item->setExpanded(true); - } - - fillItemForCheatCode(item, static_cast(index), new_code); - } - else - { - // shouldn't happen... - updateCheatList(); - } - - Host::RunOnCPUThread( - [index, &new_code]() { - System::GetCheatList()->SetCode(static_cast(index), std::move(new_code)); - System::SaveCheatList(); - }, - true); - } -} - -void CheatManagerDialog::deleteCodeClicked() -{ - int index = getSelectedCheatIndex(); - if (index < 0) - return; - - CheatList* list = getCheatList(); - if (static_cast(index) >= list->GetCodeCount()) - return; - - if (QMessageBox::question(this, tr("Delete Code"), - tr("Are you sure you wish to delete the selected code? This action is not reversible."), - QMessageBox::Yes, QMessageBox::No) != QMessageBox::Yes) - { - return; - } - - Host::RunOnCPUThread( - [index]() { - System::GetCheatList()->RemoveCode(static_cast(index)); - System::SaveCheatList(); - }, - true); - updateCheatList(); -} - -void CheatManagerDialog::activateCodeClicked() -{ - int index = getSelectedCheatIndex(); - if (index < 0) - return; - - activateCheat(static_cast(index)); -} - -void CheatManagerDialog::importClicked() -{ - QMenu menu(this); - connect(menu.addAction(tr("From File...")), &QAction::triggered, this, &CheatManagerDialog::importFromFileTriggered); - connect(menu.addAction(tr("From Text...")), &QAction::triggered, this, &CheatManagerDialog::importFromTextTriggered); - menu.exec(QCursor::pos()); -} - -void CheatManagerDialog::importFromFileTriggered() -{ - 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; - - CheatList new_cheats; - if (!new_cheats.LoadFromFile(filename.toUtf8().constData(), CheatList::Format::Autodetect)) - { - QMessageBox::critical(this, tr("Error"), tr("Failed to parse cheat file. The log may contain more information.")); - return; - } - - Host::RunOnCPUThread( - [&new_cheats]() { - DebugAssert(System::HasCheatList()); - System::GetCheatList()->MergeList(new_cheats); - System::SaveCheatList(); - }, - true); - updateCheatList(); -} - -void CheatManagerDialog::importFromTextTriggered() -{ - const QString text = QInputDialog::getMultiLineText(this, tr("Import Cheats"), tr("Cheat File Text:")); - if (text.isEmpty()) - return; - - CheatList new_cheats; - if (!new_cheats.LoadFromString(text.toStdString(), CheatList::Format::Autodetect)) - { - QMessageBox::critical(this, tr("Error"), tr("Failed to parse cheat file. The log may contain more information.")); - return; - } - - Host::RunOnCPUThread( - [&new_cheats]() { - DebugAssert(System::HasCheatList()); - System::GetCheatList()->MergeList(new_cheats); - System::SaveCheatList(); - }, - true); - updateCheatList(); -} - -void CheatManagerDialog::exportClicked() -{ - 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; - - if (!getCheatList()->SaveToPCSXRFile(filename.toUtf8().constData())) - QMessageBox::critical(this, tr("Error"), tr("Failed to save cheat file. The log may contain more information.")); -} - -void CheatManagerDialog::clearClicked() -{ - if (QMessageBox::question(this, tr("Confirm Clear"), - tr("Are you sure you want to remove all cheats? This is not reversible.")) != - QMessageBox::Yes) - { - return; - } - - Host::RunOnCPUThread([] { System::ClearCheatList(true); }, true); - updateCheatList(); -} - -void CheatManagerDialog::resetClicked() -{ - if (QMessageBox::question( - this, tr("Confirm Reset"), - tr( - "Are you sure you want to reset the cheat list? Any cheats not in the DuckStation database WILL BE LOST.")) != - QMessageBox::Yes) - { - return; - } - - Host::RunOnCPUThread([] { System::DeleteCheatList(); }, true); - updateCheatList(); -} - -void CheatManagerDialog::addToWatchClicked() -{ - const int indexFirst = getSelectedResultIndexFirst(); - const int indexLast = getSelectedResultIndexLast(); - if (indexFirst < 0) - return; - - for (int index = indexFirst; index <= indexLast; index++) - { - const MemoryScan::Result& res = m_scanner.GetResults()[static_cast(index)]; - m_watch.AddEntry(fmt::format("0x{:08x}", res.address), res.address, m_scanner.GetSize(), m_scanner.GetValueSigned(), - false); - updateWatch(); - } -} - -void CheatManagerDialog::addManualWatchAddressClicked() -{ - std::optional address = QtUtils::PromptForAddress(this, windowTitle(), tr("Enter manual address:"), false); - if (!address.has_value()) - return; - - QStringList items; - for (const char* title : s_size_strings) - items.append(tr(title)); - - bool ok = false; - QString selected_item(QInputDialog::getItem(this, windowTitle(), tr("Select data size:"), items, 0, false, &ok)); - int index = items.indexOf(selected_item); - if (index < 0 || !ok) - return; - - if (index == 1 || index == 4) - address.value() &= 0xFFFFFFFE; - else if (index == 2 || index == 5) - address.value() &= 0xFFFFFFFC; - - m_watch.AddEntry(fmt::format("0x{:08x}", address.value()), address.value(), static_cast(index % 3), - (index > 3), false); - updateWatch(); -} - -void CheatManagerDialog::removeWatchClicked() -{ - const int indexFirst = getSelectedWatchIndexFirst(); - const int indexLast = getSelectedWatchIndexLast(); - if (indexFirst < 0) - return; - - for (int index = indexLast; index >= indexFirst; index--) - { - m_watch.RemoveEntry(static_cast(index)); - updateWatch(); - } -} - -void CheatManagerDialog::scanCurrentItemChanged(QTableWidgetItem* current, QTableWidgetItem* previous) -{ - m_ui.scanAddWatch->setEnabled((current != nullptr)); -} - -void CheatManagerDialog::watchCurrentItemChanged(QTableWidgetItem* current, QTableWidgetItem* previous) -{ - m_ui.scanRemoveWatch->setEnabled((current != nullptr)); -} - -void CheatManagerDialog::scanItemChanged(QTableWidgetItem* item) -{ - const u32 index = static_cast(item->row()); - switch (item->column()) - { - case 1: - { - bool value_ok = false; - if (m_scanner.GetValueSigned()) - { - int value = item->text().toInt(&value_ok); - if (value_ok) - m_scanner.SetResultValue(index, static_cast(value)); - } - else - { - uint value = item->text().toUInt(&value_ok); - if (value_ok) - m_scanner.SetResultValue(index, static_cast(value)); - } - } - break; - - default: - break; - } -} - -void CheatManagerDialog::watchItemChanged(QTableWidgetItem* item) -{ - const u32 index = static_cast(item->row()); - if (index >= m_watch.GetEntryCount()) - return; - - switch (item->column()) - { - case 4: - { - m_watch.SetEntryFreeze(index, (item->checkState() == Qt::Checked)); - } - break; - - case 0: - { - m_watch.SetEntryDescription(index, item->text().toStdString()); - } - break; - - case 3: - { - const MemoryWatchList::Entry& entry = m_watch.GetEntry(index); - bool value_ok = false; - if (entry.is_signed) - { - int value = item->text().toInt(&value_ok); - if (value_ok) - m_watch.SetEntryValue(index, static_cast(value)); - } - else - { - uint value; - if (item->text()[1] == 'x' || item->text()[1] == 'X') - value = item->text().toUInt(&value_ok, 16); - else - value = item->text().toUInt(&value_ok); - if (value_ok) - m_watch.SetEntryValue(index, static_cast(value)); - } - } - break; - - default: - break; - } -} - -void CheatManagerDialog::updateScanValue() -{ - QString value = m_ui.scanValue->text(); - if (value.startsWith(QStringLiteral("0x"))) - value.remove(0, 2); - - bool ok = false; - uint uint_value = value.toUInt(&ok, (m_ui.scanValueBase->currentIndex() > 0) ? 16 : 10); - if (ok) - m_scanner.SetValue(uint_value); -} - -void CheatManagerDialog::updateResults() -{ - QSignalBlocker sb(m_ui.scanTable); - m_ui.scanTable->setRowCount(0); - - const MemoryScan::ResultVector& results = m_scanner.GetResults(); - if (!results.empty()) - { - int row = 0; - for (const MemoryScan::Result& res : m_scanner.GetResults()) - { - if (row == MAX_DISPLAYED_SCAN_RESULTS) - { - break; - } - - m_ui.scanTable->insertRow(row); - - QTableWidgetItem* address_item = new QTableWidgetItem(formatHexValue(res.address, 8)); - address_item->setFlags(address_item->flags() & ~(Qt::ItemIsEditable)); - m_ui.scanTable->setItem(row, 0, address_item); - - QTableWidgetItem* value_item; - if (m_ui.scanValueBase->currentIndex() == 0) - value_item = new QTableWidgetItem(formatValue(res.value, m_scanner.GetValueSigned())); - else if (m_scanner.GetSize() == MemoryAccessSize::Byte) - value_item = new QTableWidgetItem(formatHexValue(res.value, 2)); - else if (m_scanner.GetSize() == MemoryAccessSize::HalfWord) - value_item = new QTableWidgetItem(formatHexValue(res.value, 4)); - else - value_item = new QTableWidgetItem(formatHexValue(res.value, 8)); - m_ui.scanTable->setItem(row, 1, value_item); - - QTableWidgetItem* previous_item; - if (m_ui.scanValueBase->currentIndex() == 0) - previous_item = new QTableWidgetItem(formatValue(res.last_value, m_scanner.GetValueSigned())); - else if (m_scanner.GetSize() == MemoryAccessSize::Byte) - previous_item = new QTableWidgetItem(formatHexValue(res.last_value, 2)); - else if (m_scanner.GetSize() == MemoryAccessSize::HalfWord) - previous_item = new QTableWidgetItem(formatHexValue(res.last_value, 4)); - else - previous_item = new QTableWidgetItem(formatHexValue(res.last_value, 8)); - - previous_item->setFlags(address_item->flags() & ~(Qt::ItemIsEditable)); - m_ui.scanTable->setItem(row, 2, previous_item); - row++; - } - m_ui.scanResultsCount->setText(QString::number(m_scanner.GetResultCount())); - } - else - m_ui.scanResultsCount->setText("0"); - - m_ui.scanResetSearch->setEnabled(!results.empty()); - m_ui.scanSearchAgain->setEnabled(!results.empty()); - m_ui.scanAddWatch->setEnabled(false); -} - -void CheatManagerDialog::updateResultsValues() -{ - QSignalBlocker sb(m_ui.scanTable); - - int row = 0; - for (const MemoryScan::Result& res : m_scanner.GetResults()) - { - if (res.value_changed) - { - QTableWidgetItem* item = m_ui.scanTable->item(row, 1); - if (m_ui.scanValueBase->currentIndex() == 0) - item->setText(formatValue(res.value, m_scanner.GetValueSigned())); - else if (m_scanner.GetSize() == MemoryAccessSize::Byte) - item->setText(formatHexValue(res.value, 2)); - else if (m_scanner.GetSize() == MemoryAccessSize::HalfWord) - item->setText(formatHexValue(res.value, 4)); - else - item->setText(formatHexValue(res.value, 8)); - item->setForeground(Qt::red); - } - - row++; - if (row == MAX_DISPLAYED_SCAN_RESULTS) - break; - } -} - -void CheatManagerDialog::updateWatch() -{ - m_watch.UpdateValues(); - - QSignalBlocker sb(m_ui.watchTable); - m_ui.watchTable->setRowCount(0); - - const MemoryWatchList::EntryVector& entries = m_watch.GetEntries(); - if (!entries.empty()) - { - int row = 0; - for (const MemoryWatchList::Entry& res : entries) - { - m_ui.watchTable->insertRow(row); - - QTableWidgetItem* description_item = new QTableWidgetItem(formatCheatCode(res.address, res.value, res.size)); - m_ui.watchTable->setItem(row, 0, description_item); - - QTableWidgetItem* address_item = new QTableWidgetItem(formatHexValue(res.address, 8)); - address_item->setFlags(address_item->flags() & ~(Qt::ItemIsEditable)); - m_ui.watchTable->setItem(row, 1, address_item); - - QTableWidgetItem* size_item = - new QTableWidgetItem(tr(s_size_strings[static_cast(res.size) + (res.is_signed ? 3 : 0)])); - size_item->setFlags(address_item->flags() & ~(Qt::ItemIsEditable)); - m_ui.watchTable->setItem(row, 2, size_item); - - QTableWidgetItem* value_item; - if (res.size == MemoryAccessSize::Byte) - value_item = new QTableWidgetItem(formatHexAndDecValue(res.value, 2, res.is_signed)); - else if (res.size == MemoryAccessSize::HalfWord) - value_item = new QTableWidgetItem(formatHexAndDecValue(res.value, 4, res.is_signed)); - else - value_item = new QTableWidgetItem(formatHexAndDecValue(res.value, 8, res.is_signed)); - - m_ui.watchTable->setItem(row, 3, value_item); - - QTableWidgetItem* freeze_item = new QTableWidgetItem(); - freeze_item->setFlags(freeze_item->flags() | (Qt::ItemIsEditable | Qt::ItemIsUserCheckable)); - freeze_item->setCheckState(res.freeze ? Qt::Checked : Qt::Unchecked); - m_ui.watchTable->setItem(row, 4, freeze_item); - - row++; - } - } - - m_ui.scanSaveWatch->setEnabled(!entries.empty()); - m_ui.scanRemoveWatch->setEnabled(false); -} - -void CheatManagerDialog::updateWatchValues() -{ - QSignalBlocker sb(m_ui.watchTable); - int row = 0; - for (const MemoryWatchList::Entry& res : m_watch.GetEntries()) - { - if (res.changed) - { - if (m_ui.scanValueBase->currentIndex() == 0) - m_ui.watchTable->item(row, 3)->setText(formatValue(res.value, res.is_signed)); - else if (m_scanner.GetSize() == MemoryAccessSize::Byte) - m_ui.watchTable->item(row, 3)->setText(formatHexValue(res.value, 2)); - else if (m_scanner.GetSize() == MemoryAccessSize::HalfWord) - m_ui.watchTable->item(row, 3)->setText(formatHexValue(res.value, 4)); - else - m_ui.watchTable->item(row, 3)->setText(formatHexValue(res.value, 8)); - } - row++; - } -} - -void CheatManagerDialog::updateScanUi() -{ - m_scanner.UpdateResultsValues(); - m_watch.UpdateValues(); - - updateResultsValues(); - updateWatchValues(); -} diff --git a/src/duckstation-qt/cheatmanagerwindow.cpp b/src/duckstation-qt/cheatmanagerwindow.cpp new file mode 100644 index 000000000..21f40062a --- /dev/null +++ b/src/duckstation-qt/cheatmanagerwindow.cpp @@ -0,0 +1,578 @@ +// SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin and contributors. +// SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0) + +#include "cheatmanagerwindow.h" +#include "cheatcodeeditordialog.h" +#include "mainwindow.h" +#include "qthost.h" +#include "qtutils.h" + +#include "core/bus.h" +#include "core/cpu_core.h" +#include "core/host.h" +#include "core/system.h" + +#include "common/assert.h" +#include "common/string_util.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +CheatManagerWindow::CheatManagerWindow() : QWidget() +{ + m_ui.setupUi(this); + + connectUi(); + + updateCheatList(); +} + +CheatManagerWindow::~CheatManagerWindow() = default; + +void CheatManagerWindow::connectUi() +{ + connect(m_ui.cheatList, &QTreeWidget::currentItemChanged, this, &CheatManagerWindow::cheatListCurrentItemChanged); + connect(m_ui.cheatList, &QTreeWidget::itemActivated, this, &CheatManagerWindow::cheatListItemActivated); + connect(m_ui.cheatList, &QTreeWidget::itemChanged, this, &CheatManagerWindow::cheatListItemChanged); + connect(m_ui.cheatListNewCategory, &QPushButton::clicked, this, &CheatManagerWindow::newCategoryClicked); + connect(m_ui.cheatListAdd, &QPushButton::clicked, this, &CheatManagerWindow::addCodeClicked); + connect(m_ui.cheatListEdit, &QPushButton::clicked, this, &CheatManagerWindow::editCodeClicked); + connect(m_ui.cheatListRemove, &QPushButton::clicked, this, &CheatManagerWindow::deleteCodeClicked); + connect(m_ui.cheatListActivate, &QPushButton::clicked, this, &CheatManagerWindow::activateCodeClicked); + connect(m_ui.cheatListImport, &QPushButton::clicked, this, &CheatManagerWindow::importClicked); + connect(m_ui.cheatListExport, &QPushButton::clicked, this, &CheatManagerWindow::exportClicked); + connect(m_ui.cheatListClear, &QPushButton::clicked, this, &CheatManagerWindow::clearClicked); + connect(m_ui.cheatListReset, &QPushButton::clicked, this, &CheatManagerWindow::resetClicked); + + connect(g_emu_thread, &EmuThread::cheatEnabled, this, &CheatManagerWindow::setCheatCheckState); + connect(g_emu_thread, &EmuThread::runningGameChanged, this, &CheatManagerWindow::updateCheatList); +} + +void CheatManagerWindow::showEvent(QShowEvent* event) +{ + QWidget::showEvent(event); + resizeColumns(); +} + +void CheatManagerWindow::closeEvent(QCloseEvent* event) +{ + QWidget::closeEvent(event); + emit closed(); +} + +void CheatManagerWindow::resizeEvent(QResizeEvent* event) +{ + QWidget::resizeEvent(event); + resizeColumns(); +} + +void CheatManagerWindow::resizeColumns() +{ + QtUtils::ResizeColumnsForTreeView(m_ui.cheatList, {-1, 100, 150, 100}); +} + +QTreeWidgetItem* CheatManagerWindow::getItemForCheatIndex(u32 index) const +{ + QTreeWidgetItemIterator iter(m_ui.cheatList); + while (*iter) + { + QTreeWidgetItem* item = *iter; + const QVariant item_data(item->data(0, Qt::UserRole)); + if (item_data.isValid() && item_data.toUInt() == index) + return item; + + ++iter; + } + + return nullptr; +} + +QTreeWidgetItem* CheatManagerWindow::getItemForCheatGroup(const QString& group_name) const +{ + const int count = m_ui.cheatList->topLevelItemCount(); + for (int i = 0; i < count; i++) + { + QTreeWidgetItem* item = m_ui.cheatList->topLevelItem(i); + if (item->text(0) == group_name) + return item; + } + + return nullptr; +} + +QTreeWidgetItem* CheatManagerWindow::createItemForCheatGroup(const QString& group_name) const +{ + QTreeWidgetItem* group = new QTreeWidgetItem(); + group->setFlags(group->flags() | Qt::ItemIsUserCheckable); + group->setText(0, group_name); + m_ui.cheatList->addTopLevelItem(group); + return group; +} + +QStringList CheatManagerWindow::getCheatGroupNames() const +{ + QStringList group_names; + + const int count = m_ui.cheatList->topLevelItemCount(); + for (int i = 0; i < count; i++) + { + QTreeWidgetItem* item = m_ui.cheatList->topLevelItem(i); + group_names.push_back(item->text(0)); + } + + return group_names; +} + +static int getCheatIndexFromItem(QTreeWidgetItem* item) +{ + QVariant item_data(item->data(0, Qt::UserRole)); + if (!item_data.isValid()) + return -1; + + return static_cast(item_data.toUInt()); +} + +int CheatManagerWindow::getSelectedCheatIndex() const +{ + QList sel = m_ui.cheatList->selectedItems(); + if (sel.isEmpty()) + return -1; + + return static_cast(getCheatIndexFromItem(sel.first())); +} + +CheatList* CheatManagerWindow::getCheatList() const +{ + return System::IsValid() ? System::GetCheatList() : nullptr; +} + +void CheatManagerWindow::updateCheatList() +{ + QSignalBlocker sb(m_ui.cheatList); + while (m_ui.cheatList->topLevelItemCount() > 0) + delete m_ui.cheatList->takeTopLevelItem(0); + + m_ui.cheatList->setEnabled(false); + m_ui.cheatListAdd->setEnabled(false); + m_ui.cheatListNewCategory->setEnabled(false); + m_ui.cheatListEdit->setEnabled(false); + m_ui.cheatListRemove->setEnabled(false); + m_ui.cheatListActivate->setText(tr("Activate")); + m_ui.cheatListActivate->setEnabled(false); + m_ui.cheatListImport->setEnabled(false); + m_ui.cheatListExport->setEnabled(false); + m_ui.cheatListClear->setEnabled(false); + m_ui.cheatListReset->setEnabled(false); + + Host::RunOnCPUThread([]() { + if (!System::IsValid()) + return; + + CheatList* list = System::GetCheatList(); + if (!list) + { + System::LoadCheatList(); + list = System::GetCheatList(); + } + if (!list) + { + System::LoadCheatListFromDatabase(); + list = System::GetCheatList(); + } + if (!list) + { + System::SetCheatList(std::make_unique()); + list = System::GetCheatList(); + } + + // still racey... + QtHost::RunOnUIThread([list]() { + if (!QtHost::IsSystemValid()) + return; + + CheatManagerWindow* cm = g_main_window->getCheatManagerWindow(); + if (!cm) + return; + + QSignalBlocker sb(cm->m_ui.cheatList); + + const std::vector groups = list->GetCodeGroups(); + for (const std::string& group_name : groups) + { + QTreeWidgetItem* group = cm->createItemForCheatGroup(QString::fromStdString(group_name)); + + const u32 count = list->GetCodeCount(); + bool all_enabled = true; + for (u32 i = 0; i < count; i++) + { + const CheatCode& code = list->GetCode(i); + if (code.group != group_name) + continue; + + QTreeWidgetItem* item = new QTreeWidgetItem(group); + cm->fillItemForCheatCode(item, i, code); + + all_enabled &= code.enabled; + } + + group->setCheckState(0, all_enabled ? Qt::Checked : Qt::Unchecked); + group->setExpanded(true); + } + + cm->m_ui.cheatList->setEnabled(true); + cm->m_ui.cheatListAdd->setEnabled(true); + cm->m_ui.cheatListNewCategory->setEnabled(true); + cm->m_ui.cheatListImport->setEnabled(true); + cm->m_ui.cheatListClear->setEnabled(true); + cm->m_ui.cheatListReset->setEnabled(true); + cm->m_ui.cheatListExport->setEnabled(cm->m_ui.cheatList->topLevelItemCount() > 0); + }); + }); +} + +void CheatManagerWindow::fillItemForCheatCode(QTreeWidgetItem* item, u32 index, const CheatCode& code) +{ + item->setData(0, Qt::UserRole, QVariant(static_cast(index))); + if (code.IsManuallyActivated()) + { + item->setFlags(item->flags() & ~(Qt::ItemIsUserCheckable)); + } + else + { + item->setFlags(item->flags() | Qt::ItemIsUserCheckable); + item->setCheckState(0, code.enabled ? Qt::Checked : Qt::Unchecked); + } + item->setText(0, QString::fromStdString(code.description)); + item->setText(1, qApp->translate("Cheats", CheatCode::GetTypeDisplayName(code.type))); + item->setText(2, qApp->translate("Cheats", CheatCode::GetActivationDisplayName(code.activation))); + item->setText(3, QString::number(static_cast(code.instructions.size()))); +} + +void CheatManagerWindow::saveCheatList() +{ + Host::RunOnCPUThread([]() { System::SaveCheatList(); }); +} + +void CheatManagerWindow::cheatListCurrentItemChanged(QTreeWidgetItem* current, QTreeWidgetItem* previous) +{ + const int cheat_index = current ? getCheatIndexFromItem(current) : -1; + const bool has_current = (cheat_index >= 0); + m_ui.cheatListEdit->setEnabled(has_current); + m_ui.cheatListRemove->setEnabled(has_current); + m_ui.cheatListActivate->setEnabled(has_current); + + if (!has_current) + { + m_ui.cheatListActivate->setText(tr("Activate")); + } + else + { + const bool manual_activation = getCheatList()->GetCode(static_cast(cheat_index)).IsManuallyActivated(); + m_ui.cheatListActivate->setText(manual_activation ? tr("Activate") : tr("Toggle")); + } +} + +void CheatManagerWindow::cheatListItemActivated(QTreeWidgetItem* item) +{ + if (!item) + return; + + const int index = getCheatIndexFromItem(item); + if (index >= 0) + activateCheat(static_cast(index)); +} + +void CheatManagerWindow::cheatListItemChanged(QTreeWidgetItem* item, int column) +{ + if (!item || column != 0) + return; + + CheatList* list = getCheatList(); + + const int index = getCheatIndexFromItem(item); + if (index < 0) + { + // we're probably a parent/group node + const int child_count = item->childCount(); + const Qt::CheckState cs = item->checkState(0); + for (int i = 0; i < child_count; i++) + item->child(i)->setCheckState(0, cs); + + return; + } + + if (static_cast(index) >= list->GetCodeCount()) + return; + + CheatCode& cc = list->GetCode(static_cast(index)); + if (cc.IsManuallyActivated()) + return; + + const bool new_enabled = (item->checkState(0) == Qt::Checked); + if (cc.enabled == new_enabled) + return; + + Host::RunOnCPUThread([index, new_enabled]() { + System::GetCheatList()->SetCodeEnabled(static_cast(index), new_enabled); + System::SaveCheatList(); + }); +} + +void CheatManagerWindow::activateCheat(u32 index) +{ + CheatList* list = getCheatList(); + if (index >= list->GetCodeCount()) + return; + + CheatCode& cc = list->GetCode(index); + if (cc.IsManuallyActivated()) + { + g_emu_thread->applyCheat(index); + return; + } + + const bool new_enabled = !cc.enabled; + setCheatCheckState(index, new_enabled); + + Host::RunOnCPUThread([index, new_enabled]() { + System::GetCheatList()->SetCodeEnabled(index, new_enabled); + System::SaveCheatList(); + }); +} + +void CheatManagerWindow::setCheatCheckState(u32 index, bool checked) +{ + QTreeWidgetItem* item = getItemForCheatIndex(index); + if (item) + { + QSignalBlocker sb(m_ui.cheatList); + item->setCheckState(0, checked ? Qt::Checked : Qt::Unchecked); + } +} + +void CheatManagerWindow::newCategoryClicked() +{ + QString group_name = QInputDialog::getText(this, tr("Add Group"), tr("Group Name:")); + if (group_name.isEmpty()) + return; + + if (getItemForCheatGroup(group_name) != nullptr) + { + QMessageBox::critical(this, tr("Error"), tr("This group name already exists.")); + return; + } + + createItemForCheatGroup(group_name); +} + +void CheatManagerWindow::addCodeClicked() +{ + CheatList* list = getCheatList(); + + CheatCode new_code; + new_code.group = "Ungrouped"; + + CheatCodeEditorDialog editor(getCheatGroupNames(), &new_code, this); + if (editor.exec() > 0) + { + const QString group_name_qstr(QString::fromStdString(new_code.group)); + QTreeWidgetItem* group_item = getItemForCheatGroup(group_name_qstr); + if (!group_item) + group_item = createItemForCheatGroup(group_name_qstr); + + QTreeWidgetItem* item = new QTreeWidgetItem(group_item); + fillItemForCheatCode(item, list->GetCodeCount(), new_code); + group_item->setExpanded(true); + + Host::RunOnCPUThread( + [&new_code]() { + System::GetCheatList()->AddCode(std::move(new_code)); + System::SaveCheatList(); + }, + true); + } +} + +void CheatManagerWindow::editCodeClicked() +{ + int index = getSelectedCheatIndex(); + if (index < 0) + return; + + CheatList* list = getCheatList(); + if (static_cast(index) >= list->GetCodeCount()) + return; + + CheatCode new_code = list->GetCode(static_cast(index)); + CheatCodeEditorDialog editor(getCheatGroupNames(), &new_code, this); + if (editor.exec() > 0) + { + QTreeWidgetItem* item = getItemForCheatIndex(static_cast(index)); + if (item) + { + if (new_code.group != list->GetCode(static_cast(index)).group) + { + item = item->parent()->takeChild(item->parent()->indexOfChild(item)); + + const QString group_name_qstr(QString::fromStdString(new_code.group)); + QTreeWidgetItem* group_item = getItemForCheatGroup(group_name_qstr); + if (!group_item) + group_item = createItemForCheatGroup(group_name_qstr); + group_item->addChild(item); + group_item->setExpanded(true); + } + + fillItemForCheatCode(item, static_cast(index), new_code); + } + else + { + // shouldn't happen... + updateCheatList(); + } + + Host::RunOnCPUThread( + [index, &new_code]() { + System::GetCheatList()->SetCode(static_cast(index), std::move(new_code)); + System::SaveCheatList(); + }, + true); + } +} + +void CheatManagerWindow::deleteCodeClicked() +{ + int index = getSelectedCheatIndex(); + if (index < 0) + return; + + CheatList* list = getCheatList(); + if (static_cast(index) >= list->GetCodeCount()) + return; + + if (QMessageBox::question(this, tr("Delete Code"), + tr("Are you sure you wish to delete the selected code? This action is not reversible."), + QMessageBox::Yes, QMessageBox::No) != QMessageBox::Yes) + { + return; + } + + Host::RunOnCPUThread( + [index]() { + System::GetCheatList()->RemoveCode(static_cast(index)); + System::SaveCheatList(); + }, + true); + updateCheatList(); +} + +void CheatManagerWindow::activateCodeClicked() +{ + int index = getSelectedCheatIndex(); + if (index < 0) + return; + + activateCheat(static_cast(index)); +} + +void CheatManagerWindow::importClicked() +{ + QMenu menu(this); + connect(menu.addAction(tr("From File...")), &QAction::triggered, this, &CheatManagerWindow::importFromFileTriggered); + connect(menu.addAction(tr("From Text...")), &QAction::triggered, this, &CheatManagerWindow::importFromTextTriggered); + menu.exec(QCursor::pos()); +} + +void CheatManagerWindow::importFromFileTriggered() +{ + 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; + + CheatList new_cheats; + if (!new_cheats.LoadFromFile(filename.toUtf8().constData(), CheatList::Format::Autodetect)) + { + QMessageBox::critical(this, tr("Error"), tr("Failed to parse cheat file. The log may contain more information.")); + return; + } + + Host::RunOnCPUThread( + [&new_cheats]() { + DebugAssert(System::HasCheatList()); + System::GetCheatList()->MergeList(new_cheats); + System::SaveCheatList(); + }, + true); + updateCheatList(); +} + +void CheatManagerWindow::importFromTextTriggered() +{ + const QString text = QInputDialog::getMultiLineText(this, tr("Import Cheats"), tr("Cheat File Text:")); + if (text.isEmpty()) + return; + + CheatList new_cheats; + if (!new_cheats.LoadFromString(text.toStdString(), CheatList::Format::Autodetect)) + { + QMessageBox::critical(this, tr("Error"), tr("Failed to parse cheat file. The log may contain more information.")); + return; + } + + Host::RunOnCPUThread( + [&new_cheats]() { + DebugAssert(System::HasCheatList()); + System::GetCheatList()->MergeList(new_cheats); + System::SaveCheatList(); + }, + true); + updateCheatList(); +} + +void CheatManagerWindow::exportClicked() +{ + 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; + + if (!getCheatList()->SaveToPCSXRFile(filename.toUtf8().constData())) + QMessageBox::critical(this, tr("Error"), tr("Failed to save cheat file. The log may contain more information.")); +} + +void CheatManagerWindow::clearClicked() +{ + if (QMessageBox::question(this, tr("Confirm Clear"), + tr("Are you sure you want to remove all cheats? This is not reversible.")) != + QMessageBox::Yes) + { + return; + } + + Host::RunOnCPUThread([] { System::ClearCheatList(true); }, true); + updateCheatList(); +} + +void CheatManagerWindow::resetClicked() +{ + if (QMessageBox::question( + this, tr("Confirm Reset"), + tr( + "Are you sure you want to reset the cheat list? Any cheats not in the DuckStation database WILL BE LOST.")) != + QMessageBox::Yes) + { + return; + } + + Host::RunOnCPUThread([] { System::DeleteCheatList(); }, true); + updateCheatList(); +} diff --git a/src/duckstation-qt/cheatmanagerdialog.h b/src/duckstation-qt/cheatmanagerwindow.h similarity index 60% rename from src/duckstation-qt/cheatmanagerdialog.h rename to src/duckstation-qt/cheatmanagerwindow.h index fddd231dd..f82d3698d 100644 --- a/src/duckstation-qt/cheatmanagerdialog.h +++ b/src/duckstation-qt/cheatmanagerwindow.h @@ -1,32 +1,38 @@ -// SPDX-FileCopyrightText: 2019-2022 Connor McLaughlin +// SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin // SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0) #pragma once + +#include "ui_cheatmanagerwindow.h" + #include "core/cheats.h" -#include "ui_cheatmanagerdialog.h" + #include #include -#include #include #include #include +#include #include -class CheatManagerDialog : public QDialog +class CheatManagerWindow : public QWidget { Q_OBJECT public: - CheatManagerDialog(QWidget* parent); - ~CheatManagerDialog(); + CheatManagerWindow(); + ~CheatManagerWindow(); + +Q_SIGNALS: + void closed(); protected: void showEvent(QShowEvent* event); + void closeEvent(QCloseEvent* event); void resizeEvent(QResizeEvent* event); - -private Q_SLOTS: void resizeColumns(); +private Q_SLOTS: CheatList* getCheatList() const; void updateCheatList(); void saveCheatList(); @@ -47,29 +53,13 @@ private Q_SLOTS: void clearClicked(); void resetClicked(); - void addToWatchClicked(); - void addManualWatchAddressClicked(); - void removeWatchClicked(); - void scanCurrentItemChanged(QTableWidgetItem* current, QTableWidgetItem* previous); - void watchCurrentItemChanged(QTableWidgetItem* current, QTableWidgetItem* previous); - void scanItemChanged(QTableWidgetItem* item); - void watchItemChanged(QTableWidgetItem* item); - void updateScanValue(); - void updateScanUi(); - private: enum : int { MAX_DISPLAYED_SCAN_RESULTS = 5000 }; - void setupAdditionalUi(); void connectUi(); - void setUpdateTimerEnabled(bool enabled); - void updateResults(); - void updateResultsValues(); - void updateWatch(); - void updateWatchValues(); void fillItemForCheatCode(QTreeWidgetItem* item, u32 index, const CheatCode& code); QTreeWidgetItem* getItemForCheatIndex(u32 index) const; @@ -77,15 +67,8 @@ private: QTreeWidgetItem* createItemForCheatGroup(const QString& group_name) const; QStringList getCheatGroupNames() const; int getSelectedCheatIndex() const; - int getSelectedResultIndexFirst() const; - int getSelectedResultIndexLast() const; - int getSelectedWatchIndexFirst() const; - int getSelectedWatchIndexLast() const; - Ui::CheatManagerDialog m_ui; - - MemoryScan m_scanner; - MemoryWatchList m_watch; + Ui::CheatManagerWindow m_ui; QTimer* m_update_timer = nullptr; }; diff --git a/src/duckstation-qt/cheatmanagerdialog.ui b/src/duckstation-qt/cheatmanagerwindow.ui similarity index 83% rename from src/duckstation-qt/cheatmanagerdialog.ui rename to src/duckstation-qt/cheatmanagerwindow.ui index 233b48faa..f65435fa5 100644 --- a/src/duckstation-qt/cheatmanagerdialog.ui +++ b/src/duckstation-qt/cheatmanagerwindow.ui @@ -1,18 +1,22 @@ - CheatManagerDialog - + CheatManagerWindow + 0 0 - 1046 - 778 + 882 + 572 Cheat Manager + + + :/icons/duck.png:/icons/duck.png + @@ -101,7 +105,7 @@ - Qt::Horizontal + Qt::Orientation::Horizontal @@ -116,10 +120,10 @@ - QAbstractItemView::SingleSelection + QAbstractItemView::SelectionMode::SingleSelection - QAbstractItemView::SelectRows + QAbstractItemView::SelectionBehavior::SelectRows @@ -153,7 +157,7 @@ - Qt::Vertical + Qt::Orientation::Vertical @@ -163,10 +167,10 @@ true - QAbstractItemView::ContiguousSelection + QAbstractItemView::SelectionMode::ContiguousSelection - QAbstractItemView::SelectRows + QAbstractItemView::SelectionBehavior::SelectRows false @@ -466,85 +470,85 @@ - - - false - - - - 300 - 30 - - - - Qt::ImhNone - - - QFrame::NoFrame - - - Qt::ScrollBarAlwaysOff - - - Qt::ScrollBarAlwaysOff - - - true - - - Qt::NoTextInteraction - - - Number of Results (Display limited to first 5000) : - - - - - - - false - - - - 60 - 30 - - - - Qt::ImhNone - - - QFrame::NoFrame - - - Qt::ScrollBarAlwaysOff - - - Qt::ScrollBarAlwaysOff - - - true - - - false - - - Qt::NoTextInteraction - - - - - - 0 - - - - + + + false + + + + 300 + 30 + + + + Qt::InputMethodHint::ImhNone + + + QFrame::Shape::NoFrame + + + Qt::ScrollBarPolicy::ScrollBarAlwaysOff + + + Qt::ScrollBarPolicy::ScrollBarAlwaysOff + + + true + + + Qt::TextInteractionFlag::NoTextInteraction + + + Number of Results (Display limited to first 5000) : + + + + + + + false + + + + 60 + 30 + + + + Qt::InputMethodHint::ImhNone + + + QFrame::Shape::NoFrame + + + Qt::ScrollBarPolicy::ScrollBarAlwaysOff + + + Qt::ScrollBarPolicy::ScrollBarAlwaysOff + + + true + + + false + + + Qt::TextInteractionFlag::NoTextInteraction + + + + + + 0 + + + + - Qt::Vertical + Qt::Orientation::Vertical @@ -566,10 +570,10 @@ true - QAbstractItemView::ContiguousSelection + QAbstractItemView::SelectionMode::ContiguousSelection - QAbstractItemView::SelectRows + QAbstractItemView::SelectionBehavior::SelectRows false @@ -658,7 +662,7 @@ - Qt::Horizontal + Qt::Orientation::Horizontal diff --git a/src/duckstation-qt/duckstation-qt.vcxproj b/src/duckstation-qt/duckstation-qt.vcxproj index ebca67b57..441692259 100644 --- a/src/duckstation-qt/duckstation-qt.vcxproj +++ b/src/duckstation-qt/duckstation-qt.vcxproj @@ -9,7 +9,7 @@ - + @@ -30,6 +30,7 @@ + @@ -58,7 +59,7 @@ - + @@ -86,6 +87,7 @@ + @@ -141,7 +143,7 @@ Document - + Document @@ -224,7 +226,7 @@ - + @@ -255,6 +257,7 @@ + @@ -329,6 +332,9 @@ Document + + Document + diff --git a/src/duckstation-qt/duckstation-qt.vcxproj.filters b/src/duckstation-qt/duckstation-qt.vcxproj.filters index 7fd2885b4..a196a2e56 100644 --- a/src/duckstation-qt/duckstation-qt.vcxproj.filters +++ b/src/duckstation-qt/duckstation-qt.vcxproj.filters @@ -27,7 +27,7 @@ - + @@ -73,7 +73,7 @@ moc - + moc @@ -182,6 +182,10 @@ moc + + + moc + @@ -226,7 +230,7 @@ - + @@ -246,6 +250,7 @@ + @@ -261,7 +266,7 @@ - + @@ -287,6 +292,7 @@ + diff --git a/src/duckstation-qt/mainwindow.cpp b/src/duckstation-qt/mainwindow.cpp index 698e58e22..c137c41fd 100644 --- a/src/duckstation-qt/mainwindow.cpp +++ b/src/duckstation-qt/mainwindow.cpp @@ -5,7 +5,7 @@ #include "aboutdialog.h" #include "achievementlogindialog.h" #include "autoupdaterdialog.h" -#include "cheatmanagerdialog.h" +#include "cheatmanagerwindow.h" #include "coverdownloaddialog.h" #include "debuggerwindow.h" #include "displaywidget.h" @@ -14,6 +14,7 @@ #include "interfacesettingswidget.h" #include "logwindow.h" #include "memorycardeditorwindow.h" +#include "memoryscannerwindow.h" #include "qthost.h" #include "qtutils.h" #include "settingswindow.h" @@ -601,12 +602,6 @@ void MainWindow::onSystemDestroyed() // reload played time if (m_game_list_widget->isShowingGameList()) m_game_list_widget->refresh(false); - - if (m_cheat_manager_dialog) - { - delete m_cheat_manager_dialog; - m_cheat_manager_dialog = nullptr; - } } void MainWindow::onRunningGameChanged(const QString& filename, const QString& game_serial, const QString& game_title) @@ -752,33 +747,12 @@ void MainWindow::recreate() void MainWindow::destroySubWindows() { - if (m_debugger_window) - { - m_debugger_window->close(); - m_debugger_window->deleteLater(); - m_debugger_window = nullptr; - } - - if (m_memory_card_editor_window) - { - m_memory_card_editor_window->close(); - m_memory_card_editor_window->deleteLater(); - m_memory_card_editor_window = nullptr; - } - - if (m_controller_settings_window) - { - m_controller_settings_window->close(); - m_controller_settings_window->deleteLater(); - m_controller_settings_window = nullptr; - } - - if (m_settings_window) - { - m_settings_window->close(); - m_settings_window->deleteLater(); - m_settings_window = nullptr; - } + QtUtils::CloseAndDeleteWindow(m_memory_scanner_window); + QtUtils::CloseAndDeleteWindow(m_debugger_window); + QtUtils::CloseAndDeleteWindow(m_cheat_manager_window); + QtUtils::CloseAndDeleteWindow(m_memory_card_editor_window); + QtUtils::CloseAndDeleteWindow(m_controller_settings_window); + QtUtils::CloseAndDeleteWindow(m_settings_window); SettingsWindow::closeGamePropertiesDialogs(); @@ -1805,6 +1779,7 @@ void MainWindow::updateEmulationActions(bool starting, bool running, bool cheevo m_ui.menuChangeDisc->setDisabled(starting || !running); m_ui.menuCheats->setDisabled(starting || !running || cheevos_challenge_mode); m_ui.actionCPUDebugger->setDisabled(cheevos_challenge_mode); + m_ui.actionMemoryScanner->setDisabled(cheevos_challenge_mode); m_ui.actionDumpRAM->setDisabled(starting || !running || cheevos_challenge_mode); m_ui.actionDumpVRAM->setDisabled(starting || !running || cheevos_challenge_mode); m_ui.actionDumpSPURAM->setDisabled(starting || !running || cheevos_challenge_mode); @@ -2094,7 +2069,8 @@ void MainWindow::connectSignals() connect(m_ui.actionAboutQt, &QAction::triggered, qApp, &QApplication::aboutQt); connect(m_ui.actionAbout, &QAction::triggered, this, &MainWindow::onAboutActionTriggered); connect(m_ui.actionCheckForUpdates, &QAction::triggered, this, &MainWindow::onCheckForUpdatesActionTriggered); - connect(m_ui.actionMemory_Card_Editor, &QAction::triggered, this, &MainWindow::onToolsMemoryCardEditorTriggered); + connect(m_ui.actionMemoryCardEditor, &QAction::triggered, this, &MainWindow::onToolsMemoryCardEditorTriggered); + connect(m_ui.actionMemoryScanner, &QAction::triggered, this, &MainWindow::onToolsMemoryScannerTriggered); connect(m_ui.actionCoverDownloader, &QAction::triggered, this, &MainWindow::onToolsCoverDownloaderTriggered); connect(m_ui.actionCPUDebugger, &QAction::triggered, this, &MainWindow::openCPUDebugger); SettingWidgetBinder::BindWidgetToBoolSetting(nullptr, m_ui.actionEnableGDBServer, "Debug", "EnableGDBServer", false); @@ -2571,17 +2547,7 @@ SettingsWindow* MainWindow::getSettingsDialog() void MainWindow::doSettings(const char* category /* = nullptr */) { SettingsWindow* dlg = getSettingsDialog(); - if (!dlg->isVisible()) - { - dlg->show(); - } - else - { - dlg->raise(); - dlg->activateWindow(); - dlg->setFocus(); - } - + QtUtils::ShowOrRaiseWindow(dlg); if (category) dlg->setCategory(category); } @@ -2592,17 +2558,7 @@ void MainWindow::doControllerSettings( if (!m_controller_settings_window) m_controller_settings_window = new ControllerSettingsWindow(); - if (!m_controller_settings_window->isVisible()) - { - m_controller_settings_window->show(); - } - else - { - m_controller_settings_window->raise(); - m_controller_settings_window->activateWindow(); - m_controller_settings_window->setFocus(); - } - + QtUtils::ShowOrRaiseWindow(m_controller_settings_window); if (category != ControllerSettingsWindow::Category::Count) m_controller_settings_window->setCategory(category); } @@ -2933,16 +2889,7 @@ void MainWindow::openMemoryCardEditor(const QString& card_a_path, const QString& if (!m_memory_card_editor_window) m_memory_card_editor_window = new MemoryCardEditorWindow(); - if (!m_memory_card_editor_window->isVisible()) - { - m_memory_card_editor_window->show(); - } - else - { - m_memory_card_editor_window->raise(); - m_memory_card_editor_window->activateWindow(); - m_memory_card_editor_window->setFocus(); - } + QtUtils::ShowOrRaiseWindow(m_memory_card_editor_window); if (!card_a_path.isEmpty()) { @@ -2987,19 +2934,9 @@ void MainWindow::onAchievementsChallengeModeChanged(bool enabled) { if (enabled) { - if (m_cheat_manager_dialog) - { - m_cheat_manager_dialog->close(); - delete m_cheat_manager_dialog; - m_cheat_manager_dialog = nullptr; - } - - if (m_debugger_window) - { - m_debugger_window->close(); - delete m_debugger_window; - m_debugger_window = nullptr; - } + QtUtils::CloseAndDeleteWindow(m_cheat_manager_window); + QtUtils::CloseAndDeleteWindow(m_debugger_window); + QtUtils::CloseAndDeleteWindow(m_memory_scanner_window); } updateEmulationActions(false, System::IsValid(), enabled); @@ -3019,44 +2956,52 @@ void MainWindow::onToolsCoverDownloaderTriggered() dlg.exec(); } +void MainWindow::onToolsMemoryScannerTriggered() +{ + if (Achievements::IsHardcoreModeActive()) + return; + + if (!m_memory_scanner_window) + { + m_memory_scanner_window = new MemoryScannerWindow(); + connect(m_memory_scanner_window, &MemoryScannerWindow::closed, this, [this]() { + m_memory_scanner_window->deleteLater(); + m_memory_scanner_window = nullptr; + }); + } + + QtUtils::ShowOrRaiseWindow(m_memory_scanner_window); +} + void MainWindow::openCheatManager() { - if (!m_cheat_manager_dialog) - m_cheat_manager_dialog = new CheatManagerDialog(this); + if (Achievements::IsHardcoreModeActive()) + return; - if (!m_cheat_manager_dialog->isVisible()) + if (!m_cheat_manager_window) { - m_cheat_manager_dialog->show(); - } - else - { - m_cheat_manager_dialog->raise(); - m_cheat_manager_dialog->activateWindow(); - m_cheat_manager_dialog->setFocus(); + m_cheat_manager_window = new CheatManagerWindow(); + connect(m_cheat_manager_window, &CheatManagerWindow::closed, this, [this]() { + m_cheat_manager_window->deleteLater(); + m_cheat_manager_window = nullptr; + }); } + + QtUtils::ShowOrRaiseWindow(m_cheat_manager_window); } void MainWindow::openCPUDebugger() { - if (m_debugger_window) + if (!m_debugger_window) { - m_debugger_window->raise(); - m_debugger_window->activateWindow(); - m_debugger_window->setFocus(); - return; + m_debugger_window = new DebuggerWindow(); + connect(m_debugger_window, &DebuggerWindow::closed, this, [this]() { + m_debugger_window->deleteLater(); + m_debugger_window = nullptr; + }); } - Assert(!m_debugger_window); - m_debugger_window = new DebuggerWindow(); - connect(m_debugger_window, &DebuggerWindow::closed, this, &MainWindow::onCPUDebuggerClosed); - m_debugger_window->show(); -} - -void MainWindow::onCPUDebuggerClosed() -{ - Assert(m_debugger_window); - m_debugger_window->deleteLater(); - m_debugger_window = nullptr; + QtUtils::ShowOrRaiseWindow(m_debugger_window); } void MainWindow::onToolsOpenDataDirectoryTriggered() @@ -3067,13 +3012,9 @@ void MainWindow::onToolsOpenDataDirectoryTriggered() void MainWindow::onSettingsTriggeredFromToolbar() { if (s_system_valid) - { m_settings_toolbar_menu->exec(QCursor::pos()); - } else - { doSettings(); - } } void MainWindow::checkForUpdates(bool display_message) diff --git a/src/duckstation-qt/mainwindow.h b/src/duckstation-qt/mainwindow.h index 8bb399092..039879269 100644 --- a/src/duckstation-qt/mainwindow.h +++ b/src/duckstation-qt/mainwindow.h @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2019-2022 Connor McLaughlin +// SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin // SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0) #pragma once @@ -24,13 +24,14 @@ class QLabel; class QThread; class QProgressBar; +class MainWindow; class GameListWidget; class EmuThread; class AutoUpdaterDialog; class MemoryCardEditorWindow; -class CheatManagerDialog; +class CheatManagerWindow; class DebuggerWindow; -class MainWindow; +class MemoryScannerWindow; class GPUDevice; namespace Achievements { @@ -96,6 +97,9 @@ public: ALWAYS_INLINE QLabel* getStatusFPSWidget() const { return m_status_fps_widget; } ALWAYS_INLINE QLabel* getStatusVPSWidget() const { return m_status_vps_widget; } + /// Accessors for child windows. + CheatManagerWindow* getCheatManagerWindow() const { return m_cheat_manager_window; } + public Q_SLOTS: /// Updates debug menu visibility (hides if disabled). void updateDebugMenuVisibility(); @@ -166,6 +170,7 @@ private Q_SLOTS: void onAboutActionTriggered(); void onCheckForUpdatesActionTriggered(); void onToolsMemoryCardEditorTriggered(); + void onToolsMemoryScannerTriggered(); void onToolsCoverDownloaderTriggered(); void onToolsOpenDataDirectoryTriggered(); void onSettingsTriggeredFromToolbar(); @@ -180,7 +185,6 @@ private Q_SLOTS: void openCheatManager(); void openCPUDebugger(); - void onCPUDebuggerClosed(); protected: void showEvent(QShowEvent* event) override; @@ -290,8 +294,9 @@ private: AutoUpdaterDialog* m_auto_updater_dialog = nullptr; MemoryCardEditorWindow* m_memory_card_editor_window = nullptr; - CheatManagerDialog* m_cheat_manager_dialog = nullptr; + CheatManagerWindow* m_cheat_manager_window = nullptr; DebuggerWindow* m_debugger_window = nullptr; + MemoryScannerWindow* m_memory_scanner_window = nullptr; bool m_was_paused_by_focus_loss = false; bool m_open_debugger_on_start = false; diff --git a/src/duckstation-qt/mainwindow.ui b/src/duckstation-qt/mainwindow.ui index 7f7a60f66..b50aa88d8 100644 --- a/src/duckstation-qt/mainwindow.ui +++ b/src/duckstation-qt/mainwindow.ui @@ -30,7 +30,7 @@ 0 0 800 - 21 + 33 @@ -42,8 +42,7 @@ Change Disc - - .. + @@ -57,8 +56,7 @@ Cheats - - .. + @@ -66,8 +64,7 @@ Load State - - .. + @@ -75,8 +72,7 @@ Save State - - .. + @@ -109,8 +105,7 @@ Theme - - .. + @@ -118,8 +113,7 @@ Language - - .. + @@ -157,7 +151,7 @@ - + @@ -235,9 +229,11 @@ &Tools - + + + @@ -258,7 +254,7 @@ - Qt::ToolButtonTextUnderIcon + Qt::ToolButtonStyle::ToolButtonTextUnderIcon TopToolBarArea @@ -287,8 +283,7 @@ - - .. + Start &File... @@ -296,8 +291,7 @@ - - .. + Start &Disc... @@ -305,8 +299,7 @@ - - .. + Start &BIOS @@ -314,8 +307,7 @@ - - .. + &Scan For New Games @@ -323,8 +315,7 @@ - - .. + &Rescan All Games @@ -332,8 +323,7 @@ - - .. + Power &Off @@ -341,8 +331,7 @@ - - .. + &Reset @@ -353,8 +342,7 @@ true - - .. + &Pause @@ -362,8 +350,7 @@ - - .. + &Load State @@ -371,8 +358,7 @@ - - .. + &Save State @@ -380,8 +366,7 @@ - - .. + E&xit @@ -389,8 +374,7 @@ - - .. + B&IOS @@ -398,8 +382,7 @@ - - .. + C&onsole @@ -407,8 +390,7 @@ - - .. + E&mulation @@ -416,8 +398,7 @@ - - .. + &Controllers @@ -425,8 +406,7 @@ - - .. + &Hotkeys @@ -434,8 +414,7 @@ - - .. + &Graphics @@ -443,8 +422,7 @@ - - .. + &Post-Processing @@ -452,8 +430,7 @@ - - .. + Fullscreen @@ -493,8 +470,7 @@ - - .. + Check for &Updates... @@ -525,8 +501,7 @@ - - .. + Change Disc... @@ -534,8 +509,7 @@ - - .. + Cheats... @@ -543,8 +517,7 @@ - - .. + Cheats @@ -552,8 +525,7 @@ - - .. + Audio @@ -561,8 +533,7 @@ - - .. + Achievements @@ -570,8 +541,7 @@ - - .. + Folders @@ -579,8 +549,7 @@ - - .. + Game List @@ -588,8 +557,7 @@ - - .. + &Interface @@ -597,8 +565,7 @@ - - .. + Advanced @@ -606,8 +573,7 @@ - - .. + Add Game Directory... @@ -615,26 +581,24 @@ - - .. + &Settings - QAction::PreferencesRole + QAction::MenuRole::PreferencesRole - - .. + &Settings - QAction::PreferencesRole + QAction::MenuRole::PreferencesRole @@ -788,8 +752,7 @@ - - .. + &Screenshot @@ -797,8 +760,7 @@ - - .. + &Memory Cards @@ -806,8 +768,7 @@ - - .. + Resume @@ -851,8 +812,7 @@ - - .. + Game &List @@ -871,14 +831,13 @@ false - - .. + Game &Properties - + Memory &Card Editor @@ -898,8 +857,7 @@ - - .. + Game &Grid @@ -949,8 +907,7 @@ - - .. + Power Off &Without Saving @@ -958,8 +915,7 @@ - - .. + Start Big Picture Mode @@ -967,8 +923,7 @@ - - .. + Big Picture @@ -979,6 +934,11 @@ Cover Downloader + + + Memory &Scanner + + diff --git a/src/duckstation-qt/memoryscannerwindow.cpp b/src/duckstation-qt/memoryscannerwindow.cpp new file mode 100644 index 000000000..f4437cf0b --- /dev/null +++ b/src/duckstation-qt/memoryscannerwindow.cpp @@ -0,0 +1,592 @@ +// SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin and contributors. +// SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0) + +#include "memoryscannerwindow.h" +#include "cheatcodeeditordialog.h" +#include "common/assert.h" +#include "common/string_util.h" +#include "core/bus.h" +#include "core/cpu_core.h" +#include "core/host.h" +#include "core/system.h" +#include "qthost.h" +#include "qtutils.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static constexpr std::array s_size_strings = { + {TRANSLATE_NOOP("MemoryScannerWindow", "Byte"), TRANSLATE_NOOP("MemoryScannerWindow", "Halfword"), + TRANSLATE_NOOP("MemoryScannerWindow", "Word"), TRANSLATE_NOOP("MemoryScannerWindow", "Signed Byte"), + TRANSLATE_NOOP("MemoryScannerWindow", "Signed Halfword"), TRANSLATE_NOOP("MemoryScannerWindow", "Signed Word")}}; + +static QString formatHexValue(u32 value, u8 size) +{ + return QStringLiteral("0x%1").arg(static_cast(value), size, 16, QChar('0')); +} + +static QString formatHexAndDecValue(u32 value, u8 size, bool is_signed) +{ + + if (is_signed) + { + u32 value_raw = value; + if (size == 2) + value_raw &= 0xFF; + else if (size == 4) + value_raw &= 0xFFFF; + return QStringLiteral("0x%1 (%2)") + .arg(static_cast(value_raw), size, 16, QChar('0')) + .arg(static_cast(value)); + } + else + return QStringLiteral("0x%1 (%2)").arg(static_cast(value), size, 16, QChar('0')).arg(static_cast(value)); +} + +static QString formatCheatCode(u32 address, u32 value, const MemoryAccessSize size) +{ + + if (size == MemoryAccessSize::Byte && address <= 0x00200000) + return QStringLiteral("CHEAT CODE: %1 %2") + .arg(static_cast(address) + 0x30000000, 8, 16, QChar('0')) + .toUpper() + .arg(static_cast(value), 4, 16, QChar('0')) + .toUpper(); + else if (size == MemoryAccessSize::HalfWord && address <= 0x001FFFFE) + return QStringLiteral("CHEAT CODE: %1 %2") + .arg(static_cast(address) + 0x80000000, 8, 16, QChar('0')) + .toUpper() + .arg(static_cast(value), 4, 16, QChar('0')) + .toUpper(); + else if (size == MemoryAccessSize::Word && address <= 0x001FFFFC) + return QStringLiteral("CHEAT CODE: %1 %2") + .arg(static_cast(address) + 0x90000000, 8, 16, QChar('0')) + .toUpper() + .arg(static_cast(value), 8, 16, QChar('0')) + .toUpper(); + else + return QStringLiteral("OUTSIDE RAM RANGE. POKE %1 with %2") + .arg(static_cast(address), 8, 16, QChar('0')) + .toUpper() + .arg(static_cast(value), 8, 16, QChar('0')) + .toUpper(); +} + +static QString formatValue(u32 value, bool is_signed) +{ + if (is_signed) + return QString::number(static_cast(value)); + else + return QString::number(static_cast(value)); +} + +MemoryScannerWindow::MemoryScannerWindow() : QWidget() +{ + m_ui.setupUi(this); + connectUi(); +} + +MemoryScannerWindow::~MemoryScannerWindow() = default; + +void MemoryScannerWindow::connectUi() +{ + m_ui.scanStartAddress->setText(formatHexValue(m_scanner.GetStartAddress(), 8)); + m_ui.scanEndAddress->setText(formatHexValue(m_scanner.GetEndAddress(), 8)); + + connect(m_ui.scanValue, &QLineEdit::textChanged, this, &MemoryScannerWindow::updateScanValue); + connect(m_ui.scanValueBase, QOverload::of(&QComboBox::currentIndexChanged), + [this](int index) { updateScanValue(); }); + connect(m_ui.scanSize, QOverload::of(&QComboBox::currentIndexChanged), [this](int index) { + m_scanner.SetSize(static_cast(index)); + m_scanner.ResetSearch(); + updateResults(); + }); + connect(m_ui.scanValueSigned, QOverload::of(&QComboBox::currentIndexChanged), [this](int index) { + m_scanner.SetValueSigned(index == 0); + m_scanner.ResetSearch(); + updateResults(); + }); + connect(m_ui.scanOperator, QOverload::of(&QComboBox::currentIndexChanged), + [this](int index) { m_scanner.SetOperator(static_cast(index)); }); + connect(m_ui.scanStartAddress, &QLineEdit::textChanged, [this](const QString& value) { + uint address; + if (value.startsWith(QStringLiteral("0x")) && value.length() > 2) + address = value.mid(2).toUInt(nullptr, 16); + else + address = value.toUInt(nullptr, 16); + m_scanner.SetStartAddress(static_cast(address)); + }); + connect(m_ui.scanEndAddress, &QLineEdit::textChanged, [this](const QString& value) { + uint address; + if (value.startsWith(QStringLiteral("0x")) && value.length() > 2) + address = value.mid(2).toUInt(nullptr, 16); + else + address = value.toUInt(nullptr, 16); + m_scanner.SetEndAddress(static_cast(address)); + }); + connect(m_ui.scanPresetRange, QOverload::of(&QComboBox::currentIndexChanged), [this](int index) { + if (index == 0) + { + m_ui.scanStartAddress->setText(formatHexValue(0, 8)); + m_ui.scanEndAddress->setText(formatHexValue(Bus::g_ram_size, 8)); + } + else if (index == 1) + { + m_ui.scanStartAddress->setText(formatHexValue(CPU::SCRATCHPAD_ADDR, 8)); + m_ui.scanEndAddress->setText(formatHexValue(CPU::SCRATCHPAD_ADDR + CPU::SCRATCHPAD_SIZE, 8)); + } + else + { + m_ui.scanStartAddress->setText(formatHexValue(Bus::BIOS_BASE, 8)); + m_ui.scanEndAddress->setText(formatHexValue(Bus::BIOS_BASE + Bus::BIOS_SIZE, 8)); + } + }); + connect(m_ui.scanNewSearch, &QPushButton::clicked, [this]() { + m_scanner.Search(); + updateResults(); + }); + connect(m_ui.scanSearchAgain, &QPushButton::clicked, [this]() { + m_scanner.SearchAgain(); + updateResults(); + }); + connect(m_ui.scanResetSearch, &QPushButton::clicked, [this]() { + m_scanner.ResetSearch(); + updateResults(); + }); + connect(m_ui.scanAddWatch, &QPushButton::clicked, this, &MemoryScannerWindow::addToWatchClicked); + connect(m_ui.scanAddManualAddress, &QPushButton::clicked, this, &MemoryScannerWindow::addManualWatchAddressClicked); + connect(m_ui.scanRemoveWatch, &QPushButton::clicked, this, &MemoryScannerWindow::removeWatchClicked); + connect(m_ui.scanTable, &QTableWidget::currentItemChanged, this, &MemoryScannerWindow::scanCurrentItemChanged); + connect(m_ui.watchTable, &QTableWidget::currentItemChanged, this, &MemoryScannerWindow::watchCurrentItemChanged); + connect(m_ui.scanTable, &QTableWidget::itemChanged, this, &MemoryScannerWindow::scanItemChanged); + connect(m_ui.watchTable, &QTableWidget::itemChanged, this, &MemoryScannerWindow::watchItemChanged); + + m_update_timer = new QTimer(this); + connect(m_update_timer, &QTimer::timeout, this, &MemoryScannerWindow::updateScanUi); + + connect(g_emu_thread, &EmuThread::systemStarted, this, &MemoryScannerWindow::onSystemStarted); + connect(g_emu_thread, &EmuThread::systemDestroyed, this, &MemoryScannerWindow::onSystemDestroyed); + + if (QtHost::IsSystemValid()) + onSystemStarted(); + else + enableUi(false); +} + +void MemoryScannerWindow::enableUi(bool enabled) +{ + const bool has_results = (m_scanner.GetResultCount() > 0); + + m_ui.scanValue->setEnabled(enabled); + m_ui.scanValueBase->setEnabled(enabled); + m_ui.scanValueSigned->setEnabled(enabled); + m_ui.scanSize->setEnabled(enabled); + m_ui.scanOperator->setEnabled(enabled); + m_ui.scanStartAddress->setEnabled(enabled); + m_ui.scanEndAddress->setEnabled(enabled); + m_ui.scanPresetRange->setEnabled(enabled); + m_ui.scanResultCount->setEnabled(enabled); + m_ui.scanNewSearch->setEnabled(enabled); + m_ui.scanSearchAgain->setEnabled(enabled && has_results); + m_ui.scanResetSearch->setEnabled(enabled && has_results); + m_ui.scanAddWatch->setEnabled(enabled && !m_ui.scanTable->selectedItems().empty()); + m_ui.watchTable->setEnabled(enabled); + m_ui.scanAddManualAddress->setEnabled(enabled); + m_ui.scanRemoveWatch->setEnabled(enabled && !m_ui.watchTable->selectedItems().empty()); +} + +void MemoryScannerWindow::showEvent(QShowEvent* event) +{ + QWidget::showEvent(event); + resizeColumns(); +} + +void MemoryScannerWindow::closeEvent(QCloseEvent* event) +{ + QWidget::closeEvent(event); + emit closed(); +} + +void MemoryScannerWindow::resizeEvent(QResizeEvent* event) +{ + QWidget::resizeEvent(event); + resizeColumns(); +} + +void MemoryScannerWindow::resizeColumns() +{ + QtUtils::ResizeColumnsForTableView(m_ui.scanTable, {-1, 130, 130}); + QtUtils::ResizeColumnsForTableView(m_ui.watchTable, {-1, 100, 100, 100, 40}); +} + +int MemoryScannerWindow::getSelectedResultIndexFirst() const +{ + QList sel = m_ui.scanTable->selectedRanges(); + if (sel.isEmpty()) + return -1; + + return sel.front().topRow(); +} + +int MemoryScannerWindow::getSelectedResultIndexLast() const +{ + QList sel = m_ui.scanTable->selectedRanges(); + if (sel.isEmpty()) + return -1; + + return sel.front().bottomRow(); +} + +int MemoryScannerWindow::getSelectedWatchIndexFirst() const +{ + QList sel = m_ui.watchTable->selectedRanges(); + if (sel.isEmpty()) + return -1; + + return sel.front().topRow(); +} + +int MemoryScannerWindow::getSelectedWatchIndexLast() const +{ + QList sel = m_ui.watchTable->selectedRanges(); + if (sel.isEmpty()) + return -1; + + return sel.front().bottomRow(); +} + +void MemoryScannerWindow::onSystemStarted() +{ + if (!m_update_timer->isActive()) + m_update_timer->start(SCAN_INTERVAL); + + enableUi(true); +} + +void MemoryScannerWindow::onSystemDestroyed() +{ + if (m_update_timer->isActive()) + m_update_timer->stop(); + + enableUi(false); +} + +void MemoryScannerWindow::addToWatchClicked() +{ + const int indexFirst = getSelectedResultIndexFirst(); + const int indexLast = getSelectedResultIndexLast(); + if (indexFirst < 0) + return; + + for (int index = indexFirst; index <= indexLast; index++) + { + const MemoryScan::Result& res = m_scanner.GetResults()[static_cast(index)]; + m_watch.AddEntry(fmt::format("0x{:08x}", res.address), res.address, m_scanner.GetSize(), m_scanner.GetValueSigned(), + false); + updateWatch(); + } +} + +void MemoryScannerWindow::addManualWatchAddressClicked() +{ + std::optional address = QtUtils::PromptForAddress(this, windowTitle(), tr("Enter manual address:"), false); + if (!address.has_value()) + return; + + QStringList items; + for (const char* title : s_size_strings) + items.append(tr(title)); + + bool ok = false; + QString selected_item(QInputDialog::getItem(this, windowTitle(), tr("Select data size:"), items, 0, false, &ok)); + int index = items.indexOf(selected_item); + if (index < 0 || !ok) + return; + + if (index == 1 || index == 4) + address.value() &= 0xFFFFFFFE; + else if (index == 2 || index == 5) + address.value() &= 0xFFFFFFFC; + + m_watch.AddEntry(fmt::format("0x{:08x}", address.value()), address.value(), static_cast(index % 3), + (index > 3), false); + updateWatch(); +} + +void MemoryScannerWindow::removeWatchClicked() +{ + const int indexFirst = getSelectedWatchIndexFirst(); + const int indexLast = getSelectedWatchIndexLast(); + if (indexFirst < 0) + return; + + for (int index = indexLast; index >= indexFirst; index--) + { + m_watch.RemoveEntry(static_cast(index)); + updateWatch(); + } +} + +void MemoryScannerWindow::scanCurrentItemChanged(QTableWidgetItem* current, QTableWidgetItem* previous) +{ + m_ui.scanAddWatch->setEnabled((current != nullptr)); +} + +void MemoryScannerWindow::watchCurrentItemChanged(QTableWidgetItem* current, QTableWidgetItem* previous) +{ + m_ui.scanRemoveWatch->setEnabled((current != nullptr)); +} + +void MemoryScannerWindow::scanItemChanged(QTableWidgetItem* item) +{ + const u32 index = static_cast(item->row()); + switch (item->column()) + { + case 1: + { + bool value_ok = false; + if (m_scanner.GetValueSigned()) + { + int value = item->text().toInt(&value_ok); + if (value_ok) + m_scanner.SetResultValue(index, static_cast(value)); + } + else + { + uint value = item->text().toUInt(&value_ok); + if (value_ok) + m_scanner.SetResultValue(index, static_cast(value)); + } + } + break; + + default: + break; + } +} + +void MemoryScannerWindow::watchItemChanged(QTableWidgetItem* item) +{ + const u32 index = static_cast(item->row()); + if (index >= m_watch.GetEntryCount()) + return; + + switch (item->column()) + { + case 4: + { + m_watch.SetEntryFreeze(index, (item->checkState() == Qt::Checked)); + } + break; + + case 0: + { + m_watch.SetEntryDescription(index, item->text().toStdString()); + } + break; + + case 3: + { + const MemoryWatchList::Entry& entry = m_watch.GetEntry(index); + bool value_ok = false; + if (entry.is_signed) + { + int value = item->text().toInt(&value_ok); + if (value_ok) + m_watch.SetEntryValue(index, static_cast(value)); + } + else + { + uint value; + if (item->text()[1] == 'x' || item->text()[1] == 'X') + value = item->text().toUInt(&value_ok, 16); + else + value = item->text().toUInt(&value_ok); + if (value_ok) + m_watch.SetEntryValue(index, static_cast(value)); + } + } + break; + + default: + break; + } +} + +void MemoryScannerWindow::updateScanValue() +{ + QString value = m_ui.scanValue->text(); + if (value.startsWith(QStringLiteral("0x"))) + value.remove(0, 2); + + bool ok = false; + uint uint_value = value.toUInt(&ok, (m_ui.scanValueBase->currentIndex() > 0) ? 16 : 10); + if (ok) + m_scanner.SetValue(uint_value); +} + +void MemoryScannerWindow::updateResults() +{ + QSignalBlocker sb(m_ui.scanTable); + m_ui.scanTable->setRowCount(0); + + int row = 0; + const MemoryScan::ResultVector& results = m_scanner.GetResults(); + for (const MemoryScan::Result& res : results) + { + if (row == MAX_DISPLAYED_SCAN_RESULTS) + break; + + m_ui.scanTable->insertRow(row); + + QTableWidgetItem* address_item = new QTableWidgetItem(formatHexValue(res.address, 8)); + address_item->setFlags(address_item->flags() & ~(Qt::ItemIsEditable)); + m_ui.scanTable->setItem(row, 0, address_item); + + QTableWidgetItem* value_item; + if (m_ui.scanValueBase->currentIndex() == 0) + value_item = new QTableWidgetItem(formatValue(res.value, m_scanner.GetValueSigned())); + else if (m_scanner.GetSize() == MemoryAccessSize::Byte) + value_item = new QTableWidgetItem(formatHexValue(res.value, 2)); + else if (m_scanner.GetSize() == MemoryAccessSize::HalfWord) + value_item = new QTableWidgetItem(formatHexValue(res.value, 4)); + else + value_item = new QTableWidgetItem(formatHexValue(res.value, 8)); + m_ui.scanTable->setItem(row, 1, value_item); + + QTableWidgetItem* previous_item; + if (m_ui.scanValueBase->currentIndex() == 0) + previous_item = new QTableWidgetItem(formatValue(res.last_value, m_scanner.GetValueSigned())); + else if (m_scanner.GetSize() == MemoryAccessSize::Byte) + previous_item = new QTableWidgetItem(formatHexValue(res.last_value, 2)); + else if (m_scanner.GetSize() == MemoryAccessSize::HalfWord) + previous_item = new QTableWidgetItem(formatHexValue(res.last_value, 4)); + else + previous_item = new QTableWidgetItem(formatHexValue(res.last_value, 8)); + + previous_item->setFlags(address_item->flags() & ~(Qt::ItemIsEditable)); + m_ui.scanTable->setItem(row, 2, previous_item); + row++; + } + + m_ui.scanResultCount->setText((row < static_cast(results.size())) ? + tr("%1 (only showing first %2)").arg(results.size()).arg(row) : + QString::number(m_scanner.GetResultCount())); + + m_ui.scanResetSearch->setEnabled(!results.empty()); + m_ui.scanSearchAgain->setEnabled(!results.empty()); + m_ui.scanAddWatch->setEnabled(false); +} + +void MemoryScannerWindow::updateResultsValues() +{ + QSignalBlocker sb(m_ui.scanTable); + + int row = 0; + for (const MemoryScan::Result& res : m_scanner.GetResults()) + { + if (res.value_changed) + { + QTableWidgetItem* item = m_ui.scanTable->item(row, 1); + if (m_ui.scanValueBase->currentIndex() == 0) + item->setText(formatValue(res.value, m_scanner.GetValueSigned())); + else if (m_scanner.GetSize() == MemoryAccessSize::Byte) + item->setText(formatHexValue(res.value, 2)); + else if (m_scanner.GetSize() == MemoryAccessSize::HalfWord) + item->setText(formatHexValue(res.value, 4)); + else + item->setText(formatHexValue(res.value, 8)); + item->setForeground(Qt::red); + } + + row++; + if (row == MAX_DISPLAYED_SCAN_RESULTS) + break; + } +} + +void MemoryScannerWindow::updateWatch() +{ + m_watch.UpdateValues(); + + QSignalBlocker sb(m_ui.watchTable); + m_ui.watchTable->setRowCount(0); + + const MemoryWatchList::EntryVector& entries = m_watch.GetEntries(); + if (!entries.empty()) + { + int row = 0; + for (const MemoryWatchList::Entry& res : entries) + { + m_ui.watchTable->insertRow(row); + + QTableWidgetItem* description_item = new QTableWidgetItem(formatCheatCode(res.address, res.value, res.size)); + m_ui.watchTable->setItem(row, 0, description_item); + + QTableWidgetItem* address_item = new QTableWidgetItem(formatHexValue(res.address, 8)); + address_item->setFlags(address_item->flags() & ~(Qt::ItemIsEditable)); + m_ui.watchTable->setItem(row, 1, address_item); + + QTableWidgetItem* size_item = + new QTableWidgetItem(tr(s_size_strings[static_cast(res.size) + (res.is_signed ? 3 : 0)])); + size_item->setFlags(address_item->flags() & ~(Qt::ItemIsEditable)); + m_ui.watchTable->setItem(row, 2, size_item); + + QTableWidgetItem* value_item; + if (res.size == MemoryAccessSize::Byte) + value_item = new QTableWidgetItem(formatHexAndDecValue(res.value, 2, res.is_signed)); + else if (res.size == MemoryAccessSize::HalfWord) + value_item = new QTableWidgetItem(formatHexAndDecValue(res.value, 4, res.is_signed)); + else + value_item = new QTableWidgetItem(formatHexAndDecValue(res.value, 8, res.is_signed)); + + m_ui.watchTable->setItem(row, 3, value_item); + + QTableWidgetItem* freeze_item = new QTableWidgetItem(); + freeze_item->setFlags(freeze_item->flags() | (Qt::ItemIsEditable | Qt::ItemIsUserCheckable)); + freeze_item->setCheckState(res.freeze ? Qt::Checked : Qt::Unchecked); + m_ui.watchTable->setItem(row, 4, freeze_item); + + row++; + } + } + + m_ui.scanSaveWatch->setEnabled(!entries.empty()); + m_ui.scanRemoveWatch->setEnabled(false); +} + +void MemoryScannerWindow::updateWatchValues() +{ + QSignalBlocker sb(m_ui.watchTable); + int row = 0; + for (const MemoryWatchList::Entry& res : m_watch.GetEntries()) + { + if (res.changed) + { + if (m_ui.scanValueBase->currentIndex() == 0) + m_ui.watchTable->item(row, 3)->setText(formatValue(res.value, res.is_signed)); + else if (m_scanner.GetSize() == MemoryAccessSize::Byte) + m_ui.watchTable->item(row, 3)->setText(formatHexValue(res.value, 2)); + else if (m_scanner.GetSize() == MemoryAccessSize::HalfWord) + m_ui.watchTable->item(row, 3)->setText(formatHexValue(res.value, 4)); + else + m_ui.watchTable->item(row, 3)->setText(formatHexValue(res.value, 8)); + } + row++; + } +} + +void MemoryScannerWindow::updateScanUi() +{ + m_scanner.UpdateResultsValues(); + m_watch.UpdateValues(); + + updateResultsValues(); + updateWatchValues(); +} diff --git a/src/duckstation-qt/memoryscannerwindow.h b/src/duckstation-qt/memoryscannerwindow.h new file mode 100644 index 000000000..282ff70f2 --- /dev/null +++ b/src/duckstation-qt/memoryscannerwindow.h @@ -0,0 +1,74 @@ +// SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin +// SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0) + +#pragma once + +#include "ui_memoryscannerwindow.h" + +#include "core/cheats.h" + +#include +#include +#include +#include +#include +#include +#include + +class MemoryScannerWindow : public QWidget +{ + Q_OBJECT + +public: + MemoryScannerWindow(); + ~MemoryScannerWindow(); + +Q_SIGNALS: + void closed(); + +protected: + void showEvent(QShowEvent* event); + void closeEvent(QCloseEvent* event); + void resizeEvent(QResizeEvent* event); + +private Q_SLOTS: + void onSystemStarted(); + void onSystemDestroyed(); + + void addToWatchClicked(); + void addManualWatchAddressClicked(); + void removeWatchClicked(); + void scanCurrentItemChanged(QTableWidgetItem* current, QTableWidgetItem* previous); + void watchCurrentItemChanged(QTableWidgetItem* current, QTableWidgetItem* previous); + void scanItemChanged(QTableWidgetItem* item); + void watchItemChanged(QTableWidgetItem* item); + void updateScanValue(); + void updateScanUi(); + +private: + enum : int + { + MAX_DISPLAYED_SCAN_RESULTS = 5000, + SCAN_INTERVAL = 100, + }; + + void connectUi(); + void enableUi(bool enabled); + void resizeColumns(); + void updateResults(); + void updateResultsValues(); + void updateWatch(); + void updateWatchValues(); + + int getSelectedResultIndexFirst() const; + int getSelectedResultIndexLast() const; + int getSelectedWatchIndexFirst() const; + int getSelectedWatchIndexLast() const; + + Ui::MemoryScannerWindow m_ui; + + MemoryScan m_scanner; + MemoryWatchList m_watch; + + QTimer* m_update_timer = nullptr; +}; diff --git a/src/duckstation-qt/memoryscannerwindow.ui b/src/duckstation-qt/memoryscannerwindow.ui new file mode 100644 index 000000000..b9a201f86 --- /dev/null +++ b/src/duckstation-qt/memoryscannerwindow.ui @@ -0,0 +1,470 @@ + + + MemoryScannerWindow + + + + 0 + 0 + 833 + 610 + + + + Memory Scanner + + + + :/icons/duck.png:/icons/duck.png + + + + + + true + + + QAbstractItemView::SelectionMode::ContiguousSelection + + + QAbstractItemView::SelectionBehavior::SelectRows + + + false + + + true + + + false + + + false + + + + Address + + + + + Value + + + + + Previous Value + + + + + + + + Search Parameters + + + + + + Value: + + + + + + + + + + + + 1 + + + + Signed + + + + + Unsigned + + + + + + + + 1 + + + + Decimal + + + + + Hex + + + + + + + + + + Data Size: + + + + + + + 1 + + + + Byte (1 byte) + + + + + Halfword (2 bytes) + + + + + Word (4 bytes) + + + + + + + + Operator: + + + + + + + + Equal to... + + + + + Not Equal to... + + + + + Greater Than... + + + + + Greater or Equal... + + + + + Less Than... + + + + + Less or Equal... + + + + + Increased By... + + + + + Decreased By... + + + + + Changed By... + + + + + Equal to Previous (Unchanged Value) + + + + + Not Equal to Previous (Changed Value) + + + + + Greater Than Previous + + + + + Greater or Equal to Previous + + + + + Less Than Previous + + + + + Less or Equal to Previous + + + + + Any Value + + + + + + + + Start Address: + + + + + + + + + + End Address: + + + + + + + + + + Preset Range: + + + + + + + + RAM + + + + + Scratchpad + + + + + BIOS + + + + + + + + Result Count: + + + + + + + 0 + + + + + + + + + New Search + + + + + + + false + + + Search Again + + + + + + + false + + + Clear Results + + + + + + + + + false + + + Add Selected Results To Watch List + + + + + + + Qt::Orientation::Vertical + + + + 20 + 40 + + + + + + + + + + + + + true + + + QAbstractItemView::SelectionMode::ContiguousSelection + + + QAbstractItemView::SelectionBehavior::SelectRows + + + false + + + true + + + false + + + false + + + + Simple Cheat Code or Description + + + + + Address + + + + + Type + + + + + Value + + + + + Freeze + + + + + + + + + + false + + + false + + + Load Watch + + + + + + + false + + + false + + + Save Watch + + + + + + + Add Manual Address + + + + + + + false + + + Remove Selected Entries from Watch List + + + + + + + Qt::Orientation::Horizontal + + + + 40 + 20 + + + + + + + + + + + + + diff --git a/src/duckstation-qt/qtutils.cpp b/src/duckstation-qt/qtutils.cpp index 094d37519..b5106a45c 100644 --- a/src/duckstation-qt/qtutils.cpp +++ b/src/duckstation-qt/qtutils.cpp @@ -64,6 +64,23 @@ QWidget* GetRootWidget(QWidget* widget, bool stop_at_window_or_dialog) return widget; } +void ShowOrRaiseWindow(QWidget* window) +{ + if (!window) + return; + + if (!window->isVisible()) + { + window->show(); + } + else + { + window->raise(); + window->activateWindow(); + window->setFocus(); + } +} + template ALWAYS_INLINE_RELEASE static void ResizeColumnsForView(T* view, const std::initializer_list& widths) { diff --git a/src/duckstation-qt/qtutils.h b/src/duckstation-qt/qtutils.h index 44a8e8447..5ecbdd48c 100644 --- a/src/duckstation-qt/qtutils.h +++ b/src/duckstation-qt/qtutils.h @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2019-2022 Connor McLaughlin +// SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin // SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0) #pragma once @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -46,6 +47,25 @@ QFrame* CreateHorizontalLine(QWidget* parent); /// Returns the greatest parent of a widget, i.e. its dialog/window. QWidget* GetRootWidget(QWidget* widget, bool stop_at_window_or_dialog = true); +/// Shows or raises a window (brings it to the front). +void ShowOrRaiseWindow(QWidget* window); + +/// Closes and deletes a window later, outside of this event pump. +template +[[maybe_unused]] static void CloseAndDeleteWindow(T*& window) +{ + if (!window) + return; + + window->close(); + + // Some windows delete themselves. + if (window) + window->deleteLater(); + + window = nullptr; +} + /// Resizes columns of the table view to at the specified widths. A negative width will stretch the column to use the /// remaining space. void ResizeColumnsForTableView(QTableView* view, const std::initializer_list& widths);