From 3a03b579d2269761bb653cd38729c796797dbd5c Mon Sep 17 00:00:00 2001 From: Dan McCarthy Date: Sat, 30 Dec 2023 20:05:16 -0600 Subject: [PATCH] Debugger: Add Saved Addresses tab widget for bookmarking mem addresses Adds a tab widget to the debugger that allows saving/bookmarking memory addresses and giving them labels/descriptions for convenience. Includes the ability to jump back to memory addresses from the Saved Addresses tab, and adding Saved Addresses from memory search search results context menu and the memory view context menu. Also supports importing and exporting the saved addresses as CSV format. --- pcsx2-qt/CMakeLists.txt | 2 + pcsx2-qt/Debugger/CpuWidget.cpp | 144 +++++++++++++++++ pcsx2-qt/Debugger/CpuWidget.h | 7 + pcsx2-qt/Debugger/CpuWidget.ui | 32 ++++ pcsx2-qt/Debugger/MemoryViewWidget.cpp | 4 + pcsx2-qt/Debugger/MemoryViewWidget.h | 1 + .../Debugger/Models/SavedAddressesModel.cpp | 152 ++++++++++++++++++ .../Debugger/Models/SavedAddressesModel.h | 52 ++++++ pcsx2-qt/pcsx2-qt.vcxproj | 3 + pcsx2-qt/pcsx2-qt.vcxproj.filters | 9 ++ 10 files changed, 406 insertions(+) create mode 100644 pcsx2-qt/Debugger/Models/SavedAddressesModel.cpp create mode 100644 pcsx2-qt/Debugger/Models/SavedAddressesModel.h diff --git a/pcsx2-qt/CMakeLists.txt b/pcsx2-qt/CMakeLists.txt index 245077a89d..4371e84282 100644 --- a/pcsx2-qt/CMakeLists.txt +++ b/pcsx2-qt/CMakeLists.txt @@ -168,6 +168,8 @@ target_sources(pcsx2-qt PRIVATE Debugger/Models/ThreadModel.h Debugger/Models/StackModel.cpp Debugger/Models/StackModel.h + Debugger/Models/SavedAddressesModel.cpp + Debugger/Models/SavedAddressesModel.h Tools/InputRecording/NewInputRecordingDlg.cpp Tools/InputRecording/NewInputRecordingDlg.h Tools/InputRecording/NewInputRecordingDlg.ui diff --git a/pcsx2-qt/Debugger/CpuWidget.cpp b/pcsx2-qt/Debugger/CpuWidget.cpp index 36c499f9df..5b050fc188 100644 --- a/pcsx2-qt/Debugger/CpuWidget.cpp +++ b/pcsx2-qt/Debugger/CpuWidget.cpp @@ -7,6 +7,7 @@ #include "BreakpointDialog.h" #include "Models/BreakpointModel.h" #include "Models/ThreadModel.h" +#include "Models/SavedAddressesModel.h" #include "DebugTools/DebugInterface.h" #include "DebugTools/Breakpoints.h" @@ -39,6 +40,7 @@ CpuWidget::CpuWidget(QWidget* parent, DebugInterface& cpu) , m_bpModel(cpu) , m_threadModel(cpu) , m_stackModel(cpu) + , m_savedAddressesModel(cpu) { m_ui.setupUi(this); @@ -46,6 +48,7 @@ CpuWidget::CpuWidget(QWidget* parent, DebugInterface& cpu) connect(m_ui.registerWidget, &RegisterWidget::gotoInDisasm, m_ui.disassemblyWidget, &DisassemblyWidget::gotoAddress); connect(m_ui.memoryviewWidget, &MemoryViewWidget::gotoInDisasm, m_ui.disassemblyWidget, &DisassemblyWidget::gotoAddress); + connect(m_ui.memoryviewWidget, &MemoryViewWidget::addToSavedAddresses, this, &CpuWidget::addAddressToSavedAddressesList); connect(m_ui.registerWidget, &RegisterWidget::gotoInMemory, m_ui.memoryviewWidget, &MemoryViewWidget::gotoAddress); connect(m_ui.disassemblyWidget, &DisassemblyWidget::gotoInMemory, m_ui.memoryviewWidget, &MemoryViewWidget::gotoAddress); @@ -127,6 +130,18 @@ CpuWidget::CpuWidget(QWidget* parent, DebugInterface& cpu) m_resultsLoadTimer.setInterval(100); m_resultsLoadTimer.setSingleShot(true); connect(&m_resultsLoadTimer, &QTimer::timeout, this, &CpuWidget::loadSearchResults); + + m_ui.savedAddressesList->setModel(&m_savedAddressesModel); + m_ui.savedAddressesList->setContextMenuPolicy(Qt::CustomContextMenu); + connect(m_ui.savedAddressesList, &QTableView::customContextMenuRequested, this, &CpuWidget::onSavedAddressesListContextMenu); + for (std::size_t i = 0; auto mode : SavedAddressesModel::HeaderResizeModes) + { + m_ui.savedAddressesList->horizontalHeader()->setSectionResizeMode(i++, mode); + } + QTableView* savedAddressesTableView = m_ui.savedAddressesList; + connect(m_ui.savedAddressesList->model(), &QAbstractItemModel::dataChanged, [savedAddressesTableView](const QModelIndex& topLeft) { + savedAddressesTableView->resizeColumnToContents(topLeft.column()); + }); } CpuWidget::~CpuWidget() = default; @@ -490,6 +505,128 @@ void CpuWidget::contextBPListPasteCSV() } } +void CpuWidget::onSavedAddressesListContextMenu(QPoint pos) +{ + QMenu* contextMenu = new QMenu("Saved Addresses List Context Menu", m_ui.savedAddressesList); + + QAction* newAction = new QAction(tr("New"), m_ui.savedAddressesList); + connect(newAction, &QAction::triggered, this, &CpuWidget::contextSavedAddressesListNew); + contextMenu->addAction(newAction); + + const QModelIndex indexAtPos = m_ui.savedAddressesList->indexAt(pos); + const bool isIndexValid = indexAtPos.isValid(); + + if (isIndexValid) + { + if (m_cpu.isAlive()) + { + QAction* goToAddressMemViewAction = new QAction(tr("Go to in Memory View"), m_ui.savedAddressesList); + connect(goToAddressMemViewAction, &QAction::triggered, this, [this, indexAtPos]() { + const QModelIndex rowAddressIndex = m_ui.savedAddressesList->model()->index(indexAtPos.row(), 0, QModelIndex()); + m_ui.memoryviewWidget->gotoAddress(m_ui.savedAddressesList->model()->data(rowAddressIndex, Qt::UserRole).toUInt()); + m_ui.tabWidget->setCurrentWidget(m_ui.tab_memory); + }); + contextMenu->addAction(goToAddressMemViewAction); + + QAction* goToAddressDisassemblyAction = new QAction(tr("Go to in Disassembly"), m_ui.savedAddressesList); + connect(goToAddressDisassemblyAction, &QAction::triggered, this, [this, indexAtPos]() { + const QModelIndex rowAddressIndex = m_ui.savedAddressesList->model()->index(indexAtPos.row(), 0, QModelIndex()); + m_ui.disassemblyWidget->gotoAddress(m_ui.savedAddressesList->model()->data(rowAddressIndex, Qt::UserRole).toUInt()); + }); + contextMenu->addAction(goToAddressDisassemblyAction); + } + + QAction* copyAction = new QAction(indexAtPos.column() == 0 ? tr("Copy Address") : tr("Copy Text"), m_ui.savedAddressesList); + connect(copyAction, &QAction::triggered, [this, indexAtPos]() { + QGuiApplication::clipboard()->setText(m_ui.savedAddressesList->model()->data(indexAtPos, Qt::DisplayRole).toString()); + }); + contextMenu->addAction(copyAction); + } + + if (m_ui.savedAddressesList->model()->rowCount() > 0) + { + QAction* actionExportCSV = new QAction(tr("Copy all as CSV"), m_ui.savedAddressesList); + connect(actionExportCSV, &QAction::triggered, [this]() { + QGuiApplication::clipboard()->setText(QtUtils::AbstractItemModelToCSV(m_ui.savedAddressesList->model(), Qt::DisplayRole, true)); + }); + contextMenu->addAction(actionExportCSV); + } + + QAction* actionImportCSV = new QAction(tr("Paste from CSV"), m_ui.savedAddressesList); + connect(actionImportCSV, &QAction::triggered, this, &CpuWidget::contextSavedAddressesListPasteCSV); + contextMenu->addAction(actionImportCSV); + + contextMenu->popup(m_ui.savedAddressesList->viewport()->mapToGlobal(pos)); + + if (isIndexValid) + { + QAction* deleteAction = new QAction(tr("Delete"), m_ui.savedAddressesList); + connect(deleteAction, &QAction::triggered, this, [this, indexAtPos]() { + m_ui.savedAddressesList->model()->removeRows(indexAtPos.row(), 1); + }); + contextMenu->addAction(deleteAction); + } +} + +void CpuWidget::contextSavedAddressesListPasteCSV() +{ + QString csv = QGuiApplication::clipboard()->text(); + // Skip header + csv = csv.mid(csv.indexOf('\n') + 1); + + for (const QString& line : csv.split('\n')) + { + QStringList fields; + // In order to handle text with commas in them we must wrap values in quotes to mark + // where a value starts and end so that text commas aren't identified as delimiters. + // So matches each quote pair, parse it out, and removes the quotes to get the value. + QRegularExpression eachQuotePair(R"("([^"]|\\.)*")"); + QRegularExpressionMatchIterator it = eachQuotePair.globalMatch(line); + while (it.hasNext()) + { + QRegularExpressionMatch match = it.next(); + QString matchedValue = match.captured(0); + fields << matchedValue.mid(1, matchedValue.length() - 2); + } + + if (fields.size() != SavedAddressesModel::HeaderColumns::COLUMN_COUNT) + { + Console.WriteLn("Debugger CSV Import: Invalid number of columns, skipping"); + continue; + } + + bool ok; + const u32 address = fields[SavedAddressesModel::HeaderColumns::ADDRESS].toUInt(&ok, 16); + if (!ok) + { + Console.WriteLn("Debugger CSV Import: Failed to parse address '%s', skipping", fields[SavedAddressesModel::HeaderColumns::ADDRESS].toUtf8().constData()); + continue; + } + + const QString label = fields[SavedAddressesModel::HeaderColumns::LABEL]; + const QString description = fields[SavedAddressesModel::HeaderColumns::DESCRIPTION]; + const SavedAddressesModel::SavedAddress importedAddress = {address, label, description}; + m_savedAddressesModel.addRow(importedAddress); + } +} + +void CpuWidget::contextSavedAddressesListNew() +{ + qobject_cast(m_ui.savedAddressesList->model())->addRow(); + const u32 rowCount = m_ui.savedAddressesList->model()->rowCount(); + m_ui.savedAddressesList->edit(m_ui.savedAddressesList->model()->index(rowCount - 1, 0)); +} + +void CpuWidget::addAddressToSavedAddressesList(u32 address) +{ + qobject_cast(m_ui.savedAddressesList->model())->addRow(); + const u32 rowCount = m_ui.savedAddressesList->model()->rowCount(); + const QModelIndex addressIndex = m_ui.savedAddressesList->model()->index(rowCount - 1, 0); + m_ui.tabWidget->setCurrentWidget(m_ui.tab_savedaddresses); + m_ui.savedAddressesList->model()->setData(addressIndex, address, Qt::UserRole); + m_ui.savedAddressesList->edit(m_ui.savedAddressesList->model()->index(rowCount - 1, 1)); +} + void CpuWidget::contextSearchResultGoToDisassembly() { const QItemSelectionModel* selModel = m_ui.listSearchResults->selectionModel(); @@ -812,6 +949,7 @@ void CpuWidget::onListSearchResultsContextMenu(QPoint pos) { QMenu* contextMenu = new QMenu(tr("Search Results List Context Menu"), m_ui.listSearchResults); const QItemSelectionModel* selModel = m_ui.listSearchResults->selectionModel(); + const auto listSearchResults = m_ui.listSearchResults; if (selModel->hasSelection()) { @@ -819,6 +957,12 @@ void CpuWidget::onListSearchResultsContextMenu(QPoint pos) connect(goToDisassemblyAction, &QAction::triggered, this, &CpuWidget::contextSearchResultGoToDisassembly); contextMenu->addAction(goToDisassemblyAction); + QAction* addToSavedAddressesAction = new QAction(tr("Add to Saved Memory Addresses"), m_ui.listSearchResults); + connect(addToSavedAddressesAction, &QAction::triggered, this, [this, listSearchResults]() { + addAddressToSavedAddressesList(listSearchResults->selectedItems().first()->data(Qt::UserRole).toUInt()); + }); + contextMenu->addAction(addToSavedAddressesAction); + QAction* removeResultAction = new QAction(tr("Remove Result"), m_ui.listSearchResults); connect(removeResultAction, &QAction::triggered, this, &CpuWidget::contextRemoveSearchResult); contextMenu->addAction(removeResultAction); diff --git a/pcsx2-qt/Debugger/CpuWidget.h b/pcsx2-qt/Debugger/CpuWidget.h index 6c4f895adc..54a63d0558 100644 --- a/pcsx2-qt/Debugger/CpuWidget.h +++ b/pcsx2-qt/Debugger/CpuWidget.h @@ -13,6 +13,7 @@ #include "Models/BreakpointModel.h" #include "Models/ThreadModel.h" #include "Models/StackModel.h" +#include "Models/SavedAddressesModel.h" #include "QtHost.h" #include @@ -74,6 +75,11 @@ public slots: void contextBPListEdit(); void contextBPListPasteCSV(); + void onSavedAddressesListContextMenu(QPoint pos); + void contextSavedAddressesListPasteCSV(); + void contextSavedAddressesListNew(); + void addAddressToSavedAddressesList(u32 address); + void updateThreads(); void onThreadListDoubleClick(const QModelIndex& index); void onThreadListContextMenu(QPoint pos); @@ -128,6 +134,7 @@ private: ThreadModel m_threadModel; QSortFilterProxyModel m_threadProxyModel; StackModel m_stackModel; + SavedAddressesModel m_savedAddressesModel; QTimer m_resultsLoadTimer; bool m_demangleFunctions = true; diff --git a/pcsx2-qt/Debugger/CpuWidget.ui b/pcsx2-qt/Debugger/CpuWidget.ui index ad7f61c9ed..4b3c849460 100644 --- a/pcsx2-qt/Debugger/CpuWidget.ui +++ b/pcsx2-qt/Debugger/CpuWidget.ui @@ -593,6 +593,38 @@ + + + Saved Addresses + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Qt::CustomContextMenu + + + Qt::ScrollBarAlwaysOff + + + Qt::NoPen + + + + + diff --git a/pcsx2-qt/Debugger/MemoryViewWidget.cpp b/pcsx2-qt/Debugger/MemoryViewWidget.cpp index 681500cb8c..9a4180ce7f 100644 --- a/pcsx2-qt/Debugger/MemoryViewWidget.cpp +++ b/pcsx2-qt/Debugger/MemoryViewWidget.cpp @@ -424,6 +424,10 @@ void MemoryViewWidget::customMenuRequested(QPoint pos) m_contextMenu->addSeparator(); + action = new QAction((tr("Add to Saved Memory Addresses"))); + m_contextMenu->addAction(action); + connect(action, &QAction::triggered, this, [this]() { emit addToSavedAddresses(m_table.selectedAddress); }); + action = new QAction(tr("Copy Byte")); m_contextMenu->addAction(action); connect(action, &QAction::triggered, this, [this]() { contextCopyByte(); }); diff --git a/pcsx2-qt/Debugger/MemoryViewWidget.h b/pcsx2-qt/Debugger/MemoryViewWidget.h index 2b1b5217c9..ef498a92a7 100644 --- a/pcsx2-qt/Debugger/MemoryViewWidget.h +++ b/pcsx2-qt/Debugger/MemoryViewWidget.h @@ -104,6 +104,7 @@ public slots: signals: void gotoInDisasm(u32 address, bool should_set_focus = true); + void addToSavedAddresses(u32 address); void VMUpdate(); private: diff --git a/pcsx2-qt/Debugger/Models/SavedAddressesModel.cpp b/pcsx2-qt/Debugger/Models/SavedAddressesModel.cpp new file mode 100644 index 0000000000..ab07832b73 --- /dev/null +++ b/pcsx2-qt/Debugger/Models/SavedAddressesModel.cpp @@ -0,0 +1,152 @@ +// SPDX-FileCopyrightText: 2002-2024 PCSX2 Dev Team +// SPDX-License-Identifier: LGPL-3.0+ + +#include "PrecompiledHeader.h" +#include "SavedAddressesModel.h" + +SavedAddressesModel::SavedAddressesModel(DebugInterface& cpu, QObject* parent) + : QAbstractTableModel(parent) + , m_cpu(cpu) +{ +} + +QVariant SavedAddressesModel::data(const QModelIndex& index, int role) const +{ + if (role == Qt::CheckStateRole) + { + return QVariant(); + } + + if (role == Qt::DisplayRole || role == Qt::EditRole) + { + SavedAddress savedAddress = m_savedAddresses.at(index.row()); + switch (index.column()) + { + case HeaderColumns::ADDRESS: + return QString::number(savedAddress.address, 16).toUpper(); + case HeaderColumns::LABEL: + return savedAddress.label; + case HeaderColumns::DESCRIPTION: + return savedAddress.description; + } + } + if (role == Qt::UserRole) + { + SavedAddress savedAddress = m_savedAddresses.at(index.row()); + switch (index.column()) + { + case HeaderColumns::ADDRESS: + return savedAddress.address; + case HeaderColumns::LABEL: + return savedAddress.label; + case HeaderColumns::DESCRIPTION: + return savedAddress.description; + } + } + return QVariant(); +} + +bool SavedAddressesModel::setData(const QModelIndex& index, const QVariant& value, int role) +{ + if (role == Qt::CheckStateRole) + { + return false; + } + + if (role == Qt::EditRole) + { + SavedAddress addressToEdit = m_savedAddresses.at(index.row()); + if (index.column() == HeaderColumns::ADDRESS) + { + bool ok = false; + const u32 address = value.toString().toUInt(&ok, 16); + if (ok) + addressToEdit.address = address; + else + return false; + } + if (index.column() == HeaderColumns::DESCRIPTION) + addressToEdit.description = value.toString(); + if (index.column() == HeaderColumns::LABEL) + addressToEdit.label = value.toString(); + m_savedAddresses.at(index.row()) = addressToEdit; + + emit dataChanged(index, index, QList(role)); + return true; + } + else if (role == Qt::UserRole) + { + SavedAddress addressToEdit = m_savedAddresses.at(index.row()); + if (index.column() == HeaderColumns::ADDRESS) + { + const u32 address = value.toUInt(); + addressToEdit.address = address; + } + if (index.column() == HeaderColumns::DESCRIPTION) + addressToEdit.description = value.toString(); + if (index.column() == HeaderColumns::LABEL) + addressToEdit.label = value.toString(); + m_savedAddresses.at(index.row()) = addressToEdit; + + emit dataChanged(index, index, QList(role)); + return true; + } + return false; +} + +QVariant SavedAddressesModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + if ((role != Qt::DisplayRole && role != Qt::EditRole) || orientation != Qt::Horizontal) + return QVariant(); + + switch (section) + { + case SavedAddressesModel::ADDRESS: + return tr("MEMORY ADDRESS"); + case SavedAddressesModel::LABEL: + return tr("LABEL"); + case SavedAddressesModel::DESCRIPTION: + return tr("DESCRIPTION"); + default: + return QVariant(); + } +} + +Qt::ItemFlags SavedAddressesModel::flags(const QModelIndex& index) const +{ + return Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsEditable; +} + +void SavedAddressesModel::addRow() +{ + const SavedAddress defaultNewAddress = {NULL, "Name", "Description"}; + addRow(defaultNewAddress); +} + +void SavedAddressesModel::addRow(SavedAddress addresstoSave) +{ + const int newRowIndex = m_savedAddresses.size(); + beginInsertRows(QModelIndex(), newRowIndex, newRowIndex); + m_savedAddresses.push_back(addresstoSave); + endInsertRows(); +} + +bool SavedAddressesModel::removeRows(int row, int count, const QModelIndex& parent) +{ + if (row + count > m_savedAddresses.size() || row < 0 || count < 1) + return false; + beginRemoveRows(parent, row, row + count - 1); + m_savedAddresses.erase(m_savedAddresses.begin() + row, m_savedAddresses.begin() + row + count); + endRemoveRows(); + return true; +} + +int SavedAddressesModel::rowCount(const QModelIndex&) const +{ + return m_savedAddresses.size(); +} + +int SavedAddressesModel::columnCount(const QModelIndex&) const +{ + return HeaderColumns::COLUMN_COUNT; +} diff --git a/pcsx2-qt/Debugger/Models/SavedAddressesModel.h b/pcsx2-qt/Debugger/Models/SavedAddressesModel.h new file mode 100644 index 0000000000..f3b293b588 --- /dev/null +++ b/pcsx2-qt/Debugger/Models/SavedAddressesModel.h @@ -0,0 +1,52 @@ +// SPDX-FileCopyrightText: 2002-2024 PCSX2 Dev Team +// SPDX-License-Identifier: LGPL-3.0+ + +#pragma once + +#include +#include + +#include "DebugTools/DebugInterface.h" + +class SavedAddressesModel : public QAbstractTableModel +{ + Q_OBJECT + +public: + struct SavedAddress + { + u32 address; + QString label; + QString description; + }; + + enum HeaderColumns : int + { + ADDRESS = 0, + LABEL, + DESCRIPTION, + COLUMN_COUNT + }; + + static constexpr QHeaderView::ResizeMode HeaderResizeModes[HeaderColumns::COLUMN_COUNT] = + { + QHeaderView::ResizeMode::ResizeToContents, + QHeaderView::ResizeMode::ResizeToContents, + QHeaderView::ResizeMode::Stretch, + }; + + explicit SavedAddressesModel(DebugInterface& cpu, QObject* parent = nullptr); + QVariant data(const QModelIndex& index, int role) const override; + QVariant headerData(int section, Qt::Orientation orientation, int role) const override; + int rowCount(const QModelIndex& parent = QModelIndex()) const override; + int columnCount(const QModelIndex& parent = QModelIndex()) const override; + Qt::ItemFlags flags(const QModelIndex& index) const override; + void addRow(); + void addRow(SavedAddress addresstoSave); + bool removeRows(int row, int count, const QModelIndex& parent = QModelIndex()) override; + bool setData(const QModelIndex& index, const QVariant& value, int role) override; + +private: + DebugInterface& m_cpu; + std::vector m_savedAddresses; +}; diff --git a/pcsx2-qt/pcsx2-qt.vcxproj b/pcsx2-qt/pcsx2-qt.vcxproj index 5b1578d2b1..d461c1f1ed 100644 --- a/pcsx2-qt/pcsx2-qt.vcxproj +++ b/pcsx2-qt/pcsx2-qt.vcxproj @@ -108,6 +108,7 @@ + @@ -196,6 +197,7 @@ + @@ -251,6 +253,7 @@ + diff --git a/pcsx2-qt/pcsx2-qt.vcxproj.filters b/pcsx2-qt/pcsx2-qt.vcxproj.filters index 0574cf5a6c..140cd153da 100644 --- a/pcsx2-qt/pcsx2-qt.vcxproj.filters +++ b/pcsx2-qt/pcsx2-qt.vcxproj.filters @@ -287,6 +287,9 @@ Debugger\Models + + Debugger\Models + moc @@ -314,6 +317,9 @@ moc + + moc + moc @@ -482,6 +488,9 @@ Debugger\Models + + Debugger\Models +