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 +