First Commit
This commit is contained in:
754
pcsx2-qt/Debugger/Memory/MemorySearchView.cpp
Normal file
754
pcsx2-qt/Debugger/Memory/MemorySearchView.cpp
Normal file
@@ -0,0 +1,754 @@
|
||||
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
|
||||
// SPDX-License-Identifier: GPL-3.0+
|
||||
|
||||
#include "MemorySearchView.h"
|
||||
|
||||
#include "DebugTools/DebugInterface.h"
|
||||
|
||||
#include "QtUtils.h"
|
||||
|
||||
#include "common/Console.h"
|
||||
|
||||
#include <QtGui/QClipboard>
|
||||
#include <QtWidgets/QMenu>
|
||||
#include <QtWidgets/QScrollBar>
|
||||
#include <QtWidgets/QMessageBox>
|
||||
#include <QtConcurrent/QtConcurrent>
|
||||
#include <QtCore/QFutureWatcher>
|
||||
#include <QtGui/QPainter>
|
||||
|
||||
using SearchComparison = MemorySearchView::SearchComparison;
|
||||
using SearchType = MemorySearchView::SearchType;
|
||||
using SearchResult = MemorySearchView::SearchResult;
|
||||
|
||||
using namespace QtUtils;
|
||||
|
||||
MemorySearchView::MemorySearchView(const DebuggerViewParameters& parameters)
|
||||
: DebuggerView(parameters, MONOSPACE_FONT)
|
||||
{
|
||||
m_ui.setupUi(this);
|
||||
this->repaint();
|
||||
|
||||
m_ui.listSearchResults->setContextMenuPolicy(Qt::CustomContextMenu);
|
||||
connect(m_ui.btnSearch, &QPushButton::clicked, this, &MemorySearchView::onSearchButtonClicked);
|
||||
connect(m_ui.btnFilterSearch, &QPushButton::clicked, this, &MemorySearchView::onSearchButtonClicked);
|
||||
connect(m_ui.listSearchResults, &QListWidget::itemDoubleClicked, [](QListWidgetItem* item) {
|
||||
goToInMemoryView(item->text().toUInt(nullptr, 16), true);
|
||||
});
|
||||
connect(m_ui.listSearchResults->verticalScrollBar(), &QScrollBar::valueChanged, this, &MemorySearchView::onSearchResultsListScroll);
|
||||
connect(m_ui.listSearchResults, &QListView::customContextMenuRequested, this, &MemorySearchView::onListSearchResultsContextMenu);
|
||||
connect(m_ui.cmbSearchType, &QComboBox::currentIndexChanged, this, &MemorySearchView::onSearchTypeChanged);
|
||||
connect(m_ui.cmbSearchComparison, &QComboBox::currentIndexChanged, this, &MemorySearchView::onSearchComparisonChanged);
|
||||
|
||||
// Ensures we don't retrigger the load results function unintentionally
|
||||
m_resultsLoadTimer.setInterval(100);
|
||||
m_resultsLoadTimer.setSingleShot(true);
|
||||
connect(&m_resultsLoadTimer, &QTimer::timeout, this, &MemorySearchView::loadSearchResults);
|
||||
|
||||
receiveEvent<DebuggerEvents::Refresh>([this](const DebuggerEvents::Refresh& event) -> bool {
|
||||
update();
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
void MemorySearchView::contextRemoveSearchResult()
|
||||
{
|
||||
const QItemSelectionModel* selModel = m_ui.listSearchResults->selectionModel();
|
||||
if (!selModel->hasSelection())
|
||||
return;
|
||||
|
||||
const int selectedResultIndex = m_ui.listSearchResults->row(m_ui.listSearchResults->selectedItems().first());
|
||||
const auto* rowToRemove = m_ui.listSearchResults->takeItem(selectedResultIndex);
|
||||
u32 address = rowToRemove->data(Qt::UserRole).toUInt();
|
||||
if (m_searchResults.size() > static_cast<size_t>(selectedResultIndex) && m_searchResults.at(selectedResultIndex).getAddress() == address)
|
||||
{
|
||||
m_searchResults.erase(m_searchResults.begin() + selectedResultIndex);
|
||||
}
|
||||
delete rowToRemove;
|
||||
}
|
||||
|
||||
void MemorySearchView::contextCopySearchResultAddress()
|
||||
{
|
||||
if (!m_ui.listSearchResults->selectionModel()->hasSelection())
|
||||
return;
|
||||
|
||||
const u32 selectedResultIndex = m_ui.listSearchResults->row(m_ui.listSearchResults->selectedItems().first());
|
||||
const u32 rowAddress = m_ui.listSearchResults->item(selectedResultIndex)->data(Qt::UserRole).toUInt();
|
||||
const QString addressString = FilledQStringFromValue(rowAddress, 16);
|
||||
QApplication::clipboard()->setText(addressString);
|
||||
}
|
||||
|
||||
void MemorySearchView::onListSearchResultsContextMenu(QPoint pos)
|
||||
{
|
||||
const QItemSelectionModel* selection_model = m_ui.listSearchResults->selectionModel();
|
||||
const QListWidget* list_search_results = m_ui.listSearchResults;
|
||||
|
||||
QMenu* menu = new QMenu(this);
|
||||
menu->setAttribute(Qt::WA_DeleteOnClose);
|
||||
|
||||
if (selection_model->hasSelection())
|
||||
{
|
||||
connect(menu->addAction(tr("Copy Address")), &QAction::triggered,
|
||||
this, &MemorySearchView::contextCopySearchResultAddress);
|
||||
|
||||
createEventActions<DebuggerEvents::GoToAddress>(menu, [list_search_results]() {
|
||||
u32 selected_address = list_search_results->selectedItems().first()->data(Qt::UserRole).toUInt();
|
||||
DebuggerEvents::GoToAddress event;
|
||||
event.address = selected_address;
|
||||
return std::optional(event);
|
||||
});
|
||||
|
||||
createEventActions<DebuggerEvents::AddToSavedAddresses>(menu, [list_search_results]() {
|
||||
u32 selected_address = list_search_results->selectedItems().first()->data(Qt::UserRole).toUInt();
|
||||
DebuggerEvents::AddToSavedAddresses event;
|
||||
event.address = selected_address;
|
||||
return std::optional(event);
|
||||
});
|
||||
|
||||
connect(menu->addAction(tr("Remove Result")), &QAction::triggered,
|
||||
this, &MemorySearchView::contextRemoveSearchResult);
|
||||
}
|
||||
|
||||
menu->popup(m_ui.listSearchResults->viewport()->mapToGlobal(pos));
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
T readValueAtAddress(DebugInterface* cpu, u32 addr);
|
||||
template <>
|
||||
float readValueAtAddress<float>(DebugInterface* cpu, u32 addr)
|
||||
{
|
||||
return std::bit_cast<float>(cpu->read32(addr));
|
||||
}
|
||||
|
||||
template <>
|
||||
double readValueAtAddress<double>(DebugInterface* cpu, u32 addr)
|
||||
{
|
||||
return std::bit_cast<double>(cpu->read64(addr));
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
T readValueAtAddress(DebugInterface* cpu, u32 addr)
|
||||
{
|
||||
T val = 0;
|
||||
switch (sizeof(T))
|
||||
{
|
||||
case sizeof(u8):
|
||||
val = cpu->read8(addr);
|
||||
break;
|
||||
case sizeof(u16):
|
||||
val = cpu->read16(addr);
|
||||
break;
|
||||
case sizeof(u32):
|
||||
{
|
||||
val = cpu->read32(addr);
|
||||
break;
|
||||
}
|
||||
case sizeof(u64):
|
||||
{
|
||||
val = cpu->read64(addr);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return val;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
static bool memoryValueComparator(SearchComparison searchComparison, T searchValue, T readValue)
|
||||
{
|
||||
const bool isNotOperator = searchComparison == SearchComparison::NotEquals;
|
||||
switch (searchComparison)
|
||||
{
|
||||
case SearchComparison::Equals:
|
||||
case SearchComparison::NotEquals:
|
||||
{
|
||||
bool areValuesEqual = false;
|
||||
if constexpr (std::is_same_v<T, float>)
|
||||
{
|
||||
const T fTop = searchValue + 0.00001f;
|
||||
const T fBottom = searchValue - 0.00001f;
|
||||
areValuesEqual = (fBottom < readValue && readValue < fTop);
|
||||
}
|
||||
else if constexpr (std::is_same_v<T, double>)
|
||||
{
|
||||
const double dTop = searchValue + 0.00001f;
|
||||
const double dBottom = searchValue - 0.00001f;
|
||||
areValuesEqual = (dBottom < readValue && readValue < dTop);
|
||||
}
|
||||
else
|
||||
{
|
||||
areValuesEqual = searchValue == readValue;
|
||||
}
|
||||
return isNotOperator ? !areValuesEqual : areValuesEqual;
|
||||
break;
|
||||
}
|
||||
case SearchComparison::GreaterThan:
|
||||
case SearchComparison::GreaterThanOrEqual:
|
||||
case SearchComparison::LessThan:
|
||||
case SearchComparison::LessThanOrEqual:
|
||||
{
|
||||
const bool hasEqualsCheck = searchComparison == SearchComparison::GreaterThanOrEqual || searchComparison == SearchComparison::LessThanOrEqual;
|
||||
if (hasEqualsCheck && memoryValueComparator(SearchComparison::Equals, searchValue, readValue))
|
||||
return true;
|
||||
|
||||
const bool isGreaterOperator = searchComparison == SearchComparison::GreaterThan || searchComparison == SearchComparison::GreaterThanOrEqual;
|
||||
if (std::is_same_v<T, float>)
|
||||
{
|
||||
const T fTop = searchValue + 0.00001f;
|
||||
const T fBottom = searchValue - 0.00001f;
|
||||
const bool isGreater = readValue > fTop;
|
||||
const bool isLesser = readValue < fBottom;
|
||||
return isGreaterOperator ? isGreater : isLesser;
|
||||
}
|
||||
else if (std::is_same_v<T, double>)
|
||||
{
|
||||
const double dTop = searchValue + 0.00001f;
|
||||
const double dBottom = searchValue - 0.00001f;
|
||||
const bool isGreater = readValue > dTop;
|
||||
const bool isLesser = readValue < dBottom;
|
||||
return isGreaterOperator ? isGreater : isLesser;
|
||||
}
|
||||
|
||||
return isGreaterOperator ? (readValue > searchValue) : (readValue < searchValue);
|
||||
}
|
||||
default:
|
||||
Console.Error("Debugger: Unknown type when doing memory search!");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Handles the comparison of the read value against either the search value, or if existing searchResults are available, the value at the same address in the searchResultsMap
|
||||
template <typename T>
|
||||
bool handleSearchComparison(SearchComparison searchComparison, u32 searchAddress, const SearchResult* priorResult, T searchValue, T readValue)
|
||||
{
|
||||
const bool isNotOperator = searchComparison == SearchComparison::NotEquals || searchComparison == SearchComparison::NotChanged;
|
||||
switch (searchComparison)
|
||||
{
|
||||
case SearchComparison::Equals:
|
||||
case SearchComparison::NotEquals:
|
||||
case SearchComparison::GreaterThan:
|
||||
case SearchComparison::GreaterThanOrEqual:
|
||||
case SearchComparison::LessThan:
|
||||
case SearchComparison::LessThanOrEqual:
|
||||
{
|
||||
return memoryValueComparator(searchComparison, searchValue, readValue);
|
||||
break;
|
||||
}
|
||||
case SearchComparison::Increased:
|
||||
{
|
||||
const T priorValue = priorResult->getValue<T>();
|
||||
return memoryValueComparator(SearchComparison::GreaterThan, priorValue, readValue);
|
||||
break;
|
||||
}
|
||||
case SearchComparison::IncreasedBy:
|
||||
{
|
||||
const T priorValue = priorResult->getValue<T>();
|
||||
const T expectedIncrease = searchValue + priorValue;
|
||||
return memoryValueComparator(SearchComparison::Equals, readValue, expectedIncrease);
|
||||
break;
|
||||
}
|
||||
case SearchComparison::Decreased:
|
||||
{
|
||||
const T priorValue = priorResult->getValue<T>();
|
||||
return memoryValueComparator(SearchComparison::LessThan, priorValue, readValue);
|
||||
break;
|
||||
}
|
||||
case SearchComparison::DecreasedBy:
|
||||
{
|
||||
const T priorValue = priorResult->getValue<T>();
|
||||
const T expectedDecrease = priorValue - searchValue;
|
||||
return memoryValueComparator(SearchComparison::Equals, readValue, expectedDecrease);
|
||||
break;
|
||||
}
|
||||
case SearchComparison::Changed:
|
||||
case SearchComparison::NotChanged:
|
||||
{
|
||||
const T priorValue = priorResult->getValue<T>();
|
||||
return memoryValueComparator(isNotOperator ? SearchComparison::Equals : SearchComparison::NotEquals, priorValue, readValue);
|
||||
break;
|
||||
}
|
||||
case SearchComparison::ChangedBy:
|
||||
{
|
||||
const T priorValue = priorResult->getValue<T>();
|
||||
const T expectedIncrease = searchValue + priorValue;
|
||||
const T expectedDecrease = priorValue - searchValue;
|
||||
return memoryValueComparator(SearchComparison::Equals, readValue, expectedIncrease) || memoryValueComparator(SearchComparison::Equals, readValue, expectedDecrease);
|
||||
}
|
||||
case SearchComparison::UnknownValue:
|
||||
{
|
||||
return true;
|
||||
}
|
||||
default:
|
||||
Console.Error("Debugger: Unknown type when doing memory search!");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
void searchWorker(DebugInterface* cpu, std::vector<SearchResult>& searchResults, SearchType searchType, SearchComparison searchComparison, u32 start, u32 end, T searchValue)
|
||||
{
|
||||
const bool isSearchingRange = searchResults.size() <= 0;
|
||||
if (isSearchingRange)
|
||||
{
|
||||
for (u32 addr = start; addr < end; addr += sizeof(T))
|
||||
{
|
||||
if (!cpu->isValidAddress(addr))
|
||||
continue;
|
||||
|
||||
T readValue = readValueAtAddress<T>(cpu, addr);
|
||||
if (handleSearchComparison(searchComparison, addr, nullptr, searchValue, readValue))
|
||||
{
|
||||
searchResults.push_back(MemorySearchView::SearchResult(addr, QVariant::fromValue(readValue), searchType));
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
auto removeIt = std::remove_if(searchResults.begin(), searchResults.end(), [cpu, searchType, searchComparison, searchValue](SearchResult& searchResult) -> bool {
|
||||
const u32 addr = searchResult.getAddress();
|
||||
if (!cpu->isValidAddress(addr))
|
||||
return true;
|
||||
|
||||
const auto readValue = readValueAtAddress<T>(cpu, addr);
|
||||
|
||||
const bool doesMatch = handleSearchComparison(searchComparison, addr, &searchResult, searchValue, readValue);
|
||||
if (doesMatch)
|
||||
searchResult = MemorySearchView::SearchResult(addr, QVariant::fromValue(readValue), searchType);
|
||||
|
||||
return !doesMatch;
|
||||
});
|
||||
searchResults.erase(removeIt, searchResults.end());
|
||||
}
|
||||
}
|
||||
|
||||
static bool compareByteArrayAtAddress(DebugInterface* cpu, SearchComparison searchComparison, u32 addr, QByteArray value)
|
||||
{
|
||||
const bool isNotOperator = searchComparison == SearchComparison::NotEquals;
|
||||
for (qsizetype i = 0; i < value.length(); i++)
|
||||
{
|
||||
const char nextByte = cpu->read8(addr + i);
|
||||
switch (searchComparison)
|
||||
{
|
||||
case SearchComparison::Equals:
|
||||
{
|
||||
if (nextByte != value[i])
|
||||
return false;
|
||||
break;
|
||||
}
|
||||
case SearchComparison::NotEquals:
|
||||
{
|
||||
if (nextByte != value[i])
|
||||
return true;
|
||||
break;
|
||||
}
|
||||
default:
|
||||
{
|
||||
Console.Error("Debugger: Unknown search comparison when doing memory search");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return !isNotOperator;
|
||||
}
|
||||
|
||||
bool handleArraySearchComparison(DebugInterface* cpu, SearchComparison searchComparison, u32 searchAddress, SearchResult* priorResult, QByteArray searchValue)
|
||||
{
|
||||
const bool isNotOperator = searchComparison == SearchComparison::NotEquals || searchComparison == SearchComparison::NotChanged;
|
||||
switch (searchComparison)
|
||||
{
|
||||
case SearchComparison::Equals:
|
||||
case SearchComparison::NotEquals:
|
||||
{
|
||||
return compareByteArrayAtAddress(cpu, searchComparison, searchAddress, searchValue);
|
||||
break;
|
||||
}
|
||||
case SearchComparison::Changed:
|
||||
case SearchComparison::NotChanged:
|
||||
{
|
||||
QByteArray priorValue = priorResult->getArrayValue();
|
||||
return compareByteArrayAtAddress(cpu, isNotOperator ? SearchComparison::Equals : SearchComparison::NotEquals, searchAddress, priorValue);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
{
|
||||
Console.Error("Debugger: Unknown search comparison when doing memory search");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// Default to no match found unless the comparison is a NotEquals
|
||||
return isNotOperator;
|
||||
}
|
||||
|
||||
static QByteArray readArrayAtAddress(DebugInterface* cpu, u32 address, u32 length)
|
||||
{
|
||||
QByteArray readArray;
|
||||
for (u32 i = address; i < address + length; i++)
|
||||
{
|
||||
readArray.append(cpu->read8(i));
|
||||
}
|
||||
return readArray;
|
||||
}
|
||||
|
||||
static void searchWorkerByteArray(DebugInterface* cpu, SearchType searchType, SearchComparison searchComparison, std::vector<SearchResult>& searchResults, u32 start, u32 end, QByteArray searchValue)
|
||||
{
|
||||
const bool isSearchingRange = searchResults.size() <= 0;
|
||||
if (isSearchingRange)
|
||||
{
|
||||
for (u32 addr = start; addr < end; addr += 1)
|
||||
{
|
||||
if (!cpu->isValidAddress(addr))
|
||||
continue;
|
||||
if (handleArraySearchComparison(cpu, searchComparison, addr, nullptr, searchValue))
|
||||
{
|
||||
searchResults.push_back(MemorySearchView::SearchResult(addr, searchValue, searchType));
|
||||
addr += searchValue.length() - 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
auto removeIt = std::remove_if(searchResults.begin(), searchResults.end(), [searchComparison, searchType, searchValue, cpu](SearchResult& searchResult) -> bool {
|
||||
const u32 addr = searchResult.getAddress();
|
||||
if (!cpu->isValidAddress(addr))
|
||||
return true;
|
||||
|
||||
const bool doesMatch = handleArraySearchComparison(cpu, searchComparison, addr, &searchResult, searchValue);
|
||||
if (doesMatch)
|
||||
{
|
||||
QByteArray matchValue;
|
||||
if (searchComparison == SearchComparison::Equals)
|
||||
matchValue = searchValue;
|
||||
else if (searchComparison == SearchComparison::NotChanged)
|
||||
matchValue = searchResult.getArrayValue();
|
||||
else
|
||||
matchValue = readArrayAtAddress(cpu, addr, searchValue.length() - 1);
|
||||
searchResult = MemorySearchView::SearchResult(addr, matchValue, searchType);
|
||||
}
|
||||
return !doesMatch;
|
||||
});
|
||||
searchResults.erase(removeIt, searchResults.end());
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<SearchResult> startWorker(DebugInterface* cpu, const SearchType type, const SearchComparison comparison, std::vector<SearchResult> searchResults, u32 start, u32 end, QString value, int base)
|
||||
{
|
||||
const bool isSigned = value.startsWith("-");
|
||||
switch (type)
|
||||
{
|
||||
case SearchType::ByteType:
|
||||
isSigned ? searchWorker<s8>(cpu, searchResults, type, comparison, start, end, value.toShort(nullptr, base)) : searchWorker<u8>(cpu, searchResults, type, comparison, start, end, value.toUShort(nullptr, base));
|
||||
break;
|
||||
case SearchType::Int16Type:
|
||||
isSigned ? searchWorker<s16>(cpu, searchResults, type, comparison, start, end, value.toShort(nullptr, base)) : searchWorker<u16>(cpu, searchResults, type, comparison, start, end, value.toUShort(nullptr, base));
|
||||
break;
|
||||
case SearchType::Int32Type:
|
||||
isSigned ? searchWorker<s32>(cpu, searchResults, type, comparison, start, end, value.toInt(nullptr, base)) : searchWorker<u32>(cpu, searchResults, type, comparison, start, end, value.toUInt(nullptr, base));
|
||||
break;
|
||||
case SearchType::Int64Type:
|
||||
isSigned ? searchWorker<s64>(cpu, searchResults, type, comparison, start, end, value.toLongLong(nullptr, base)) : searchWorker<u64>(cpu, searchResults, type, comparison, start, end, value.toULongLong(nullptr, base));
|
||||
break;
|
||||
case SearchType::FloatType:
|
||||
searchWorker<float>(cpu, searchResults, type, comparison, start, end, value.toFloat());
|
||||
break;
|
||||
case SearchType::DoubleType:
|
||||
searchWorker<double>(cpu, searchResults, type, comparison, start, end, value.toDouble());
|
||||
break;
|
||||
case SearchType::StringType:
|
||||
searchWorkerByteArray(cpu, type, comparison, searchResults, start, end, value.toUtf8());
|
||||
break;
|
||||
case SearchType::ArrayType:
|
||||
searchWorkerByteArray(cpu, type, comparison, searchResults, start, end, QByteArray::fromHex(value.toUtf8()));
|
||||
break;
|
||||
default:
|
||||
Console.Error("Debugger: Unknown type when doing memory search!");
|
||||
return {};
|
||||
};
|
||||
return searchResults;
|
||||
}
|
||||
|
||||
void MemorySearchView::onSearchButtonClicked()
|
||||
{
|
||||
if (!cpu().isAlive())
|
||||
return;
|
||||
|
||||
const SearchType searchType = getCurrentSearchType();
|
||||
const bool searchHex = m_ui.chkSearchHex->isChecked();
|
||||
|
||||
bool ok;
|
||||
const u32 searchStart = m_ui.txtSearchStart->text().toUInt(&ok, 16);
|
||||
|
||||
if (!ok)
|
||||
{
|
||||
QMessageBox::critical(this, tr("Debugger"), tr("Invalid start address"));
|
||||
return;
|
||||
}
|
||||
|
||||
const u32 searchEnd = m_ui.txtSearchEnd->text().toUInt(&ok, 16);
|
||||
|
||||
if (!ok)
|
||||
{
|
||||
QMessageBox::critical(this, tr("Debugger"), tr("Invalid end address"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (searchStart >= searchEnd)
|
||||
{
|
||||
QMessageBox::critical(this, tr("Debugger"), tr("Start address can't be equal to or greater than the end address"));
|
||||
return;
|
||||
}
|
||||
|
||||
const QString searchValue = m_ui.txtSearchValue->text();
|
||||
const SearchComparison searchComparison = getCurrentSearchComparison();
|
||||
const bool isFilterSearch = sender() == m_ui.btnFilterSearch;
|
||||
unsigned long long value;
|
||||
|
||||
if (searchComparison != SearchComparison::UnknownValue)
|
||||
{
|
||||
if (doesSearchComparisonTakeInput(searchComparison))
|
||||
{
|
||||
switch (searchType)
|
||||
{
|
||||
case SearchType::ByteType:
|
||||
case SearchType::Int16Type:
|
||||
case SearchType::Int32Type:
|
||||
case SearchType::Int64Type:
|
||||
value = searchValue.toULongLong(&ok, searchHex ? 16 : 10);
|
||||
break;
|
||||
case SearchType::FloatType:
|
||||
case SearchType::DoubleType:
|
||||
searchValue.toDouble(&ok);
|
||||
break;
|
||||
case SearchType::StringType:
|
||||
ok = !searchValue.isEmpty();
|
||||
break;
|
||||
case SearchType::ArrayType:
|
||||
ok = !searchValue.trimmed().isEmpty();
|
||||
break;
|
||||
}
|
||||
|
||||
if (!ok)
|
||||
{
|
||||
QMessageBox::critical(this, tr("Debugger"), tr("Invalid search value"));
|
||||
return;
|
||||
}
|
||||
|
||||
switch (searchType)
|
||||
{
|
||||
case SearchType::ArrayType:
|
||||
case SearchType::StringType:
|
||||
case SearchType::DoubleType:
|
||||
case SearchType::FloatType:
|
||||
break;
|
||||
case SearchType::Int64Type:
|
||||
if (value <= std::numeric_limits<unsigned long long>::max())
|
||||
break;
|
||||
case SearchType::Int32Type:
|
||||
if (value <= std::numeric_limits<unsigned long>::max())
|
||||
break;
|
||||
case SearchType::Int16Type:
|
||||
if (value <= std::numeric_limits<unsigned short>::max())
|
||||
break;
|
||||
case SearchType::ByteType:
|
||||
if (value <= std::numeric_limits<unsigned char>::max())
|
||||
break;
|
||||
default:
|
||||
QMessageBox::critical(this, tr("Debugger"), tr("Value is larger than type"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!isFilterSearch &&
|
||||
(searchComparison == SearchComparison::Changed ||
|
||||
searchComparison == SearchComparison::ChangedBy ||
|
||||
searchComparison == SearchComparison::Decreased ||
|
||||
searchComparison == SearchComparison::DecreasedBy ||
|
||||
searchComparison == SearchComparison::Increased ||
|
||||
searchComparison == SearchComparison::IncreasedBy ||
|
||||
searchComparison == SearchComparison::NotChanged))
|
||||
{
|
||||
QMessageBox::critical(this, tr("Debugger"), tr("This search comparison can only be used with filter searches."));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!isFilterSearch && (searchComparison == SearchComparison::Changed ||
|
||||
searchComparison == SearchComparison::ChangedBy ||
|
||||
searchComparison == SearchComparison::Decreased ||
|
||||
searchComparison == SearchComparison::DecreasedBy ||
|
||||
searchComparison == SearchComparison::Increased ||
|
||||
searchComparison == SearchComparison::IncreasedBy ||
|
||||
searchComparison == SearchComparison::NotChanged))
|
||||
{
|
||||
QMessageBox::critical(this, tr("Debugger"), tr("This search comparison can only be used with filter searches."));
|
||||
return;
|
||||
}
|
||||
|
||||
QFutureWatcher<std::vector<SearchResult>>* workerWatcher = new QFutureWatcher<std::vector<SearchResult>>();
|
||||
auto onSearchFinished = [this, workerWatcher] {
|
||||
m_ui.btnSearch->setDisabled(false);
|
||||
|
||||
m_ui.listSearchResults->clear();
|
||||
const auto& results = workerWatcher->future().result();
|
||||
|
||||
m_searchResults = std::move(results);
|
||||
loadSearchResults();
|
||||
m_ui.resultsCountLabel->setText(QString(tr("%0 results found")).arg(m_searchResults.size()));
|
||||
m_ui.btnFilterSearch->setDisabled(m_ui.listSearchResults->count() == 0);
|
||||
updateSearchComparisonSelections();
|
||||
delete workerWatcher;
|
||||
};
|
||||
connect(workerWatcher, &QFutureWatcher<std::vector<u32>>::finished, onSearchFinished);
|
||||
|
||||
m_ui.btnSearch->setDisabled(true);
|
||||
if (!isFilterSearch)
|
||||
{
|
||||
m_searchResults.clear();
|
||||
}
|
||||
|
||||
QFuture<std::vector<SearchResult>> workerFuture = QtConcurrent::run(startWorker, &cpu(), searchType, searchComparison, std::move(m_searchResults), searchStart, searchEnd, searchValue, searchHex ? 16 : 10);
|
||||
workerWatcher->setFuture(workerFuture);
|
||||
connect(workerWatcher, &QFutureWatcher<std::vector<SearchResult>>::finished, onSearchFinished);
|
||||
m_searchResults.clear();
|
||||
m_ui.resultsCountLabel->setText(tr("Searching..."));
|
||||
m_ui.resultsCountLabel->setVisible(true);
|
||||
}
|
||||
|
||||
void MemorySearchView::onSearchResultsListScroll(u32 value)
|
||||
{
|
||||
const bool hasResultsToLoad = static_cast<size_t>(m_ui.listSearchResults->count()) < m_searchResults.size();
|
||||
const bool scrolledSufficiently = value > (m_ui.listSearchResults->verticalScrollBar()->maximum() * 0.95);
|
||||
if (!m_resultsLoadTimer.isActive() && hasResultsToLoad && scrolledSufficiently)
|
||||
{
|
||||
// Load results once timer ends, allowing us to debounce repeated requests and only do one load.
|
||||
m_resultsLoadTimer.start();
|
||||
}
|
||||
}
|
||||
|
||||
void MemorySearchView::loadSearchResults()
|
||||
{
|
||||
const u32 numLoaded = m_ui.listSearchResults->count();
|
||||
const u32 amountLeftToLoad = m_searchResults.size() - numLoaded;
|
||||
if (amountLeftToLoad < 1)
|
||||
return;
|
||||
|
||||
const bool isFirstLoad = numLoaded == 0;
|
||||
const u32 maxLoadAmount = isFirstLoad ? m_initialResultsLoadLimit : m_numResultsAddedPerLoad;
|
||||
const u32 numToLoad = amountLeftToLoad > maxLoadAmount ? maxLoadAmount : amountLeftToLoad;
|
||||
|
||||
for (u32 i = 0; i < numToLoad; i++)
|
||||
{
|
||||
const u32 address = m_searchResults.at(numLoaded + i).getAddress();
|
||||
QListWidgetItem* item = new QListWidgetItem(QtUtils::FilledQStringFromValue(address, 16));
|
||||
item->setData(Qt::UserRole, address);
|
||||
m_ui.listSearchResults->addItem(item);
|
||||
}
|
||||
}
|
||||
|
||||
SearchType MemorySearchView::getCurrentSearchType()
|
||||
{
|
||||
return static_cast<SearchType>(m_ui.cmbSearchType->currentIndex());
|
||||
}
|
||||
|
||||
SearchComparison MemorySearchView::getCurrentSearchComparison()
|
||||
{
|
||||
// Note: The index can't be converted directly to the enum value since we change what comparisons are shown.
|
||||
return m_searchComparisonLabelMap.labelToEnum(m_ui.cmbSearchComparison->currentText());
|
||||
}
|
||||
|
||||
bool MemorySearchView::doesSearchComparisonTakeInput(const SearchComparison comparison)
|
||||
{
|
||||
switch (comparison)
|
||||
{
|
||||
case SearchComparison::Equals:
|
||||
case SearchComparison::NotEquals:
|
||||
case SearchComparison::GreaterThan:
|
||||
case SearchComparison::GreaterThanOrEqual:
|
||||
case SearchComparison::LessThan:
|
||||
case SearchComparison::LessThanOrEqual:
|
||||
case SearchComparison::IncreasedBy:
|
||||
case SearchComparison::DecreasedBy:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void MemorySearchView::onSearchTypeChanged(int newIndex)
|
||||
{
|
||||
if (newIndex < 4)
|
||||
m_ui.chkSearchHex->setEnabled(true);
|
||||
else
|
||||
m_ui.chkSearchHex->setEnabled(false);
|
||||
|
||||
// Clear existing search results when the comparison type changes
|
||||
if (m_searchResults.size() > 0 && (int)(m_searchResults.front().getType()) != newIndex)
|
||||
{
|
||||
m_searchResults.clear();
|
||||
m_ui.btnSearch->setDisabled(false);
|
||||
m_ui.btnFilterSearch->setDisabled(true);
|
||||
}
|
||||
updateSearchComparisonSelections();
|
||||
}
|
||||
|
||||
void MemorySearchView::onSearchComparisonChanged(int newValue)
|
||||
{
|
||||
m_ui.txtSearchValue->setEnabled(getCurrentSearchComparison() != SearchComparison::UnknownValue);
|
||||
}
|
||||
|
||||
void MemorySearchView::updateSearchComparisonSelections()
|
||||
{
|
||||
const QString selectedComparisonLabel = m_ui.cmbSearchComparison->currentText();
|
||||
const SearchComparison selectedComparison = m_searchComparisonLabelMap.labelToEnum(selectedComparisonLabel);
|
||||
|
||||
const std::vector<SearchComparison> comparisons = getValidSearchComparisonsForState(getCurrentSearchType(), m_searchResults);
|
||||
m_ui.cmbSearchComparison->clear();
|
||||
for (const SearchComparison comparison : comparisons)
|
||||
{
|
||||
m_ui.cmbSearchComparison->addItem(m_searchComparisonLabelMap.enumToLabel(comparison));
|
||||
}
|
||||
|
||||
// Preserve selection if applicable
|
||||
if (selectedComparison == SearchComparison::Invalid)
|
||||
return;
|
||||
if (std::find(comparisons.begin(), comparisons.end(), selectedComparison) != comparisons.end())
|
||||
m_ui.cmbSearchComparison->setCurrentText(selectedComparisonLabel);
|
||||
}
|
||||
|
||||
std::vector<SearchComparison> MemorySearchView::getValidSearchComparisonsForState(SearchType type, std::vector<SearchResult>& existingResults)
|
||||
{
|
||||
const bool hasResults = existingResults.size() > 0;
|
||||
std::vector<SearchComparison> comparisons = {SearchComparison::Equals};
|
||||
|
||||
if (type == SearchType::ArrayType || type == SearchType::StringType)
|
||||
{
|
||||
if (hasResults && existingResults.front().isArrayValue())
|
||||
{
|
||||
comparisons.push_back(SearchComparison::NotEquals);
|
||||
comparisons.push_back(SearchComparison::Changed);
|
||||
comparisons.push_back(SearchComparison::NotChanged);
|
||||
}
|
||||
return comparisons;
|
||||
}
|
||||
comparisons.push_back(SearchComparison::NotEquals);
|
||||
comparisons.push_back(SearchComparison::GreaterThan);
|
||||
comparisons.push_back(SearchComparison::GreaterThanOrEqual);
|
||||
comparisons.push_back(SearchComparison::LessThan);
|
||||
comparisons.push_back(SearchComparison::LessThanOrEqual);
|
||||
|
||||
if (hasResults && existingResults.front().getType() == type)
|
||||
{
|
||||
comparisons.push_back(SearchComparison::Increased);
|
||||
comparisons.push_back(SearchComparison::IncreasedBy);
|
||||
comparisons.push_back(SearchComparison::Decreased);
|
||||
comparisons.push_back(SearchComparison::DecreasedBy);
|
||||
comparisons.push_back(SearchComparison::Changed);
|
||||
comparisons.push_back(SearchComparison::ChangedBy);
|
||||
comparisons.push_back(SearchComparison::NotChanged);
|
||||
}
|
||||
|
||||
if (!hasResults)
|
||||
{
|
||||
comparisons.push_back(SearchComparison::UnknownValue);
|
||||
}
|
||||
|
||||
return comparisons;
|
||||
}
|
||||
150
pcsx2-qt/Debugger/Memory/MemorySearchView.h
Normal file
150
pcsx2-qt/Debugger/Memory/MemorySearchView.h
Normal file
@@ -0,0 +1,150 @@
|
||||
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
|
||||
// SPDX-License-Identifier: GPL-3.0+
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "ui_MemorySearchView.h"
|
||||
|
||||
#include "Debugger/DebuggerView.h"
|
||||
|
||||
#include "DebugTools/DebugInterface.h"
|
||||
|
||||
#include <QtWidgets/QWidget>
|
||||
#include <QtCore/QTimer>
|
||||
#include <QtCore/QMap>
|
||||
|
||||
class MemorySearchView final : public DebuggerView
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
MemorySearchView(const DebuggerViewParameters& parameters);
|
||||
~MemorySearchView() = default;
|
||||
|
||||
enum class SearchType
|
||||
{
|
||||
ByteType,
|
||||
Int16Type,
|
||||
Int32Type,
|
||||
Int64Type,
|
||||
FloatType,
|
||||
DoubleType,
|
||||
StringType,
|
||||
ArrayType
|
||||
};
|
||||
|
||||
// Note: The order of these enum values must reflect the order in thee Search Comparison combobox.
|
||||
enum class SearchComparison
|
||||
{
|
||||
Equals,
|
||||
NotEquals,
|
||||
GreaterThan,
|
||||
GreaterThanOrEqual,
|
||||
LessThan,
|
||||
LessThanOrEqual,
|
||||
Increased,
|
||||
IncreasedBy,
|
||||
Decreased,
|
||||
DecreasedBy,
|
||||
Changed,
|
||||
ChangedBy,
|
||||
NotChanged,
|
||||
UnknownValue,
|
||||
Invalid
|
||||
};
|
||||
|
||||
class SearchComparisonLabelMap
|
||||
{
|
||||
public:
|
||||
SearchComparisonLabelMap()
|
||||
{
|
||||
insert(SearchComparison::Equals, tr("Equals"));
|
||||
insert(SearchComparison::NotEquals, tr("Not Equals"));
|
||||
insert(SearchComparison::GreaterThan, tr("Greater Than"));
|
||||
insert(SearchComparison::GreaterThanOrEqual, tr("Greater Than Or Equal"));
|
||||
insert(SearchComparison::LessThan, tr("Less Than"));
|
||||
insert(SearchComparison::LessThanOrEqual, tr("Less Than Or Equal"));
|
||||
insert(SearchComparison::Increased, tr("Increased"));
|
||||
insert(SearchComparison::IncreasedBy, tr("Increased By"));
|
||||
insert(SearchComparison::Decreased, tr("Decreased"));
|
||||
insert(SearchComparison::DecreasedBy, tr("Decreased By"));
|
||||
insert(SearchComparison::Changed, tr("Changed"));
|
||||
insert(SearchComparison::ChangedBy, tr("Changed By"));
|
||||
insert(SearchComparison::NotChanged, tr("Not Changed"));
|
||||
insert(SearchComparison::UnknownValue, tr("Unknown Initial Value"));
|
||||
insert(SearchComparison::Invalid, "");
|
||||
}
|
||||
SearchComparison labelToEnum(QString comparisonLabel)
|
||||
{
|
||||
return labelToEnumMap.value(comparisonLabel, SearchComparison::Invalid);
|
||||
}
|
||||
QString enumToLabel(SearchComparison comparison)
|
||||
{
|
||||
return enumToLabelMap.value(comparison, "");
|
||||
}
|
||||
|
||||
private:
|
||||
QMap<SearchComparison, QString> enumToLabelMap;
|
||||
QMap<QString, SearchComparison> labelToEnumMap;
|
||||
void insert(SearchComparison comparison, QString comparisonLabel)
|
||||
{
|
||||
enumToLabelMap.insert(comparison, comparisonLabel);
|
||||
labelToEnumMap.insert(comparisonLabel, comparison);
|
||||
};
|
||||
};
|
||||
|
||||
class SearchResult
|
||||
{
|
||||
private:
|
||||
u32 address;
|
||||
QVariant value;
|
||||
SearchType type;
|
||||
|
||||
public:
|
||||
SearchResult() {}
|
||||
SearchResult(u32 address, const QVariant& value, SearchType type)
|
||||
: address(address)
|
||||
, value(value)
|
||||
, type(type)
|
||||
{
|
||||
}
|
||||
bool isIntegerValue() const { return type == SearchType::ByteType || type == SearchType::Int16Type || type == SearchType::Int32Type || type == SearchType::Int64Type; }
|
||||
bool isFloatValue() const { return type == SearchType::FloatType; }
|
||||
bool isDoubleValue() const { return type == SearchType::DoubleType; }
|
||||
bool isArrayValue() const { return type == SearchType::ArrayType || type == SearchType::StringType; }
|
||||
u32 getAddress() const { return address; }
|
||||
SearchType getType() const { return type; }
|
||||
QByteArray getArrayValue() const { return isArrayValue() ? value.toByteArray() : QByteArray(); }
|
||||
|
||||
template <typename T>
|
||||
T getValue() const
|
||||
{
|
||||
return value.value<T>();
|
||||
}
|
||||
};
|
||||
|
||||
public slots:
|
||||
void onSearchButtonClicked();
|
||||
void onSearchResultsListScroll(u32 value);
|
||||
void onSearchTypeChanged(int newIndex);
|
||||
void onSearchComparisonChanged(int newIndex);
|
||||
void loadSearchResults();
|
||||
void contextRemoveSearchResult();
|
||||
void contextCopySearchResultAddress();
|
||||
void onListSearchResultsContextMenu(QPoint pos);
|
||||
|
||||
private:
|
||||
std::vector<SearchResult> m_searchResults;
|
||||
SearchComparisonLabelMap m_searchComparisonLabelMap;
|
||||
Ui::MemorySearchView m_ui;
|
||||
QTimer m_resultsLoadTimer;
|
||||
|
||||
u32 m_initialResultsLoadLimit = 20000;
|
||||
u32 m_numResultsAddedPerLoad = 10000;
|
||||
|
||||
void updateSearchComparisonSelections();
|
||||
std::vector<SearchComparison> getValidSearchComparisonsForState(SearchType type, std::vector<SearchResult>& existingResults);
|
||||
SearchType getCurrentSearchType();
|
||||
SearchComparison getCurrentSearchComparison();
|
||||
bool doesSearchComparisonTakeInput(SearchComparison comparison);
|
||||
};
|
||||
211
pcsx2-qt/Debugger/Memory/MemorySearchView.ui
Normal file
211
pcsx2-qt/Debugger/Memory/MemorySearchView.ui
Normal file
@@ -0,0 +1,211 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>MemorySearchView</class>
|
||||
<widget class="QWidget" name="MemorySearchView">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>400</width>
|
||||
<height>300</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string/>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_3">
|
||||
<item>
|
||||
<layout class="QGridLayout" name="gridLayout_2">
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="valueLabel">
|
||||
<property name="text">
|
||||
<string>Value</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1">
|
||||
<widget class="QLineEdit" name="txtSearchValue"/>
|
||||
</item>
|
||||
<item row="2" column="0">
|
||||
<widget class="QLabel" name="typeLabel">
|
||||
<property name="text">
|
||||
<string>Type</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="1">
|
||||
<widget class="QComboBox" name="cmbSearchType">
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>1 Byte (8 bits)</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>2 Bytes (16 bits)</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>4 Bytes (32 bits)</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>8 Bytes (64 bits)</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Float</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Double</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>String</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Byte Array</string>
|
||||
</property>
|
||||
</item>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="2">
|
||||
<widget class="QLabel" name="hexLabel">
|
||||
<property name="text">
|
||||
<string>Hex</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="3">
|
||||
<widget class="QCheckBox" name="chkSearchHex">
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
<property name="checked">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="2" colspan="2">
|
||||
<widget class="QPushButton" name="btnSearch">
|
||||
<property name="text">
|
||||
<string>Search</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="2" colspan="2">
|
||||
<widget class="QPushButton" name="btnFilterSearch">
|
||||
<property name="enabled">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Filter Search</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<widget class="QComboBox" name="cmbSearchComparison">
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Equals</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Not Equals</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Greater Than</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Greater Than Or Equal</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Less Than</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Less Than Or Equal</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Unknown Initial Value</string>
|
||||
</property>
|
||||
</item>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QLabel" name="comparisonLabel">
|
||||
<property name="text">
|
||||
<string>Comparison</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QGridLayout" name="gridLayout_3">
|
||||
<item row="0" column="0" alignment="Qt::AlignLeft">
|
||||
<widget class="QLabel" name="startLabel">
|
||||
<property name="text">
|
||||
<string>Start</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1" alignment="Qt::AlignLeft">
|
||||
<widget class="QLineEdit" name="txtSearchStart">
|
||||
<property name="text">
|
||||
<string notr="true">0x00</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="2">
|
||||
<widget class="QLabel" name="endLabel">
|
||||
<property name="text">
|
||||
<string>End</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="3">
|
||||
<widget class="QLineEdit" name="txtSearchEnd">
|
||||
<property name="text">
|
||||
<string notr="true">0x2000000</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QListWidget" name="listSearchResults"/>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="resultsCountLabel">
|
||||
<property name="visible">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
||||
713
pcsx2-qt/Debugger/Memory/MemoryView.cpp
Normal file
713
pcsx2-qt/Debugger/Memory/MemoryView.cpp
Normal file
@@ -0,0 +1,713 @@
|
||||
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
|
||||
// SPDX-License-Identifier: GPL-3.0+
|
||||
|
||||
#include "MemoryView.h"
|
||||
|
||||
#include "Debugger/JsonValueWrapper.h"
|
||||
|
||||
#include "QtHost.h"
|
||||
#include "QtUtils.h"
|
||||
#include <QtCore/QObject>
|
||||
#include <QtGui/QActionGroup>
|
||||
#include <QtGui/QClipboard>
|
||||
#include <QtGui/QMouseEvent>
|
||||
#include <QtWidgets/QInputDialog>
|
||||
#include <QtWidgets/QMessageBox>
|
||||
|
||||
using namespace QtUtils;
|
||||
|
||||
/*
|
||||
MemoryViewTable
|
||||
*/
|
||||
void MemoryViewTable::UpdateStartAddress(u32 start)
|
||||
{
|
||||
startAddress = start & ~0xF;
|
||||
}
|
||||
|
||||
void MemoryViewTable::UpdateSelectedAddress(u32 selected, bool page)
|
||||
{
|
||||
selectedAddress = selected;
|
||||
if (startAddress > selectedAddress)
|
||||
{
|
||||
if (page)
|
||||
startAddress -= 0x10 * rowVisible;
|
||||
else
|
||||
startAddress -= 0x10;
|
||||
}
|
||||
else if (startAddress + ((rowVisible - 1) * 0x10) < selectedAddress)
|
||||
{
|
||||
if (page)
|
||||
startAddress += 0x10 * rowVisible;
|
||||
else
|
||||
startAddress += 0x10;
|
||||
}
|
||||
}
|
||||
|
||||
void MemoryViewTable::DrawTable(QPainter& painter, const QPalette& palette, s32 height, DebugInterface& cpu)
|
||||
{
|
||||
rowHeight = painter.fontMetrics().height() + 2;
|
||||
const s32 charWidth = painter.fontMetrics().averageCharWidth();
|
||||
const s32 x = charWidth; // Left padding
|
||||
const s32 y = rowHeight;
|
||||
rowVisible = (height / rowHeight);
|
||||
rowCount = rowVisible + 1;
|
||||
|
||||
row1YAxis = 0;
|
||||
|
||||
// Draw the row addresses
|
||||
painter.setPen(palette.text().color());
|
||||
for (u32 i = 0; i < rowCount; i++)
|
||||
{
|
||||
painter.drawText(x, y + (rowHeight * i), FilledQStringFromValue(startAddress + (i * 0x10), 16));
|
||||
}
|
||||
valuexAxis = x + (charWidth * 8);
|
||||
|
||||
// Draw the row values
|
||||
for (u32 i = 0; i < rowCount; i++)
|
||||
{
|
||||
const u32 currentRowAddress = startAddress + (i * 0x10);
|
||||
s32 valX = valuexAxis;
|
||||
segmentXAxis[0] = valX;
|
||||
for (int j = 0; j < 16 / static_cast<s32>(displayType); j++)
|
||||
{
|
||||
valX += charWidth;
|
||||
const u32 thisSegmentsStart = currentRowAddress + (j * static_cast<s32>(displayType));
|
||||
|
||||
segmentXAxis[j] = valX;
|
||||
|
||||
bool penDefault = false;
|
||||
if ((selectedAddress & ~0xF) == currentRowAddress)
|
||||
{
|
||||
if (selectedAddress >= thisSegmentsStart && selectedAddress < (thisSegmentsStart + static_cast<s32>(displayType)))
|
||||
{ // If the current byte and row we are drawing is selected
|
||||
if (!selectedText)
|
||||
{
|
||||
s32 charsIntoSegment = ((selectedAddress - thisSegmentsStart) * 2) + ((selectedNibbleHI ? 0 : 1) ^ littleEndian);
|
||||
if (littleEndian)
|
||||
charsIntoSegment = (static_cast<s32>(displayType) * 2) - charsIntoSegment - 1;
|
||||
painter.setPen(QColor::fromRgb(205, 165, 0)); // SELECTED NIBBLE LINE COLOUR
|
||||
const QPoint lineStart(valX + (charsIntoSegment * charWidth) + 1, y + (rowHeight * i));
|
||||
painter.drawLine(lineStart, lineStart + QPoint(charWidth - 3, 0));
|
||||
}
|
||||
painter.setPen(QColor::fromRgb(0xaa, 0x22, 0x22)); // SELECTED BYTE COLOUR
|
||||
}
|
||||
else
|
||||
{
|
||||
penDefault = true;
|
||||
painter.setPen(palette.text().color()); // Default colour
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
penDefault = true;
|
||||
painter.setPen(palette.text().color()); // Default colour
|
||||
}
|
||||
|
||||
bool valid;
|
||||
switch (displayType)
|
||||
{
|
||||
case MemoryViewType::BYTE:
|
||||
{
|
||||
const u8 val = static_cast<u8>(cpu.read8(thisSegmentsStart, valid));
|
||||
if (penDefault && val == 0)
|
||||
painter.setPen(QColor::fromRgb(145, 145, 155)); // ZERO BYTE COLOUR
|
||||
painter.drawText(valX, y + (rowHeight * i), valid ? FilledQStringFromValue(val, 16) : "??");
|
||||
break;
|
||||
}
|
||||
case MemoryViewType::BYTEHW:
|
||||
{
|
||||
const u16 val = convertEndian<u16>(static_cast<u16>(cpu.read16(thisSegmentsStart, valid)));
|
||||
if (penDefault && val == 0)
|
||||
painter.setPen(QColor::fromRgb(145, 145, 155)); // ZERO BYTE COLOUR
|
||||
painter.drawText(valX, y + (rowHeight * i), valid ? FilledQStringFromValue(val, 16) : "????");
|
||||
break;
|
||||
}
|
||||
case MemoryViewType::WORD:
|
||||
{
|
||||
const u32 val = convertEndian<u32>(cpu.read32(thisSegmentsStart, valid));
|
||||
if (penDefault && val == 0)
|
||||
painter.setPen(QColor::fromRgb(145, 145, 155)); // ZERO BYTE COLOUR
|
||||
painter.drawText(valX, y + (rowHeight * i), valid ? FilledQStringFromValue(val, 16) : "????????");
|
||||
break;
|
||||
}
|
||||
case MemoryViewType::DWORD:
|
||||
{
|
||||
const u64 val = convertEndian<u64>(cpu.read64(thisSegmentsStart, valid));
|
||||
if (penDefault && val == 0)
|
||||
painter.setPen(QColor::fromRgb(145, 145, 155)); // ZERO BYTE COLOUR
|
||||
painter.drawText(valX, y + (rowHeight * i), valid ? FilledQStringFromValue(val, 16) : "????????????????");
|
||||
break;
|
||||
}
|
||||
}
|
||||
valX += charWidth * 2 * static_cast<s32>(displayType);
|
||||
}
|
||||
|
||||
// valX is our new X position after the hex values
|
||||
valX = valX + 6;
|
||||
textXAxis = valX;
|
||||
|
||||
// Print the string representation
|
||||
for (s32 j = 0; j < 16; j++)
|
||||
{
|
||||
if (selectedAddress == j + currentRowAddress)
|
||||
painter.setPen(palette.highlight().color());
|
||||
else
|
||||
painter.setPen(palette.text().color());
|
||||
|
||||
bool valid;
|
||||
const u8 value = cpu.read8(currentRowAddress + j, valid);
|
||||
if (valid)
|
||||
{
|
||||
QChar curChar = QChar::fromLatin1(value);
|
||||
if (!curChar.isPrint() && curChar != ' ') // Default to '.' for unprintable characters
|
||||
curChar = '.';
|
||||
|
||||
painter.drawText(valX, y + (rowHeight * i), curChar);
|
||||
}
|
||||
else
|
||||
{
|
||||
painter.drawText(valX, y + (rowHeight * i), "?");
|
||||
}
|
||||
valX += charWidth + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void MemoryViewTable::SelectAt(QPoint pos)
|
||||
{
|
||||
// Check if SelectAt was called before DrawTable.
|
||||
if (rowHeight == 0)
|
||||
return;
|
||||
|
||||
const u32 selectedRow = (pos.y() - 2) / (rowHeight);
|
||||
const s32 x = pos.x();
|
||||
const s32 avgSegmentWidth = segmentXAxis[1] - segmentXAxis[0];
|
||||
const u32 nibbleWidth = (avgSegmentWidth / (2 * (s32)displayType));
|
||||
selectedAddress = (selectedRow * 0x10) + startAddress;
|
||||
|
||||
if (x <= segmentXAxis[0])
|
||||
{
|
||||
// The user clicked before the first segment
|
||||
selectedText = false;
|
||||
if (littleEndian)
|
||||
selectedAddress += static_cast<s32>(displayType) - 1;
|
||||
selectedNibbleHI = true;
|
||||
}
|
||||
else if (x > valuexAxis && x < textXAxis)
|
||||
{
|
||||
selectedText = false;
|
||||
// The user clicked inside of the hexadecimal area
|
||||
for (s32 i = 0; i < 16; i++)
|
||||
{
|
||||
if (i == ((16 / static_cast<s32>(displayType)) - 1) || (x >= segmentXAxis[i] && x < (segmentXAxis[i + 1])))
|
||||
{
|
||||
u32 indexInSegment = (x - segmentXAxis[i]) / nibbleWidth;
|
||||
if (littleEndian)
|
||||
indexInSegment = (static_cast<s32>(displayType) * 2) - indexInSegment - 1;
|
||||
selectedAddress = selectedAddress + i * static_cast<s32>(displayType) + (indexInSegment / 2);
|
||||
selectedNibbleHI = littleEndian ? indexInSegment & 1 : !(indexInSegment & 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (x >= textXAxis)
|
||||
{
|
||||
selectedText = true;
|
||||
// The user clicked the text area
|
||||
selectedAddress += std::min((x - textXAxis) / 8, 15);
|
||||
}
|
||||
}
|
||||
|
||||
u128 MemoryViewTable::GetSelectedSegment(DebugInterface& cpu)
|
||||
{
|
||||
u128 val;
|
||||
switch (displayType)
|
||||
{
|
||||
case MemoryViewType::BYTE:
|
||||
val.lo = cpu.read8(selectedAddress);
|
||||
break;
|
||||
case MemoryViewType::BYTEHW:
|
||||
val.lo = convertEndian(static_cast<u16>(cpu.read16(selectedAddress & ~1)));
|
||||
break;
|
||||
case MemoryViewType::WORD:
|
||||
val.lo = convertEndian(cpu.read32(selectedAddress & ~3));
|
||||
break;
|
||||
case MemoryViewType::DWORD:
|
||||
val._u64[0] = convertEndian(cpu.read64(selectedAddress & ~7));
|
||||
break;
|
||||
}
|
||||
return val;
|
||||
}
|
||||
|
||||
void MemoryViewTable::InsertIntoSelectedHexView(u8 value, DebugInterface& cpu)
|
||||
{
|
||||
const u8 mask = selectedNibbleHI ? 0x0f : 0xf0;
|
||||
u8 curVal = cpu.read8(selectedAddress) & mask;
|
||||
u8 newVal = value << (selectedNibbleHI ? 4 : 0);
|
||||
curVal |= newVal;
|
||||
|
||||
Host::RunOnCPUThread([this, address = selectedAddress, &cpu, val = curVal] {
|
||||
cpu.write8(address, val);
|
||||
QtHost::RunOnUIThread([this] { parent->update(); });
|
||||
});
|
||||
}
|
||||
|
||||
void MemoryViewTable::InsertAtCurrentSelection(const QString& text, DebugInterface& cpu)
|
||||
{
|
||||
if (!cpu.isValidAddress(selectedAddress))
|
||||
return;
|
||||
|
||||
// If pasting into the hex view, also decode the input as hex bytes.
|
||||
// This approach prevents one from pasting on a nibble boundary, but that is almost always
|
||||
// user error, and we don't have an undo function in this view, so best to stay conservative.
|
||||
QByteArray input = selectedText ? text.toUtf8() : QByteArray::fromHex(text.toUtf8());
|
||||
Host::RunOnCPUThread([this, address = selectedAddress, &cpu, inBytes = input] {
|
||||
u32 currAddr = address;
|
||||
for (int i = 0; i < inBytes.size(); i++)
|
||||
{
|
||||
cpu.write8(currAddr, inBytes[i]);
|
||||
currAddr = nextAddress(currAddr);
|
||||
QtHost::RunOnUIThread([this] { parent->update(); });
|
||||
}
|
||||
QtHost::RunOnUIThread([this, inBytes] { UpdateSelectedAddress(selectedAddress + inBytes.size()); parent->update(); });
|
||||
});
|
||||
}
|
||||
|
||||
u32 MemoryViewTable::nextAddress(u32 addr)
|
||||
{
|
||||
if (!littleEndian)
|
||||
{
|
||||
return addr + 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (selectedAddress % static_cast<s32>(displayType) == 0)
|
||||
return addr + (static_cast<s32>(displayType) * 2 - 1);
|
||||
else
|
||||
return addr - 1;
|
||||
}
|
||||
}
|
||||
|
||||
u32 MemoryViewTable::prevAddress(u32 addr)
|
||||
{
|
||||
if (!littleEndian)
|
||||
{
|
||||
return addr - 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
// It works
|
||||
if ((addr & (static_cast<u32>(displayType) - 1)) == (static_cast<u32>(displayType) - 1))
|
||||
return addr - (static_cast<s32>(displayType) * 2 - 1);
|
||||
else
|
||||
return selectedAddress + 1;
|
||||
}
|
||||
}
|
||||
|
||||
void MemoryViewTable::ForwardSelection()
|
||||
{
|
||||
if (!littleEndian)
|
||||
{
|
||||
if ((selectedNibbleHI = !selectedNibbleHI))
|
||||
UpdateSelectedAddress(selectedAddress + 1);
|
||||
}
|
||||
else
|
||||
{
|
||||
if ((selectedNibbleHI = !selectedNibbleHI))
|
||||
{
|
||||
if (selectedAddress % static_cast<s32>(displayType) == 0)
|
||||
UpdateSelectedAddress(selectedAddress + (static_cast<s32>(displayType) * 2 - 1));
|
||||
else
|
||||
UpdateSelectedAddress(selectedAddress - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void MemoryViewTable::BackwardSelection()
|
||||
{
|
||||
if (!littleEndian)
|
||||
{
|
||||
if (!(selectedNibbleHI = !selectedNibbleHI))
|
||||
UpdateSelectedAddress(selectedAddress - 1);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!(selectedNibbleHI = !selectedNibbleHI))
|
||||
{
|
||||
// It works
|
||||
if ((selectedAddress & (static_cast<u32>(displayType) - 1)) == (static_cast<u32>(displayType) - 1))
|
||||
UpdateSelectedAddress(selectedAddress - (static_cast<s32>(displayType) * 2 - 1));
|
||||
else
|
||||
UpdateSelectedAddress(selectedAddress + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// We need both key and keychar because `key` is easy to use, but is case insensitive
|
||||
bool MemoryViewTable::KeyPress(int key, QChar keychar, DebugInterface& cpu)
|
||||
{
|
||||
if (!cpu.isValidAddress(selectedAddress))
|
||||
return false;
|
||||
|
||||
bool pressHandled = false;
|
||||
|
||||
const bool keyCharIsText = keychar.isLetterOrNumber() || keychar.isSpace();
|
||||
|
||||
if (selectedText)
|
||||
{
|
||||
if (keyCharIsText || (!keychar.isNonCharacter() && keychar.category() != QChar::Other_Control))
|
||||
{
|
||||
Host::RunOnCPUThread([this, address = selectedAddress, &cpu, val = keychar.toLatin1()] {
|
||||
cpu.write8(address, val);
|
||||
QtHost::RunOnUIThread([this] { UpdateSelectedAddress(selectedAddress + 1); parent->update(); });
|
||||
});
|
||||
pressHandled = true;
|
||||
}
|
||||
|
||||
switch (key)
|
||||
{
|
||||
case Qt::Key::Key_Backspace:
|
||||
case Qt::Key::Key_Escape:
|
||||
Host::RunOnCPUThread([this, address = selectedAddress, &cpu] {
|
||||
cpu.write8(address, 0);
|
||||
QtHost::RunOnUIThread([this] {BackwardSelection(); parent->update(); });
|
||||
});
|
||||
pressHandled = true;
|
||||
break;
|
||||
case Qt::Key::Key_Right:
|
||||
ForwardSelection();
|
||||
pressHandled = true;
|
||||
break;
|
||||
case Qt::Key::Key_Left:
|
||||
BackwardSelection();
|
||||
pressHandled = true;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Hex view is selected
|
||||
|
||||
if (keyCharIsText)
|
||||
{
|
||||
// Check if key pressed is hex before insertion (QString conversion fails otherwise)
|
||||
const u8 keyPressed = static_cast<u8>(QString(QChar(key)).toInt(&pressHandled, 16));
|
||||
if (pressHandled)
|
||||
{
|
||||
InsertIntoSelectedHexView(keyPressed, cpu);
|
||||
ForwardSelection();
|
||||
}
|
||||
}
|
||||
|
||||
switch (key)
|
||||
{
|
||||
case Qt::Key::Key_Backspace:
|
||||
case Qt::Key::Key_Escape:
|
||||
InsertIntoSelectedHexView(0, cpu);
|
||||
BackwardSelection();
|
||||
pressHandled = true;
|
||||
break;
|
||||
case Qt::Key::Key_Right:
|
||||
ForwardSelection();
|
||||
pressHandled = true;
|
||||
break;
|
||||
case Qt::Key::Key_Left:
|
||||
BackwardSelection();
|
||||
pressHandled = true;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Keybinds that are the same for the text and hex view
|
||||
|
||||
switch (key)
|
||||
{
|
||||
case Qt::Key::Key_Up:
|
||||
UpdateSelectedAddress(selectedAddress - 0x10);
|
||||
pressHandled = true;
|
||||
break;
|
||||
case Qt::Key::Key_PageUp:
|
||||
UpdateSelectedAddress(selectedAddress - (0x10 * rowVisible), true);
|
||||
pressHandled = true;
|
||||
break;
|
||||
case Qt::Key::Key_Down:
|
||||
UpdateSelectedAddress(selectedAddress + 0x10);
|
||||
pressHandled = true;
|
||||
break;
|
||||
case Qt::Key::Key_PageDown:
|
||||
UpdateSelectedAddress(selectedAddress + (0x10 * rowVisible), true);
|
||||
pressHandled = true;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return pressHandled;
|
||||
}
|
||||
|
||||
/*
|
||||
MemoryView
|
||||
*/
|
||||
MemoryView::MemoryView(const DebuggerViewParameters& parameters)
|
||||
: DebuggerView(parameters, MONOSPACE_FONT)
|
||||
, m_table(this)
|
||||
{
|
||||
ui.setupUi(this);
|
||||
|
||||
setFocusPolicy(Qt::FocusPolicy::ClickFocus);
|
||||
|
||||
setContextMenuPolicy(Qt::CustomContextMenu);
|
||||
connect(this, &MemoryView::customContextMenuRequested, this, &MemoryView::openContextMenu);
|
||||
|
||||
m_table.UpdateStartAddress(0x100000);
|
||||
|
||||
receiveEvent<DebuggerEvents::Refresh>([this](const DebuggerEvents::Refresh& event) -> bool {
|
||||
update();
|
||||
return true;
|
||||
});
|
||||
|
||||
receiveEvent<DebuggerEvents::GoToAddress>([this](const DebuggerEvents::GoToAddress& event) -> bool {
|
||||
if (event.filter != DebuggerEvents::GoToAddress::NONE &&
|
||||
event.filter != DebuggerEvents::GoToAddress::MEMORY_VIEW)
|
||||
return false;
|
||||
|
||||
gotoAddress(event.address);
|
||||
|
||||
if (event.switch_to_tab)
|
||||
switchToThisTab();
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
MemoryView::~MemoryView() = default;
|
||||
|
||||
void MemoryView::toJson(JsonValueWrapper& json)
|
||||
{
|
||||
DebuggerView::toJson(json);
|
||||
|
||||
json.value().AddMember("startAddress", m_table.startAddress, json.allocator());
|
||||
json.value().AddMember("viewType", static_cast<int>(m_table.GetViewType()), json.allocator());
|
||||
json.value().AddMember("littleEndian", m_table.GetLittleEndian(), json.allocator());
|
||||
}
|
||||
|
||||
bool MemoryView::fromJson(const JsonValueWrapper& json)
|
||||
{
|
||||
if (!DebuggerView::fromJson(json))
|
||||
return false;
|
||||
|
||||
auto start_address = json.value().FindMember("startAddress");
|
||||
if (start_address != json.value().MemberEnd() && start_address->value.IsUint())
|
||||
m_table.UpdateStartAddress(start_address->value.GetUint());
|
||||
|
||||
auto view_type = json.value().FindMember("viewType");
|
||||
if (view_type != json.value().MemberEnd() && view_type->value.IsInt())
|
||||
{
|
||||
MemoryViewType type = static_cast<MemoryViewType>(view_type->value.GetInt());
|
||||
if (type == MemoryViewType::BYTE ||
|
||||
type == MemoryViewType::BYTEHW ||
|
||||
type == MemoryViewType::WORD ||
|
||||
type == MemoryViewType::DWORD)
|
||||
m_table.SetViewType(type);
|
||||
}
|
||||
|
||||
auto little_endian = json.value().FindMember("littleEndian");
|
||||
if (little_endian != json.value().MemberEnd() && little_endian->value.IsBool())
|
||||
m_table.SetLittleEndian(little_endian->value.GetBool());
|
||||
|
||||
repaint();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void MemoryView::paintEvent(QPaintEvent* event)
|
||||
{
|
||||
QPainter painter(this);
|
||||
|
||||
painter.fillRect(rect(), palette().window());
|
||||
|
||||
if (!cpu().isAlive())
|
||||
return;
|
||||
|
||||
m_table.DrawTable(painter, this->palette(), this->height(), cpu());
|
||||
}
|
||||
|
||||
void MemoryView::mousePressEvent(QMouseEvent* event)
|
||||
{
|
||||
if (!cpu().isAlive())
|
||||
return;
|
||||
|
||||
m_table.SelectAt(event->pos());
|
||||
repaint();
|
||||
}
|
||||
|
||||
void MemoryView::openContextMenu(QPoint pos)
|
||||
{
|
||||
if (!cpu().isAlive())
|
||||
return;
|
||||
|
||||
QMenu* menu = new QMenu(this);
|
||||
menu->setAttribute(Qt::WA_DeleteOnClose);
|
||||
|
||||
QAction* copy_action = menu->addAction(tr("Copy Address"));
|
||||
connect(copy_action, &QAction::triggered, this, [this]() {
|
||||
QApplication::clipboard()->setText(QString::number(m_table.selectedAddress, 16).toUpper());
|
||||
});
|
||||
|
||||
createEventActions<DebuggerEvents::GoToAddress>(menu, [this]() {
|
||||
DebuggerEvents::GoToAddress event;
|
||||
event.address = m_table.selectedAddress;
|
||||
return std::optional(event);
|
||||
});
|
||||
|
||||
QAction* go_to_address_action = menu->addAction(tr("Go to address"));
|
||||
connect(go_to_address_action, &QAction::triggered, this, [this]() { contextGoToAddress(); });
|
||||
|
||||
menu->addSeparator();
|
||||
|
||||
QAction* endian_action = menu->addAction(tr("Show as Little Endian"));
|
||||
endian_action->setCheckable(true);
|
||||
endian_action->setChecked(m_table.GetLittleEndian());
|
||||
connect(endian_action, &QAction::triggered, this, [this, endian_action]() {
|
||||
m_table.SetLittleEndian(endian_action->isChecked());
|
||||
});
|
||||
|
||||
const MemoryViewType current_view_type = m_table.GetViewType();
|
||||
|
||||
// View Types
|
||||
QActionGroup* view_type_group = new QActionGroup(menu);
|
||||
view_type_group->setExclusive(true);
|
||||
|
||||
QAction* byte_action = menu->addAction(tr("Show as 1 byte"));
|
||||
byte_action->setCheckable(true);
|
||||
byte_action->setChecked(current_view_type == MemoryViewType::BYTE);
|
||||
connect(byte_action, &QAction::triggered, this, [this]() { m_table.SetViewType(MemoryViewType::BYTE); });
|
||||
view_type_group->addAction(byte_action);
|
||||
|
||||
QAction* bytehw_action = menu->addAction(tr("Show as 2 bytes"));
|
||||
bytehw_action->setCheckable(true);
|
||||
bytehw_action->setChecked(current_view_type == MemoryViewType::BYTEHW);
|
||||
connect(bytehw_action, &QAction::triggered, this, [this]() { m_table.SetViewType(MemoryViewType::BYTEHW); });
|
||||
view_type_group->addAction(bytehw_action);
|
||||
|
||||
QAction* word_action = menu->addAction(tr("Show as 4 bytes"));
|
||||
word_action->setCheckable(true);
|
||||
word_action->setChecked(current_view_type == MemoryViewType::WORD);
|
||||
connect(word_action, &QAction::triggered, this, [this]() { m_table.SetViewType(MemoryViewType::WORD); });
|
||||
view_type_group->addAction(word_action);
|
||||
|
||||
QAction* dword_action = menu->addAction(tr("Show as 8 bytes"));
|
||||
dword_action->setCheckable(true);
|
||||
dword_action->setChecked(current_view_type == MemoryViewType::DWORD);
|
||||
connect(dword_action, &QAction::triggered, this, [this]() { m_table.SetViewType(MemoryViewType::DWORD); });
|
||||
view_type_group->addAction(dword_action);
|
||||
|
||||
menu->addSeparator();
|
||||
|
||||
createEventActions<DebuggerEvents::AddToSavedAddresses>(menu, [this]() {
|
||||
DebuggerEvents::AddToSavedAddresses event;
|
||||
event.address = m_table.selectedAddress;
|
||||
return std::optional(event);
|
||||
});
|
||||
|
||||
connect(menu->addAction(tr("Copy Byte")), &QAction::triggered, this, &MemoryView::contextCopyByte);
|
||||
connect(menu->addAction(tr("Copy Segment")), &QAction::triggered, this, &MemoryView::contextCopySegment);
|
||||
connect(menu->addAction(tr("Copy Character")), &QAction::triggered, this, &MemoryView::contextCopyCharacter);
|
||||
connect(menu->addAction(tr("Paste")), &QAction::triggered, this, &MemoryView::contextPaste);
|
||||
|
||||
menu->popup(this->mapToGlobal(pos));
|
||||
|
||||
this->repaint();
|
||||
return;
|
||||
}
|
||||
|
||||
void MemoryView::contextCopyByte()
|
||||
{
|
||||
QApplication::clipboard()->setText(QString::number(cpu().read8(m_table.selectedAddress), 16).toUpper());
|
||||
}
|
||||
|
||||
void MemoryView::contextCopySegment()
|
||||
{
|
||||
QApplication::clipboard()->setText(QString::number(m_table.GetSelectedSegment(cpu()).lo, 16).toUpper());
|
||||
}
|
||||
|
||||
void MemoryView::contextCopyCharacter()
|
||||
{
|
||||
QApplication::clipboard()->setText(QChar::fromLatin1(cpu().read8(m_table.selectedAddress)).toUpper());
|
||||
}
|
||||
|
||||
void MemoryView::contextPaste()
|
||||
{
|
||||
m_table.InsertAtCurrentSelection(QApplication::clipboard()->text(), cpu());
|
||||
}
|
||||
|
||||
void MemoryView::contextGoToAddress()
|
||||
{
|
||||
bool ok;
|
||||
QString targetString = QInputDialog::getText(this, tr("Go To In Memory View"), "",
|
||||
QLineEdit::Normal, "", &ok);
|
||||
|
||||
if (!ok)
|
||||
return;
|
||||
|
||||
u64 address = 0;
|
||||
std::string error;
|
||||
if (!cpu().evaluateExpression(targetString.toStdString().c_str(), address, error))
|
||||
{
|
||||
QMessageBox::warning(this, tr("Cannot Go To"), QString::fromStdString(error));
|
||||
return;
|
||||
}
|
||||
|
||||
gotoAddress(static_cast<u32>(address));
|
||||
}
|
||||
|
||||
void MemoryView::mouseDoubleClickEvent(QMouseEvent* event)
|
||||
{
|
||||
}
|
||||
|
||||
void MemoryView::wheelEvent(QWheelEvent* event)
|
||||
{
|
||||
if (event->angleDelta().y() < 0)
|
||||
{
|
||||
m_table.UpdateStartAddress(m_table.startAddress + 0x10);
|
||||
}
|
||||
else if (event->angleDelta().y() > 0)
|
||||
{
|
||||
m_table.UpdateStartAddress(m_table.startAddress - 0x10);
|
||||
}
|
||||
this->repaint();
|
||||
}
|
||||
|
||||
void MemoryView::keyPressEvent(QKeyEvent* event)
|
||||
{
|
||||
if (!m_table.KeyPress(event->key(), event->text().size() ? event->text()[0] : '\0', cpu()))
|
||||
{
|
||||
switch (event->key())
|
||||
{
|
||||
case Qt::Key_G:
|
||||
contextGoToAddress();
|
||||
break;
|
||||
case Qt::Key_C:
|
||||
if (event->modifiers() & Qt::ControlModifier)
|
||||
contextCopySegment();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
this->repaint();
|
||||
DebuggerView::broadcastEvent(DebuggerEvents::VMUpdate());
|
||||
}
|
||||
|
||||
void MemoryView::gotoAddress(u32 address)
|
||||
{
|
||||
m_table.UpdateStartAddress(address & ~0xF);
|
||||
m_table.selectedAddress = address;
|
||||
this->repaint();
|
||||
this->setFocus();
|
||||
}
|
||||
139
pcsx2-qt/Debugger/Memory/MemoryView.h
Normal file
139
pcsx2-qt/Debugger/Memory/MemoryView.h
Normal file
@@ -0,0 +1,139 @@
|
||||
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
|
||||
// SPDX-License-Identifier: GPL-3.0+
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "ui_MemoryView.h"
|
||||
|
||||
#include "Debugger/DebuggerView.h"
|
||||
|
||||
#include "DebugTools/DebugInterface.h"
|
||||
#include "DebugTools/DisassemblyManager.h"
|
||||
|
||||
#include <QtWidgets/QWidget>
|
||||
#include <QtWidgets/QMenu>
|
||||
#include <QtWidgets/QTabBar>
|
||||
#include <QtGui/QPainter>
|
||||
#include <QtCore/QtEndian>
|
||||
|
||||
#include <vector>
|
||||
|
||||
enum class MemoryViewType
|
||||
{
|
||||
BYTE = 1,
|
||||
BYTEHW = 2,
|
||||
WORD = 4,
|
||||
DWORD = 8,
|
||||
};
|
||||
|
||||
class MemoryViewTable
|
||||
{
|
||||
QWidget* parent;
|
||||
MemoryViewType displayType = MemoryViewType::BYTE;
|
||||
bool littleEndian = true;
|
||||
u32 rowCount;
|
||||
u32 rowVisible;
|
||||
s32 rowHeight;
|
||||
|
||||
// Stuff used for selection handling
|
||||
// This gets set every paint and depends on the window size / current display mode (1byte,2byte,etc)
|
||||
s32 valuexAxis; // Where the hexadecimal view begins
|
||||
s32 textXAxis; // Where the text view begins
|
||||
s32 row1YAxis; // Where the first row starts
|
||||
s32 segmentXAxis[16]; // Where the segments begin
|
||||
bool selectedText = false; // Whether the user has clicked on text or hex
|
||||
|
||||
bool selectedNibbleHI = false;
|
||||
|
||||
void InsertIntoSelectedHexView(u8 value, DebugInterface& cpu);
|
||||
|
||||
template <class T>
|
||||
T convertEndian(T in)
|
||||
{
|
||||
if (littleEndian)
|
||||
{
|
||||
return in;
|
||||
}
|
||||
else
|
||||
{
|
||||
return qToBigEndian(in);
|
||||
}
|
||||
}
|
||||
|
||||
u32 nextAddress(u32 addr);
|
||||
u32 prevAddress(u32 addr);
|
||||
|
||||
public:
|
||||
MemoryViewTable(QWidget* parent)
|
||||
: parent(parent)
|
||||
{
|
||||
}
|
||||
|
||||
u32 startAddress;
|
||||
u32 selectedAddress;
|
||||
|
||||
void UpdateStartAddress(u32 start);
|
||||
void UpdateSelectedAddress(u32 selected, bool page = false);
|
||||
void DrawTable(QPainter& painter, const QPalette& palette, s32 height, DebugInterface& cpu);
|
||||
void SelectAt(QPoint pos);
|
||||
u128 GetSelectedSegment(DebugInterface& cpu);
|
||||
void InsertAtCurrentSelection(const QString& text, DebugInterface& cpu);
|
||||
void ForwardSelection();
|
||||
void BackwardSelection();
|
||||
// Returns true if the keypress was handled
|
||||
bool KeyPress(int key, QChar keychar, DebugInterface& cpu);
|
||||
|
||||
MemoryViewType GetViewType()
|
||||
{
|
||||
return displayType;
|
||||
}
|
||||
|
||||
void SetViewType(MemoryViewType viewType)
|
||||
{
|
||||
displayType = viewType;
|
||||
}
|
||||
|
||||
bool GetLittleEndian()
|
||||
{
|
||||
return littleEndian;
|
||||
}
|
||||
|
||||
void SetLittleEndian(bool le)
|
||||
{
|
||||
littleEndian = le;
|
||||
}
|
||||
};
|
||||
|
||||
class MemoryView final : public DebuggerView
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
MemoryView(const DebuggerViewParameters& parameters);
|
||||
~MemoryView();
|
||||
|
||||
void toJson(JsonValueWrapper& json) override;
|
||||
bool fromJson(const JsonValueWrapper& json) override;
|
||||
|
||||
protected:
|
||||
void paintEvent(QPaintEvent* event) override;
|
||||
void mousePressEvent(QMouseEvent* event) override;
|
||||
void mouseDoubleClickEvent(QMouseEvent* event) override;
|
||||
void wheelEvent(QWheelEvent* event) override;
|
||||
void keyPressEvent(QKeyEvent* event) override;
|
||||
|
||||
public slots:
|
||||
void openContextMenu(QPoint pos);
|
||||
|
||||
void contextGoToAddress();
|
||||
void contextCopyByte();
|
||||
void contextCopySegment();
|
||||
void contextCopyCharacter();
|
||||
void contextPaste();
|
||||
void gotoAddress(u32 address);
|
||||
|
||||
private:
|
||||
Ui::MemoryView ui;
|
||||
|
||||
MemoryViewTable m_table;
|
||||
};
|
||||
19
pcsx2-qt/Debugger/Memory/MemoryView.ui
Normal file
19
pcsx2-qt/Debugger/Memory/MemoryView.ui
Normal file
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>MemoryView</class>
|
||||
<widget class="QWidget" name="MemoryView">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>400</width>
|
||||
<height>300</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Memory</string>
|
||||
</property>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
||||
221
pcsx2-qt/Debugger/Memory/SavedAddressesModel.cpp
Normal file
221
pcsx2-qt/Debugger/Memory/SavedAddressesModel.cpp
Normal file
@@ -0,0 +1,221 @@
|
||||
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
|
||||
// SPDX-License-Identifier: GPL-3.0+
|
||||
|
||||
#include "PrecompiledHeader.h"
|
||||
#include "SavedAddressesModel.h"
|
||||
|
||||
#include "common/Console.h"
|
||||
|
||||
std::map<BreakPointCpu, SavedAddressesModel*> SavedAddressesModel::s_instances;
|
||||
|
||||
SavedAddressesModel::SavedAddressesModel(DebugInterface& cpu, QObject* parent)
|
||||
: QAbstractTableModel(parent)
|
||||
, m_cpu(cpu)
|
||||
{
|
||||
}
|
||||
|
||||
SavedAddressesModel* SavedAddressesModel::getInstance(DebugInterface& cpu)
|
||||
{
|
||||
auto iterator = s_instances.find(cpu.getCpuType());
|
||||
if (iterator == s_instances.end())
|
||||
iterator = s_instances.emplace(cpu.getCpuType(), new SavedAddressesModel(cpu)).first;
|
||||
|
||||
return iterator->second;
|
||||
}
|
||||
|
||||
QVariant SavedAddressesModel::data(const QModelIndex& index, int role) const
|
||||
{
|
||||
size_t row = static_cast<size_t>(index.row());
|
||||
if (!index.isValid() || row >= m_savedAddresses.size())
|
||||
return false;
|
||||
|
||||
const SavedAddress& entry = m_savedAddresses[row];
|
||||
|
||||
if (role == Qt::CheckStateRole)
|
||||
return QVariant();
|
||||
|
||||
if (role == Qt::DisplayRole || role == Qt::EditRole)
|
||||
{
|
||||
switch (index.column())
|
||||
{
|
||||
case HeaderColumns::ADDRESS:
|
||||
return QString::number(entry.address, 16).toUpper();
|
||||
case HeaderColumns::LABEL:
|
||||
return entry.label;
|
||||
case HeaderColumns::DESCRIPTION:
|
||||
return entry.description;
|
||||
}
|
||||
}
|
||||
if (role == Qt::UserRole)
|
||||
{
|
||||
switch (index.column())
|
||||
{
|
||||
case HeaderColumns::ADDRESS:
|
||||
return entry.address;
|
||||
case HeaderColumns::LABEL:
|
||||
return entry.label;
|
||||
case HeaderColumns::DESCRIPTION:
|
||||
return entry.description;
|
||||
}
|
||||
}
|
||||
|
||||
return QVariant();
|
||||
}
|
||||
|
||||
bool SavedAddressesModel::setData(const QModelIndex& index, const QVariant& value, int role)
|
||||
{
|
||||
size_t row = static_cast<size_t>(index.row());
|
||||
if (!index.isValid() || row >= m_savedAddresses.size())
|
||||
return false;
|
||||
|
||||
SavedAddress& entry = m_savedAddresses[row];
|
||||
|
||||
if (role == Qt::CheckStateRole)
|
||||
return false;
|
||||
|
||||
if (role == Qt::EditRole)
|
||||
{
|
||||
if (index.column() == HeaderColumns::ADDRESS)
|
||||
{
|
||||
bool ok = false;
|
||||
const u32 address = value.toString().toUInt(&ok, 16);
|
||||
if (ok)
|
||||
entry.address = address;
|
||||
else
|
||||
return false;
|
||||
}
|
||||
|
||||
if (index.column() == HeaderColumns::DESCRIPTION)
|
||||
entry.description = value.toString();
|
||||
|
||||
if (index.column() == HeaderColumns::LABEL)
|
||||
entry.label = value.toString();
|
||||
|
||||
emit dataChanged(index, index, QList<int>(role));
|
||||
return true;
|
||||
}
|
||||
else if (role == Qt::UserRole)
|
||||
{
|
||||
if (index.column() == HeaderColumns::ADDRESS)
|
||||
{
|
||||
const u32 address = value.toUInt();
|
||||
entry.address = address;
|
||||
}
|
||||
|
||||
if (index.column() == HeaderColumns::DESCRIPTION)
|
||||
entry.description = value.toString();
|
||||
|
||||
if (index.column() == HeaderColumns::LABEL)
|
||||
entry.label = value.toString();
|
||||
|
||||
emit dataChanged(index, index, QList<int>(role));
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
QVariant SavedAddressesModel::headerData(int section, Qt::Orientation orientation, int role) const
|
||||
{
|
||||
if (orientation != Qt::Horizontal)
|
||||
return QVariant();
|
||||
|
||||
if (role == Qt::DisplayRole)
|
||||
{
|
||||
switch (section)
|
||||
{
|
||||
case SavedAddressesModel::ADDRESS:
|
||||
return tr("MEMORY ADDRESS");
|
||||
case SavedAddressesModel::LABEL:
|
||||
return tr("LABEL");
|
||||
case SavedAddressesModel::DESCRIPTION:
|
||||
return tr("DESCRIPTION");
|
||||
default:
|
||||
return QVariant();
|
||||
}
|
||||
}
|
||||
if (role == Qt::UserRole)
|
||||
{
|
||||
switch (section)
|
||||
{
|
||||
case SavedAddressesModel::ADDRESS:
|
||||
return "MEMORY ADDRESS";
|
||||
case SavedAddressesModel::LABEL:
|
||||
return "LABEL";
|
||||
case SavedAddressesModel::DESCRIPTION:
|
||||
return "DESCRIPTION";
|
||||
default:
|
||||
return QVariant();
|
||||
}
|
||||
}
|
||||
return QVariant();
|
||||
}
|
||||
|
||||
Qt::ItemFlags SavedAddressesModel::flags(const QModelIndex& index) const
|
||||
{
|
||||
return Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsEditable;
|
||||
}
|
||||
|
||||
void SavedAddressesModel::addRow()
|
||||
{
|
||||
const SavedAddress defaultNewAddress = {0, "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 < 0 || count < 1 || static_cast<size_t>(row + count) > m_savedAddresses.size())
|
||||
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;
|
||||
}
|
||||
|
||||
void SavedAddressesModel::loadSavedAddressFromFieldList(QStringList fields)
|
||||
{
|
||||
if (fields.size() != SavedAddressesModel::HeaderColumns::COLUMN_COUNT)
|
||||
{
|
||||
Console.WriteLn("Debugger Saved Addresses Model: Invalid number of columns, skipping");
|
||||
return;
|
||||
}
|
||||
|
||||
bool ok;
|
||||
const u32 address = fields[SavedAddressesModel::HeaderColumns::ADDRESS].toUInt(&ok, 16);
|
||||
if (!ok)
|
||||
{
|
||||
Console.WriteLn("Debugger Saved Addresses Model: Failed to parse address '%s', skipping", fields[SavedAddressesModel::HeaderColumns::ADDRESS].toUtf8().constData());
|
||||
return;
|
||||
}
|
||||
|
||||
const QString label = fields[SavedAddressesModel::HeaderColumns::LABEL];
|
||||
const QString description = fields[SavedAddressesModel::HeaderColumns::DESCRIPTION];
|
||||
const SavedAddressesModel::SavedAddress importedAddress = {address, label, description};
|
||||
addRow(importedAddress);
|
||||
}
|
||||
|
||||
void SavedAddressesModel::clear()
|
||||
{
|
||||
beginResetModel();
|
||||
m_savedAddresses.clear();
|
||||
endResetModel();
|
||||
}
|
||||
58
pcsx2-qt/Debugger/Memory/SavedAddressesModel.h
Normal file
58
pcsx2-qt/Debugger/Memory/SavedAddressesModel.h
Normal file
@@ -0,0 +1,58 @@
|
||||
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
|
||||
// SPDX-License-Identifier: GPL-3.0+
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QtCore/QAbstractTableModel>
|
||||
#include <QtWidgets/QHeaderView>
|
||||
|
||||
#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,
|
||||
};
|
||||
|
||||
static SavedAddressesModel* getInstance(DebugInterface& cpu);
|
||||
|
||||
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;
|
||||
void loadSavedAddressFromFieldList(QStringList fields);
|
||||
void clear();
|
||||
|
||||
private:
|
||||
SavedAddressesModel(DebugInterface& cpu, QObject* parent = nullptr);
|
||||
|
||||
DebugInterface& m_cpu;
|
||||
std::vector<SavedAddress> m_savedAddresses;
|
||||
|
||||
static std::map<BreakPointCpu, SavedAddressesModel*> s_instances;
|
||||
};
|
||||
166
pcsx2-qt/Debugger/Memory/SavedAddressesView.cpp
Normal file
166
pcsx2-qt/Debugger/Memory/SavedAddressesView.cpp
Normal file
@@ -0,0 +1,166 @@
|
||||
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
|
||||
// SPDX-License-Identifier: GPL-3.0+
|
||||
|
||||
#include "SavedAddressesView.h"
|
||||
|
||||
#include "QtUtils.h"
|
||||
#include "Debugger/DebuggerSettingsManager.h"
|
||||
|
||||
#include <QtGui/QClipboard>
|
||||
#include <QtWidgets/QMenu>
|
||||
|
||||
SavedAddressesView::SavedAddressesView(const DebuggerViewParameters& parameters)
|
||||
: DebuggerView(parameters, DISALLOW_MULTIPLE_INSTANCES)
|
||||
, m_model(SavedAddressesModel::getInstance(cpu()))
|
||||
{
|
||||
m_ui.setupUi(this);
|
||||
|
||||
m_ui.savedAddressesList->setModel(m_model);
|
||||
|
||||
m_ui.savedAddressesList->setContextMenuPolicy(Qt::CustomContextMenu);
|
||||
connect(m_ui.savedAddressesList, &QTableView::customContextMenuRequested,
|
||||
this, &SavedAddressesView::openContextMenu);
|
||||
|
||||
connect(g_emu_thread, &EmuThread::onGameChanged, this, [this](const QString& title) {
|
||||
if (title.isEmpty())
|
||||
return;
|
||||
|
||||
if (m_model->rowCount() == 0)
|
||||
DebuggerSettingsManager::loadGameSettings(m_model);
|
||||
});
|
||||
|
||||
DebuggerSettingsManager::loadGameSettings(m_model);
|
||||
|
||||
for (std::size_t i = 0; auto mode : SavedAddressesModel::HeaderResizeModes)
|
||||
{
|
||||
m_ui.savedAddressesList->horizontalHeader()->setSectionResizeMode(i++, mode);
|
||||
}
|
||||
|
||||
QTableView* savedAddressesTableView = m_ui.savedAddressesList;
|
||||
connect(m_model, &QAbstractItemModel::dataChanged, this, [savedAddressesTableView](const QModelIndex& topLeft) {
|
||||
savedAddressesTableView->resizeColumnToContents(topLeft.column());
|
||||
});
|
||||
|
||||
receiveEvent<DebuggerEvents::AddToSavedAddresses>([this](const DebuggerEvents::AddToSavedAddresses& event) {
|
||||
addAddress(event.address);
|
||||
|
||||
if (event.switch_to_tab)
|
||||
switchToThisTab();
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
void SavedAddressesView::openContextMenu(QPoint pos)
|
||||
{
|
||||
QMenu* menu = new QMenu(this);
|
||||
menu->setAttribute(Qt::WA_DeleteOnClose);
|
||||
|
||||
QAction* new_action = menu->addAction(tr("New"));
|
||||
connect(new_action, &QAction::triggered, this, &SavedAddressesView::contextNew);
|
||||
|
||||
const QModelIndex index_at_pos = m_ui.savedAddressesList->indexAt(pos);
|
||||
const bool is_index_valid = index_at_pos.isValid();
|
||||
bool is_cpu_alive = cpu().isAlive();
|
||||
|
||||
std::vector<QAction*> go_to_actions = createEventActions<DebuggerEvents::GoToAddress>(
|
||||
menu, [this, index_at_pos]() {
|
||||
const QModelIndex rowAddressIndex = m_model->index(index_at_pos.row(), 0, QModelIndex());
|
||||
|
||||
DebuggerEvents::GoToAddress event;
|
||||
event.address = m_model->data(rowAddressIndex, Qt::UserRole).toUInt();
|
||||
return std::optional(event);
|
||||
});
|
||||
|
||||
for (QAction* go_to_action : go_to_actions)
|
||||
go_to_action->setEnabled(is_index_valid);
|
||||
|
||||
QAction* copy_action = menu->addAction(index_at_pos.column() == 0 ? tr("Copy Address") : tr("Copy Text"));
|
||||
copy_action->setEnabled(is_index_valid);
|
||||
connect(copy_action, &QAction::triggered, [this, index_at_pos]() {
|
||||
QGuiApplication::clipboard()->setText(
|
||||
m_model->data(index_at_pos, Qt::DisplayRole).toString());
|
||||
});
|
||||
|
||||
if (m_model->rowCount() > 0)
|
||||
{
|
||||
QAction* copy_all_as_csv_action = menu->addAction(tr("Copy all as CSV"));
|
||||
connect(copy_all_as_csv_action, &QAction::triggered, [this]() {
|
||||
QGuiApplication::clipboard()->setText(
|
||||
QtUtils::AbstractItemModelToCSV(m_ui.savedAddressesList->model(), Qt::DisplayRole, true));
|
||||
});
|
||||
}
|
||||
|
||||
QAction* paste_from_csv_action = menu->addAction(tr("Paste from CSV"));
|
||||
connect(paste_from_csv_action, &QAction::triggered, this, &SavedAddressesView::contextPasteCSV);
|
||||
|
||||
QAction* load_action = menu->addAction(tr("Load from Settings"));
|
||||
load_action->setEnabled(is_cpu_alive);
|
||||
connect(load_action, &QAction::triggered, [this]() {
|
||||
m_model->clear();
|
||||
DebuggerSettingsManager::loadGameSettings(m_model);
|
||||
});
|
||||
|
||||
QAction* save_action = menu->addAction(tr("Save to Settings"));
|
||||
save_action->setEnabled(is_cpu_alive);
|
||||
connect(save_action, &QAction::triggered, this, &SavedAddressesView::saveToDebuggerSettings);
|
||||
|
||||
QAction* delete_action = menu->addAction(tr("Delete"));
|
||||
connect(delete_action, &QAction::triggered, this, [this, index_at_pos]() {
|
||||
m_model->removeRows(index_at_pos.row(), 1);
|
||||
});
|
||||
delete_action->setEnabled(is_index_valid);
|
||||
|
||||
menu->popup(m_ui.savedAddressesList->viewport()->mapToGlobal(pos));
|
||||
}
|
||||
|
||||
void SavedAddressesView::contextPasteCSV()
|
||||
{
|
||||
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 each_quote_pair(R"("([^"]|\\.)*")");
|
||||
QRegularExpressionMatchIterator it = each_quote_pair.globalMatch(line);
|
||||
while (it.hasNext())
|
||||
{
|
||||
QRegularExpressionMatch match = it.next();
|
||||
QString matched_value = match.captured(0);
|
||||
fields << matched_value.mid(1, matched_value.length() - 2);
|
||||
}
|
||||
|
||||
m_model->loadSavedAddressFromFieldList(fields);
|
||||
}
|
||||
}
|
||||
|
||||
void SavedAddressesView::contextNew()
|
||||
{
|
||||
m_model->addRow();
|
||||
const u32 row_count = m_model->rowCount();
|
||||
m_ui.savedAddressesList->edit(m_model->index(row_count - 1, 0));
|
||||
}
|
||||
|
||||
void SavedAddressesView::addAddress(u32 address)
|
||||
{
|
||||
m_model->addRow();
|
||||
|
||||
u32 row_count = m_model->rowCount();
|
||||
|
||||
QModelIndex address_index = m_model->index(row_count - 1, SavedAddressesModel::ADDRESS);
|
||||
m_model->setData(address_index, address, Qt::UserRole);
|
||||
|
||||
QModelIndex label_index = m_model->index(row_count - 1, SavedAddressesModel::LABEL);
|
||||
if (label_index.isValid())
|
||||
m_ui.savedAddressesList->edit(label_index);
|
||||
}
|
||||
|
||||
void SavedAddressesView::saveToDebuggerSettings()
|
||||
{
|
||||
DebuggerSettingsManager::saveGameSettings(m_model);
|
||||
}
|
||||
29
pcsx2-qt/Debugger/Memory/SavedAddressesView.h
Normal file
29
pcsx2-qt/Debugger/Memory/SavedAddressesView.h
Normal file
@@ -0,0 +1,29 @@
|
||||
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
|
||||
// SPDX-License-Identifier: GPL-3.0+
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "ui_SavedAddressesView.h"
|
||||
|
||||
#include "SavedAddressesModel.h"
|
||||
|
||||
#include "Debugger/DebuggerView.h"
|
||||
|
||||
class SavedAddressesView : public DebuggerView
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
SavedAddressesView(const DebuggerViewParameters& parameters);
|
||||
|
||||
void openContextMenu(QPoint pos);
|
||||
void contextPasteCSV();
|
||||
void contextNew();
|
||||
void addAddress(u32 address);
|
||||
void saveToDebuggerSettings();
|
||||
|
||||
private:
|
||||
Ui::SavedAddressesView m_ui;
|
||||
|
||||
SavedAddressesModel* m_model;
|
||||
};
|
||||
39
pcsx2-qt/Debugger/Memory/SavedAddressesView.ui
Normal file
39
pcsx2-qt/Debugger/Memory/SavedAddressesView.ui
Normal file
@@ -0,0 +1,39 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>SavedAddressesView</class>
|
||||
<widget class="QWidget" name="SavedAddressesView">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>400</width>
|
||||
<height>300</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Saved Addresses</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<property name="spacing">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="leftMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="topMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="rightMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="bottomMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QTableView" name="savedAddressesList"/>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
||||
Reference in New Issue
Block a user