Qt: Add Memory Editor window

pull/3577/head
Stenzek 4 weeks ago
parent 4ee8738771
commit ecf13ded23
No known key found for this signature in database

@ -3195,7 +3195,7 @@ bool CPU::SafeReadMemoryWord(VirtualMemoryAddress addr, u32* value)
return true;
}
bool CPU::SafeReadMemoryCString(VirtualMemoryAddress addr, std::string* value, u32 max_length /*= 1024*/)
bool CPU::SafeReadMemoryCString(VirtualMemoryAddress addr, SmallStringBase* value, u32 max_length /*= 1024*/)
{
value->clear();
@ -3206,7 +3206,7 @@ bool CPU::SafeReadMemoryCString(VirtualMemoryAddress addr, std::string* value, u
return true;
value->push_back(ch);
if (value->size() >= max_length)
if (value->length() >= max_length)
return true;
addr++;

@ -15,6 +15,7 @@
#include <vector>
class StateWrapper;
class SmallStringBase;
namespace CPU {
@ -182,7 +183,7 @@ ALWAYS_INLINE bool InKernelMode()
bool SafeReadMemoryByte(VirtualMemoryAddress addr, u8* value);
bool SafeReadMemoryHalfWord(VirtualMemoryAddress addr, u16* value);
bool SafeReadMemoryWord(VirtualMemoryAddress addr, u32* value);
bool SafeReadMemoryCString(VirtualMemoryAddress addr, std::string* value, u32 max_length = 1024);
bool SafeReadMemoryCString(VirtualMemoryAddress addr, SmallStringBase* value, u32 max_length = 1024);
bool SafeReadMemoryBytes(VirtualMemoryAddress addr, void* data, u32 length);
bool SafeWriteMemoryByte(VirtualMemoryAddress addr, u8 value);
bool SafeWriteMemoryHalfWord(VirtualMemoryAddress addr, u16 value);

@ -8,6 +8,7 @@
#include "common/file_system.h"
#include "common/log.h"
#include "common/path.h"
#include "common/small_string.h"
#include "common/string_util.h"
LOG_CHANNEL(PCDrv);
@ -77,7 +78,7 @@ static bool CloseFileHandle(u32 handle)
return true;
}
static std::string ResolveHostPath(const std::string& path)
static std::string ResolveHostPath(std::string_view path)
{
// Double-check that it falls within the directory of the root.
// Not a real sandbox, but emulators shouldn't be treated as such. Don't run untrusted code!
@ -142,7 +143,7 @@ bool PCDrv::HandleSyscall(u32 instruction_bits, CPU::Registers& regs)
const bool is_open = (code == 0x103);
const char* func = (code == 0x102) ? "PCcreat" : "PCopen";
const u32 mode = regs.a2;
std::string filename;
SmallString filename;
if (!CPU::SafeReadMemoryCString(regs.a1, &filename))
{
ERROR_LOG("{}: Invalid string", func);

@ -120,6 +120,9 @@ set(SRCS
memorycardrenamefiledialog.ui
memorycardsettingswidget.cpp
memorycardsettingswidget.h
memoryeditorwindow.cpp
memoryeditorwindow.h
memoryeditorwindow.ui
memoryscannerwindow.cpp
memoryscannerwindow.h
memoryscannerwindow.ui

@ -522,12 +522,6 @@ void DebuggerWindow::connectSignals()
m_refresh_timer.setInterval(TIMER_REFRESH_INTERVAL_MS);
}
void DebuggerWindow::disconnectSignals()
{
EmuThread* hi = g_emu_thread;
hi->disconnect(this);
}
void DebuggerWindow::createModels()
{
m_registers_model = new DebuggerRegistersModel(this);
@ -570,6 +564,8 @@ void DebuggerWindow::setUIEnabled(bool enabled, bool allow_pause)
m_ui.memoryRegionEXP1->setEnabled(read_only_views);
m_ui.memoryRegionScratchpad->setEnabled(read_only_views);
m_ui.memoryRegionBIOS->setEnabled(read_only_views);
m_ui.memorySearch->setEnabled(read_only_views);
m_ui.memorySearchString->setEnabled(read_only_views);
// Partial/timer refreshes only active when not paused.
const bool timer_active = (!enabled && allow_pause);
@ -597,11 +593,13 @@ void DebuggerWindow::setMemoryViewRegion(Bus::MemoryRegion region)
const u32 start_page = static_cast<u32>(offset) >> HOST_PAGE_SHIFT;
const u32 end_page = static_cast<u32>(offset + count - 1) >> HOST_PAGE_SHIFT;
for (u32 i = start_page; i <= end_page; i++)
{
if (Bus::g_ram_code_bits[i])
CPU::CodeCache::InvalidateBlocksWithPageIndex(i);
}
Host::RunOnCPUThread([start_page, end_page]() {
for (u32 i = start_page; i <= end_page; i++)
{
if (Bus::g_ram_code_bits[i])
CPU::CodeCache::InvalidateBlocksWithPageIndex(i);
}
});
};
const PhysicalMemoryAddress start = Bus::GetMemoryRegionStart(region);

@ -36,7 +36,6 @@ protected:
private:
void setupAdditionalUi();
void connectSignals();
void disconnectSignals();
void createModels();
void setUIEnabled(bool enabled, bool allow_pause);
void saveCurrentState();

@ -29,6 +29,7 @@
<ClCompile Include="inputbindingwidgets.cpp" />
<ClCompile Include="isobrowserwindow.cpp" />
<ClCompile Include="logwindow.cpp" />
<ClCompile Include="memoryeditorwindow.cpp" />
<ClCompile Include="memoryscannerwindow.cpp" />
<ClCompile Include="memoryviewwidget.cpp" />
<ClCompile Include="displaywidget.cpp" />
@ -79,7 +80,8 @@
<QtMoc Include="controllersettingswindow.h" />
<QtMoc Include="colorpickerbutton.h" />
<ClInclude Include="controllersettingwidgetbinder.h" />
<ClInclude Include="memoryviewwidget.h" />
<QtMoc Include="memoryeditorwindow.h" />
<QtMoc Include="memoryviewwidget.h" />
<QtMoc Include="logwindow.h" />
<QtMoc Include="graphicssettingswidget.h" />
<QtMoc Include="memoryscannerwindow.h" />
@ -315,6 +317,9 @@
<QtUi Include="postprocessingoverlayconfigwidget.ui">
<FileType>Document</FileType>
</QtUi>
<QtUi Include="memoryeditorwindow.ui">
<FileType>Document</FileType>
</QtUi>
<None Include="translations\duckstation-qt_es-es.ts" />
<None Include="translations\duckstation-qt_sv.ts" />
<None Include="translations\duckstation-qt_tr.ts" />

@ -49,6 +49,7 @@
<ClCompile Include="isobrowserwindow.cpp" />
<ClCompile Include="debuggercodeview.cpp" />
<ClCompile Include="togglebutton.cpp" />
<ClCompile Include="memoryeditorwindow.cpp" />
</ItemGroup>
<ItemGroup>
<ClInclude Include="qtutils.h" />
@ -56,7 +57,6 @@
<ClInclude Include="resource.h" />
<ClInclude Include="controllersettingwidgetbinder.h" />
<ClInclude Include="pch.h" />
<ClInclude Include="memoryviewwidget.h" />
</ItemGroup>
<ItemGroup>
<Filter Include="resources">
@ -110,6 +110,8 @@
<QtMoc Include="isobrowserwindow.h" />
<QtMoc Include="debuggercodeview.h" />
<QtMoc Include="togglebutton.h" />
<QtMoc Include="memoryeditorwindow.h" />
<QtMoc Include="memoryviewwidget.h" />
</ItemGroup>
<ItemGroup>
<QtUi Include="consolesettingswidget.ui" />
@ -163,6 +165,7 @@
<QtUi Include="memorycardrenamefiledialog.ui" />
<QtUi Include="isobrowserwindow.ui" />
<QtUi Include="postprocessingoverlayconfigwidget.ui" />
<QtUi Include="memoryeditorwindow.ui" />
</ItemGroup>
<ItemGroup>
<ResourceCompile Include="duckstation-qt.rc" />

@ -14,6 +14,7 @@
#include "isobrowserwindow.h"
#include "logwindow.h"
#include "memorycardeditorwindow.h"
#include "memoryeditorwindow.h"
#include "memoryscannerwindow.h"
#include "qthost.h"
#include "qtutils.h"
@ -881,6 +882,7 @@ void MainWindow::recreate()
void MainWindow::destroySubWindows()
{
QtUtils::CloseAndDeleteWindow(m_cover_download_window);
QtUtils::CloseAndDeleteWindow(m_memory_editor_window);
QtUtils::CloseAndDeleteWindow(m_memory_scanner_window);
QtUtils::CloseAndDeleteWindow(m_debugger_window);
QtUtils::CloseAndDeleteWindow(m_memory_card_editor_window);
@ -2135,6 +2137,7 @@ void MainWindow::updateEmulationActions(bool starting, bool running, bool achiev
m_ui.menuChangeDisc->setDisabled(starting_or_not_running);
m_ui.menuCheats->setDisabled(starting_or_not_running || achievements_hardcore_mode);
m_ui.actionCPUDebugger->setDisabled(achievements_hardcore_mode);
m_ui.actionMemoryEditor->setDisabled(achievements_hardcore_mode);
m_ui.actionMemoryScanner->setDisabled(achievements_hardcore_mode);
m_ui.actionReloadTextureReplacements->setDisabled(starting_or_not_running);
m_ui.actionDumpRAM->setDisabled(starting_or_not_running || achievements_hardcore_mode);
@ -2443,6 +2446,7 @@ void MainWindow::connectSignals()
connect(m_ui.actionAbout, &QAction::triggered, this, &MainWindow::onAboutActionTriggered);
connect(m_ui.actionCheckForUpdates, &QAction::triggered, this, &MainWindow::onCheckForUpdatesActionTriggered);
connect(m_ui.actionMemoryCardEditor, &QAction::triggered, this, &MainWindow::onToolsMemoryCardEditorTriggered);
connect(m_ui.actionMemoryEditor, &QAction::triggered, this, &MainWindow::onToolsMemoryEditorTriggered);
connect(m_ui.actionMemoryScanner, &QAction::triggered, this, &MainWindow::onToolsMemoryScannerTriggered);
connect(m_ui.actionISOBrowser, &QAction::triggered, this, &MainWindow::onToolsISOBrowserTriggered);
connect(m_ui.actionCoverDownloader, &QAction::triggered, this, &MainWindow::onToolsCoverDownloaderTriggered);
@ -2728,6 +2732,20 @@ ControllerSettingsWindow* MainWindow::getControllerSettingsWindow()
return m_controller_settings_window;
}
MemoryEditorWindow* MainWindow::getMemoryEditorWindow()
{
if (!m_memory_editor_window)
{
m_memory_editor_window = new MemoryEditorWindow();
connect(m_memory_editor_window, &MemoryEditorWindow::closed, this, [this]() {
m_memory_editor_window->deleteLater();
m_memory_editor_window = nullptr;
});
}
return m_memory_editor_window;
}
void MainWindow::doControllerSettings(
ControllerSettingsWindow::Category category /*= ControllerSettingsDialog::Category::Count*/)
{
@ -3147,6 +3165,7 @@ void MainWindow::onAchievementsHardcoreModeChanged(bool enabled)
if (enabled)
{
QtUtils::CloseAndDeleteWindow(m_debugger_window);
QtUtils::CloseAndDeleteWindow(m_memory_editor_window);
QtUtils::CloseAndDeleteWindow(m_memory_scanner_window);
}
@ -3251,6 +3270,14 @@ void MainWindow::onToolsMediaCaptureToggled(bool checked)
Host::RunOnCPUThread([path = path.toStdString()]() { System::StartMediaCapture(path); });
}
void MainWindow::onToolsMemoryEditorTriggered()
{
if (s_achievements_hardcore_mode)
return;
QtUtils::ShowOrRaiseWindow(getMemoryEditorWindow());
}
void MainWindow::onToolsMemoryScannerTriggered()
{
if (s_achievements_hardcore_mode)

@ -33,6 +33,7 @@ class AutoUpdaterWindow;
class MemoryCardEditorWindow;
class DebuggerWindow;
class MemoryScannerWindow;
class MemoryEditorWindow;
class CoverDownloadWindow;
struct SystemBootParameters;
@ -114,6 +115,7 @@ public:
/// Returns pointer to settings window.
SettingsWindow* getSettingsWindow();
ControllerSettingsWindow* getControllerSettingsWindow();
MemoryEditorWindow* getMemoryEditorWindow();
/// Updates debug menu visibility (hides if disabled).
void updateDebugMenuVisibility();
@ -292,6 +294,7 @@ private:
void onAboutActionTriggered();
void onCheckForUpdatesActionTriggered();
void onToolsMemoryCardEditorTriggered();
void onToolsMemoryEditorTriggered();
void onToolsMemoryScannerTriggered();
void onToolsISOBrowserTriggered();
void onToolsCoverDownloaderTriggered();
@ -345,6 +348,7 @@ private:
MemoryCardEditorWindow* m_memory_card_editor_window = nullptr;
DebuggerWindow* m_debugger_window = nullptr;
MemoryScannerWindow* m_memory_scanner_window = nullptr;
MemoryEditorWindow* m_memory_editor_window = nullptr;
CoverDownloadWindow* m_cover_download_window = nullptr;
bool m_was_paused_by_focus_loss = false;

@ -240,6 +240,7 @@
<addaction name="actionCoverDownloader"/>
<addaction name="actionControllerTest"/>
<addaction name="separator"/>
<addaction name="actionMemoryEditor"/>
<addaction name="actionMemoryScanner"/>
<addaction name="actionISOBrowser"/>
<addaction name="separator"/>
@ -669,12 +670,12 @@
<property name="text">
<string>&amp;Settings</string>
</property>
<property name="menuRole">
<enum>QAction::MenuRole::PreferencesRole</enum>
</property>
<property name="toolTip">
<string>Opens the settings window.</string>
</property>
<property name="menuRole">
<enum>QAction::MenuRole::PreferencesRole</enum>
</property>
</action>
<action name="actionSettings2">
<property name="icon">
@ -683,12 +684,12 @@
<property name="text">
<string>&amp;Settings</string>
</property>
<property name="menuRole">
<enum>QAction::MenuRole::PreferencesRole</enum>
</property>
<property name="toolTip">
<string>Opens the settings window.</string>
</property>
<property name="menuRole">
<enum>QAction::MenuRole::PreferencesRole</enum>
</property>
</action>
<action name="actionChangeDiscFromFile">
<property name="text">
@ -1292,6 +1293,14 @@
<string>Shows titles for games in their native language.</string>
</property>
</action>
<action name="actionMemoryEditor">
<property name="text">
<string>Memory &amp;Editor</string>
</property>
<property name="toolTip">
<string>Opens the memory editor window.</string>
</property>
</action>
</widget>
<resources>
<include location="resources/duckstation-qt.qrc"/>

@ -0,0 +1,632 @@
// SPDX-FileCopyrightText: 2019-2025 Connor McLaughlin <stenzek@gmail.com>
// SPDX-License-Identifier: CC-BY-NC-ND-4.0
#include "memoryeditorwindow.h"
#include "mainwindow.h"
#include "qthost.h"
#include "qtutils.h"
#include "core/bus.h"
#include "core/cpu_code_cache.h"
#include "core/cpu_core_private.h"
#include "common/assert.h"
#include "common/string_util.h"
#include <QtCore/QSignalBlocker>
#include <QtGui/QCursor>
#include <QtGui/QFontDatabase>
#include <QtGui/QShortcut>
#include <QtWidgets/QAbstractScrollArea>
#include <QtWidgets/QFileDialog>
#include <QtWidgets/QMessageBox>
#include <bit>
#include "moc_memoryeditorwindow.cpp"
static constexpr int TIMER_REFRESH_INTERVAL_MS = 100;
MemoryEditorWindow::MemoryEditorWindow(QWidget* parent /* = nullptr */) : QWidget(parent)
{
m_ui.setupUi(this);
setupAdditionalUi();
connectSignals();
updateUIEnabled();
updateMemoryViewRegion();
updateDataInspector();
}
MemoryEditorWindow::~MemoryEditorWindow() = default;
void MemoryEditorWindow::onSystemStarted()
{
updateUIEnabled();
}
void MemoryEditorWindow::onSystemDestroyed()
{
updateUIEnabled();
}
void MemoryEditorWindow::onSystemPaused()
{
updateUIEnabled();
refreshAll();
}
void MemoryEditorWindow::onSystemResumed()
{
updateUIEnabled();
}
void MemoryEditorWindow::timerRefresh()
{
m_ui.memoryView->forceRefresh();
updateDataInspector();
}
void MemoryEditorWindow::refreshAll()
{
m_ui.memoryView->forceRefresh();
updateDataInspector();
}
void MemoryEditorWindow::onMemoryViewTopAddressChanged(size_t address)
{
m_ui.address->setText(formatAddress(static_cast<VirtualMemoryAddress>(address)));
}
void MemoryEditorWindow::onAddressEditingFinished()
{
QString address_str = m_ui.address->text();
if (address_str.startsWith(QStringLiteral("0x")) || address_str.startsWith(QStringLiteral("0X")))
address_str = address_str.mid(2);
const std::optional<VirtualMemoryAddress> address =
StringUtil::FromChars<VirtualMemoryAddress>(address_str.toStdString(), 16);
if (!address.has_value())
{
m_ui.address->setText(formatAddress(static_cast<VirtualMemoryAddress>(m_ui.memoryView->topAddress())));
return;
}
scrollToMemoryAddress(address.value());
}
void MemoryEditorWindow::onDumpAddressTriggered()
{
std::optional<VirtualMemoryAddress> address =
QtUtils::PromptForAddress(this, windowTitle(), tr("Enter memory address:"), false);
if (!address.has_value())
return;
scrollToMemoryAddress(address.value());
}
void MemoryEditorWindow::onMemoryRegionButtonToggled(QAbstractButton*, bool checked)
{
if (!checked)
return;
updateMemoryViewRegion();
}
void MemoryEditorWindow::onDataInspectorBaseButtonToggled(QAbstractButton*, bool checked)
{
if (!checked)
return;
updateDataInspector();
}
void MemoryEditorWindow::onDataInspectorEndianButtonToggled(QAbstractButton*, bool checked)
{
if (!checked)
return;
updateDataInspector();
}
void MemoryEditorWindow::onMemorySearchTriggered()
{
m_ui.memoryView->clearHighlightRange();
const QString pattern_str = m_ui.memorySearchString->text();
if (pattern_str.isEmpty())
return;
std::vector<u8> pattern;
std::vector<u8> mask;
u8 spattern = 0;
u8 smask = 0;
bool msb = false;
pattern.reserve(static_cast<size_t>(pattern_str.length()) / 2);
mask.reserve(static_cast<size_t>(pattern_str.length()) / 2);
for (int i = 0; i < pattern_str.length(); i++)
{
const QChar ch = pattern_str[i];
if (ch == ' ')
continue;
if (ch == '?')
{
spattern = (spattern << 4);
smask = (smask << 4);
}
else if (ch.isDigit())
{
spattern = (spattern << 4) | static_cast<u8>(ch.digitValue());
smask = (smask << 4) | 0xF;
}
else if (ch.unicode() >= 'a' && ch.unicode() <= 'f')
{
spattern = (spattern << 4) | (0xA + static_cast<u8>(ch.unicode() - 'a'));
smask = (smask << 4) | 0xF;
}
else if (ch.unicode() >= 'A' && ch.unicode() <= 'F')
{
spattern = (spattern << 4) | (0xA + static_cast<u8>(ch.unicode() - 'A'));
smask = (smask << 4) | 0xF;
}
else
{
QMessageBox::critical(this, windowTitle(),
tr("Invalid search pattern. It should contain hex digits or question marks."));
return;
}
if (msb)
{
pattern.push_back(spattern);
mask.push_back(smask);
spattern = 0;
smask = 0;
}
msb = !msb;
}
if (msb)
{
// partial byte on the end
spattern = (spattern << 4);
smask = (smask << 4);
pattern.push_back(spattern);
mask.push_back(smask);
}
if (pattern.empty())
{
QMessageBox::critical(this, windowTitle(),
tr("Invalid search pattern. It should contain hex digits or question marks."));
return;
}
std::optional<PhysicalMemoryAddress> found_address =
Bus::SearchMemory(m_next_memory_search_address, pattern.data(), mask.data(), static_cast<u32>(pattern.size()));
bool wrapped_around = false;
if (!found_address.has_value())
{
found_address = Bus::SearchMemory(0, pattern.data(), mask.data(), static_cast<u32>(pattern.size()));
if (!found_address.has_value())
{
QMessageBox::critical(this, windowTitle(), tr("Pattern not found in memory."));
return;
}
wrapped_around = true;
}
m_next_memory_search_address = found_address.value() + 1;
if (scrollToMemoryAddress(found_address.value()))
{
const size_t highlight_offset = found_address.value() - m_ui.memoryView->addressOffset();
m_ui.memoryView->setHighlightRange(highlight_offset, highlight_offset + pattern.size());
}
if (wrapped_around)
{
QMessageBox::information(this, windowTitle(),
tr("Pattern found at 0x%1 (passed the end of memory).")
.arg(static_cast<uint>(found_address.value()), 8, 16, static_cast<QChar>('0')));
}
else
{
QMessageBox::information(
this, windowTitle(),
tr("Pattern found at 0x%1.").arg(static_cast<uint>(found_address.value()), 8, 16, static_cast<QChar>('0')));
}
}
void MemoryEditorWindow::onMemorySearchStringChanged(const QString&)
{
m_next_memory_search_address = 0;
}
QString MemoryEditorWindow::formatAddress(VirtualMemoryAddress address)
{
return QString::asprintf("0x%08X", static_cast<uint>(address));
}
void MemoryEditorWindow::setIfChanged(QLineEdit* const widget, const QString& text)
{
if (widget->text() == text)
return;
widget->setText(text);
widget->setCursorPosition(0);
}
QString MemoryEditorWindow::formatNumber(u64 value, bool is_signed, int byte_size) const
{
QString ret;
QString prefix;
int base;
int width;
if (m_ui.dataInspectorOctal->isChecked())
{
prefix = QStringLiteral("0");
base = 8;
width = 0;
}
else if (m_ui.dataInspectorHexadecimal->isChecked())
{
prefix = QStringLiteral("0x");
base = 16;
width = byte_size * 2;
}
else
{
// prefix = QString();
base = 10;
width = 0;
}
switch (byte_size)
{
case 1:
{
if (is_signed)
{
if (static_cast<s8>(value) < 0)
{
ret = QStringLiteral("%1").arg(static_cast<s8>(value), width, base, QChar('0'));
ret.insert(1, prefix);
}
else
{
ret = QStringLiteral("%1%2").arg(prefix).arg(static_cast<s8>(value), width, base, QChar('0'));
}
}
else
{
ret = QStringLiteral("%1%2").arg(prefix).arg(static_cast<u8>(value), width, base, QChar('0'));
}
}
break;
case 2:
{
if (is_signed)
{
if (static_cast<s16>(value) < 0)
{
ret = QStringLiteral("%1").arg(static_cast<s16>(value), width, base, QChar('0'));
ret.insert(1, prefix);
}
else
{
ret = QStringLiteral("%1%2").arg(prefix).arg(static_cast<s16>(value), width, base, QChar('0'));
}
}
else
{
ret = QStringLiteral("%1%2").arg(prefix).arg(static_cast<u16>(value), width, base, QChar('0'));
}
}
break;
case 4:
{
if (is_signed)
{
if (static_cast<s32>(value) < 0)
{
ret = QStringLiteral("%1").arg(static_cast<s32>(value), width, base, QChar('0'));
ret.insert(1, prefix);
}
else
{
ret = QStringLiteral("%1%2").arg(prefix).arg(static_cast<s32>(value), width, base, QChar('0'));
}
}
else
{
ret = QStringLiteral("%1%2").arg(prefix).arg(static_cast<u32>(value), width, base, QChar('0'));
}
}
break;
case 8:
{
if (is_signed)
{
if (static_cast<s64>(value) < 0)
{
ret = QStringLiteral("%1").arg(static_cast<s64>(value), width, base, QChar('0'));
ret.insert(1, prefix);
}
else
{
ret = QStringLiteral("%1%2").arg(prefix).arg(static_cast<s64>(value), width, base, QChar('0'));
}
}
else
{
ret = QStringLiteral("%1%2").arg(prefix).arg(static_cast<u64>(value), width, base, QChar('0'));
}
}
break;
default:
break;
}
return ret;
}
void MemoryEditorWindow::setupAdditionalUi()
{
setWindowIcon(QtHost::GetAppIcon());
#ifdef _WIN32
QFont fixedFont;
fixedFont.setFamily(QStringLiteral("Consolas"));
fixedFont.setFixedPitch(true);
fixedFont.setStyleHint(QFont::TypeWriter);
fixedFont.setPointSize(10);
#else
QFont fixedFont = QFontDatabase::systemFont(QFontDatabase::FixedFont);
#ifdef __linux__
// Fonts on Linux tend to be wider, so reduce the size.
// Otherwise the memory view gets cut off.
fixedFont.setPointSize(9);
#endif
#endif
m_ui.memoryView->setFont(fixedFont);
QtUtils::RestoreWindowGeometry("MemoryEditorWindow", this);
// Set minimum width for data inspector.
m_ui.dataInspectorAddress->setFont(fixedFont);
m_ui.dataInspectorUnsignedByte->setFont(fixedFont);
m_ui.dataInspectorSignedByte->setFont(fixedFont);
m_ui.dataInspectorUnsignedHalfword->setFont(fixedFont);
m_ui.dataInspectorSignedHalfword->setFont(fixedFont);
m_ui.dataInspectorUnsignedWord->setFont(fixedFont);
m_ui.dataInspectorSignedWord->setFont(fixedFont);
m_ui.dataInspectorUnsignedDoubleWord->setFont(fixedFont);
m_ui.dataInspectorSignedDoubleWord->setFont(fixedFont);
m_ui.dataInspectorFloat32->setFont(fixedFont);
m_ui.dataInspectorFloat64->setFont(fixedFont);
m_ui.dataInspectorASCIICharacter->setFont(fixedFont);
m_ui.dataInspectorUTF8String->setFont(fixedFont);
m_ui.dataInspectorSignedDoubleWord->setMinimumWidth(
QFontMetrics(fixedFont).size(0, QStringLiteral("-8888888888888888888888")).width());
// Default selection.
m_ui.memoryRegionRAM->setChecked(true);
m_ui.dataInspectorHexadecimal->setChecked(true);
m_ui.dataInspectorLittleEndian->setChecked(true);
}
void MemoryEditorWindow::connectSignals()
{
connect(g_emu_thread, &EmuThread::systemPaused, this, &MemoryEditorWindow::onSystemPaused);
connect(g_emu_thread, &EmuThread::systemResumed, this, &MemoryEditorWindow::onSystemResumed);
connect(g_emu_thread, &EmuThread::systemStarted, this, &MemoryEditorWindow::onSystemStarted);
connect(g_emu_thread, &EmuThread::systemDestroyed, this, &MemoryEditorWindow::onSystemDestroyed);
connect(m_ui.address, &QLineEdit::editingFinished, this, &MemoryEditorWindow::onAddressEditingFinished);
connect(m_ui.memoryView, &MemoryViewWidget::topAddressChanged, this,
&MemoryEditorWindow::onMemoryViewTopAddressChanged);
connect(m_ui.memoryView, &MemoryViewWidget::selectedAddressChanged, this, &MemoryEditorWindow::updateDataInspector);
connect(m_ui.memoryRegionButtonGroup, &QButtonGroup::buttonToggled, this,
&MemoryEditorWindow::onMemoryRegionButtonToggled);
connect(m_ui.endianButtonGroup, &QButtonGroup::buttonToggled, this,
&MemoryEditorWindow::onDataInspectorEndianButtonToggled);
connect(m_ui.baseButtonGroup, &QButtonGroup::buttonToggled, this,
&MemoryEditorWindow::onDataInspectorBaseButtonToggled);
connect(m_ui.memorySearch, &QPushButton::clicked, this, &MemoryEditorWindow::onMemorySearchTriggered);
connect(m_ui.memorySearchString, &QLineEdit::textChanged, this, &MemoryEditorWindow::onMemorySearchStringChanged);
connect(&m_refresh_timer, &QTimer::timeout, this, &MemoryEditorWindow::timerRefresh);
m_refresh_timer.setInterval(TIMER_REFRESH_INTERVAL_MS);
m_go_shortcut = new QShortcut(QKeySequence(Qt::CTRL | Qt::Key_G), this);
connect(m_go_shortcut, &QShortcut::activated, this, &MemoryEditorWindow::onDumpAddressTriggered);
}
void MemoryEditorWindow::updateUIEnabled()
{
const bool system_valid = QtHost::IsSystemValid();
m_ui.memoryView->setEnabled(system_valid);
m_ui.address->setEnabled(system_valid);
m_ui.memoryRegionRAM->setEnabled(system_valid);
m_ui.memoryRegionEXP1->setEnabled(system_valid);
m_ui.memoryRegionScratchpad->setEnabled(system_valid);
m_ui.memoryRegionBIOS->setEnabled(system_valid);
m_ui.memorySearch->setEnabled(system_valid);
m_ui.memorySearchString->setEnabled(system_valid);
m_ui.dataInspector->setEnabled(system_valid);
m_go_shortcut->setEnabled(system_valid);
// Partial/timer refreshes only active when not paused.
const bool timer_active = system_valid && !QtHost::IsSystemPaused();
if (m_refresh_timer.isActive() != timer_active)
timer_active ? m_refresh_timer.start() : m_refresh_timer.stop();
}
void MemoryEditorWindow::closeEvent(QCloseEvent* event)
{
QtUtils::SaveWindowGeometry("MemoryEditorWindow", this);
QWidget::closeEvent(event);
emit closed();
}
bool MemoryEditorWindow::scrollToMemoryAddress(VirtualMemoryAddress address)
{
const PhysicalMemoryAddress phys_address = CPU::VirtualAddressToPhysical(address);
std::optional<Bus::MemoryRegion> region = Bus::GetMemoryRegionForAddress(phys_address);
if (!region.has_value())
return false;
if (region.value() == Bus::MemoryRegion::EXP1)
m_ui.memoryRegionEXP1->setChecked(true);
else if (region.value() == Bus::MemoryRegion::Scratchpad)
m_ui.memoryRegionScratchpad->setChecked(true);
else if (region.value() == Bus::MemoryRegion::BIOS)
m_ui.memoryRegionBIOS->setChecked(true);
else
m_ui.memoryRegionRAM->setChecked(true);
const PhysicalMemoryAddress offset = phys_address - Bus::GetMemoryRegionStart(region.value());
m_ui.memoryView->scrollToOffset(offset);
return true;
}
void MemoryEditorWindow::updateMemoryViewRegion()
{
Bus::MemoryRegion region;
if (m_ui.memoryRegionEXP1->isChecked())
region = Bus::MemoryRegion::EXP1;
else if (m_ui.memoryRegionScratchpad->isChecked())
region = Bus::MemoryRegion::Scratchpad;
else if (m_ui.memoryRegionBIOS->isChecked())
region = Bus::MemoryRegion::BIOS;
else
region = Bus::MemoryRegion::RAM;
static constexpr auto edit_ram_callback = [](size_t offset, size_t count) {
// shouldn't happen
if (offset > Bus::g_ram_size)
return;
const u32 start_page = static_cast<u32>(offset) >> HOST_PAGE_SHIFT;
const u32 end_page = static_cast<u32>(offset + count - 1) >> HOST_PAGE_SHIFT;
Host::RunOnCPUThread([start_page, end_page]() {
for (u32 i = start_page; i <= end_page; i++)
{
if (Bus::g_ram_code_bits[i])
CPU::CodeCache::InvalidateBlocksWithPageIndex(i);
}
});
};
const PhysicalMemoryAddress start = Bus::GetMemoryRegionStart(region);
const PhysicalMemoryAddress end = Bus::GetMemoryRegionEnd(region);
void* const mem_ptr = Bus::GetMemoryRegionPointer(region);
const bool mem_writable = Bus::IsMemoryRegionWritable(region);
const MemoryViewWidget::EditCallback edit_callback =
((region == Bus::MemoryRegion::RAM) ? static_cast<MemoryViewWidget::EditCallback>(edit_ram_callback) : nullptr);
m_ui.memoryView->setData(start, mem_ptr, end - start, mem_writable, edit_callback);
}
void MemoryEditorWindow::updateDataInspector()
{
const size_t address = m_ui.memoryView->selectedAddress();
if (address == MemoryViewWidget::INVALID_SELECTED_ADDRESS)
{
m_ui.dataInspectorAddress->clear();
m_ui.dataInspectorUnsignedByte->clear();
m_ui.dataInspectorSignedByte->clear();
m_ui.dataInspectorUnsignedHalfword->clear();
m_ui.dataInspectorSignedHalfword->clear();
m_ui.dataInspectorUnsignedWord->clear();
m_ui.dataInspectorSignedWord->clear();
m_ui.dataInspectorUnsignedDoubleWord->clear();
m_ui.dataInspectorSignedDoubleWord->clear();
m_ui.dataInspectorFloat32->clear();
m_ui.dataInspectorFloat64->clear();
m_ui.dataInspectorASCIICharacter->clear();
m_ui.dataInspectorUTF8String->clear();
return;
}
u8 value8 = 0;
u16 value16 = 0;
u32 value32 = 0;
u32 value64_high = 0;
u64 value64 = 0;
CPU::SafeReadMemoryWord(static_cast<VirtualMemoryAddress>(address), &value32);
CPU::SafeReadMemoryWord(static_cast<VirtualMemoryAddress>(address), &value64_high);
value64 = (ZeroExtend64(value64_high) << 32) | ZeroExtend64(value32);
const bool big_endian = m_ui.dataInspectorBigEndian->isChecked();
if (big_endian)
{
value8 = Truncate8(value32);
value16 = ByteSwap(Truncate16(value32));
value32 = ByteSwap(value32);
value64 = ByteSwap(value64);
}
else
{
value8 = Truncate8(value32);
value16 = Truncate16(value32);
}
setIfChanged(m_ui.dataInspectorAddress, formatAddress(static_cast<VirtualMemoryAddress>(address)));
setIfChanged(m_ui.dataInspectorUnsignedByte, formatNumber(value8, false, 1));
setIfChanged(m_ui.dataInspectorSignedByte, formatNumber(value8, true, 1));
setIfChanged(m_ui.dataInspectorUnsignedHalfword, formatNumber(value16, false, 2));
setIfChanged(m_ui.dataInspectorSignedHalfword, formatNumber(value16, true, 2));
setIfChanged(m_ui.dataInspectorUnsignedWord, formatNumber(value32, false, 4));
setIfChanged(m_ui.dataInspectorSignedWord, formatNumber(value32, true, 4));
setIfChanged(m_ui.dataInspectorUnsignedDoubleWord, formatNumber(value64, false, 8));
setIfChanged(m_ui.dataInspectorSignedDoubleWord, formatNumber(value64, true, 8));
setIfChanged(m_ui.dataInspectorFloat32, QString::number(std::bit_cast<float>(value32)));
setIfChanged(m_ui.dataInspectorFloat64, QString::number(std::bit_cast<double>(value64)));
if (value8 >= 0x20 && value8 <= 0x7E)
{
m_ui.dataInspectorASCIICharacter->setText(QStringLiteral("'%1'").arg(static_cast<QChar>(value8)));
SmallString str;
CPU::SafeReadMemoryCString(static_cast<VirtualMemoryAddress>(address), &str, 32);
// only display printable characters
for (size_t i = 0; i < str.length(); i++)
{
if (str[i] < 0x20 || str[i] > 0x7E)
{
str.resize(static_cast<u32>(i));
break;
}
}
if (!str.empty())
{
str.prepend('\"');
str.append('\"');
setIfChanged(m_ui.dataInspectorUTF8String, QtUtils::StringViewToQString(str));
}
else
{
m_ui.dataInspectorUTF8String->clear();
}
}
else
{
setIfChanged(m_ui.dataInspectorASCIICharacter,
QStringLiteral("'\\x%1'").arg(static_cast<uint>(value8), 2, 16, QChar('0')));
m_ui.dataInspectorUTF8String->clear();
}
}

@ -0,0 +1,69 @@
// SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin <stenzek@gmail.com>
// SPDX-License-Identifier: CC-BY-NC-ND-4.0
#pragma once
#include "ui_memoryeditorwindow.h"
#include "core/types.h"
#include <QtCore/QTimer>
#include <QtWidgets/QWidget>
#include <optional>
class QShortcut;
class MemoryEditorWindow : public QWidget
{
Q_OBJECT
public:
explicit MemoryEditorWindow(QWidget* parent = nullptr);
~MemoryEditorWindow();
bool scrollToMemoryAddress(VirtualMemoryAddress address);
Q_SIGNALS:
void closed();
protected:
void closeEvent(QCloseEvent* event);
private:
void setupAdditionalUi();
void connectSignals();
void updateUIEnabled();
void onSystemStarted();
void onSystemDestroyed();
void onSystemPaused();
void onSystemResumed();
void timerRefresh();
void refreshAll();
void updateMemoryViewRegion();
void updateDataInspector();
void onMemoryViewTopAddressChanged(size_t address);
void onAddressEditingFinished();
void onDumpAddressTriggered();
void onMemoryRegionButtonToggled(QAbstractButton*, bool checked);
void onDataInspectorBaseButtonToggled(QAbstractButton*, bool checked);
void onDataInspectorEndianButtonToggled(QAbstractButton*, bool checked);
void onMemorySearchTriggered();
void onMemorySearchStringChanged(const QString&);
QString formatNumber(u64 value, bool is_signed, int byte_size) const;
static QString formatAddress(VirtualMemoryAddress address);
static void setIfChanged(QLineEdit* const widget, const QString& text);
Ui::MemoryEditorWindow m_ui;
QShortcut* m_go_shortcut = nullptr;
QTimer m_refresh_timer;
PhysicalMemoryAddress m_next_memory_search_address = 0;
};

@ -0,0 +1,422 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>MemoryEditorWindow</class>
<widget class="QWidget" name="MemoryEditorWindow">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>1130</width>
<height>474</height>
</rect>
</property>
<property name="windowTitle">
<string>Memory Editor</string>
</property>
<layout class="QHBoxLayout" name="horizontalLayout_5" stretch="1,0">
<item>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>Address:</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="address"/>
</item>
</layout>
</item>
<item>
<widget class="MemoryViewWidget" name="memoryView" native="true">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QRadioButton" name="memoryRegionRAM">
<property name="text">
<string>RAM</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
<attribute name="buttonGroup">
<string notr="true">memoryRegionButtonGroup</string>
</attribute>
</widget>
</item>
<item>
<widget class="QRadioButton" name="memoryRegionScratchpad">
<property name="text">
<string>Scratchpad</string>
</property>
<attribute name="buttonGroup">
<string notr="true">memoryRegionButtonGroup</string>
</attribute>
</widget>
</item>
<item>
<widget class="QRadioButton" name="memoryRegionEXP1">
<property name="text">
<string>EXP1</string>
</property>
<attribute name="buttonGroup">
<string notr="true">memoryRegionButtonGroup</string>
</attribute>
</widget>
</item>
<item>
<widget class="QRadioButton" name="memoryRegionBIOS">
<property name="text">
<string>BIOS</string>
</property>
<attribute name="buttonGroup">
<string notr="true">memoryRegionButtonGroup</string>
</attribute>
</widget>
</item>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QLineEdit" name="memorySearchString"/>
</item>
<item>
<widget class="QPushButton" name="memorySearch">
<property name="text">
<string>Search</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</item>
<item>
<widget class="QGroupBox" name="dataInspector">
<property name="title">
<string>Data Inspector</string>
</property>
<layout class="QGridLayout" name="dataInspectorLayout">
<item row="11" column="0">
<widget class="QLabel" name="label_11">
<property name="text">
<string>ASCII Character:</string>
</property>
</widget>
</item>
<item row="5" column="1">
<widget class="QLineEdit" name="dataInspectorUnsignedWord">
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
<item row="12" column="1">
<widget class="QLineEdit" name="dataInspectorUTF8String">
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QLineEdit" name="dataInspectorUnsignedHalfword">
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_4">
<property name="text">
<string>Signed Byte:</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QLineEdit" name="dataInspectorSignedByte">
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
<item row="14" column="0" colspan="2">
<layout class="QHBoxLayout" name="horizontalLayout_3">
<item>
<widget class="QRadioButton" name="dataInspectorDecimal">
<property name="text">
<string>Decimal</string>
</property>
<attribute name="buttonGroup">
<string notr="true">baseButtonGroup</string>
</attribute>
</widget>
</item>
<item>
<widget class="QRadioButton" name="dataInspectorHexadecimal">
<property name="text">
<string>Hexadecimal</string>
</property>
<attribute name="buttonGroup">
<string notr="true">baseButtonGroup</string>
</attribute>
</widget>
</item>
<item>
<widget class="QRadioButton" name="dataInspectorOctal">
<property name="text">
<string>Octal</string>
</property>
<attribute name="buttonGroup">
<string notr="true">baseButtonGroup</string>
</attribute>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_3">
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_3">
<property name="text">
<string>Unsigned Byte:</string>
</property>
</widget>
</item>
<item row="13" column="0" colspan="2">
<layout class="QHBoxLayout" name="horizontalLayout_4">
<item>
<widget class="QRadioButton" name="dataInspectorLittleEndian">
<property name="text">
<string>Little Endian</string>
</property>
<attribute name="buttonGroup">
<string notr="true">endianButtonGroup</string>
</attribute>
</widget>
</item>
<item>
<widget class="QRadioButton" name="dataInspectorBigEndian">
<property name="text">
<string>Big Endian</string>
</property>
<attribute name="buttonGroup">
<string notr="true">endianButtonGroup</string>
</attribute>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_2">
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item row="3" column="0">
<widget class="QLabel" name="label_5">
<property name="text">
<string>Unsigned Halfword:</string>
</property>
</widget>
</item>
<item row="9" column="0">
<widget class="QLabel" name="label_9">
<property name="text">
<string>32-Bit Float:</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QLineEdit" name="dataInspectorUnsignedByte">
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLineEdit" name="dataInspectorAddress">
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
<item row="7" column="0">
<widget class="QLabel" name="label_13">
<property name="text">
<string>Unsigned Doubleword:</string>
</property>
</widget>
</item>
<item row="7" column="1">
<widget class="QLineEdit" name="dataInspectorUnsignedDoubleWord">
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
<item row="9" column="1">
<widget class="QLineEdit" name="dataInspectorFloat32">
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
<item row="4" column="1">
<widget class="QLineEdit" name="dataInspectorSignedHalfword">
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
<item row="6" column="1">
<widget class="QLineEdit" name="dataInspectorSignedWord">
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QLabel" name="label_2">
<property name="text">
<string>Address:</string>
</property>
</widget>
</item>
<item row="10" column="1">
<widget class="QLineEdit" name="dataInspectorFloat64">
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
<item row="12" column="0">
<widget class="QLabel" name="label_12">
<property name="text">
<string>UTF-8 String:</string>
</property>
</widget>
</item>
<item row="10" column="0">
<widget class="QLabel" name="label_10">
<property name="text">
<string>64-Bit Float:</string>
</property>
</widget>
</item>
<item row="11" column="1">
<widget class="QLineEdit" name="dataInspectorASCIICharacter">
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
<item row="5" column="0">
<widget class="QLabel" name="label_8">
<property name="text">
<string>Unsigned Word:</string>
</property>
</widget>
</item>
<item row="6" column="0">
<widget class="QLabel" name="label_7">
<property name="text">
<string>Signed Word:</string>
</property>
</widget>
</item>
<item row="15" column="0" colspan="2">
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Orientation::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
</spacer>
</item>
<item row="4" column="0">
<widget class="QLabel" name="label_6">
<property name="text">
<string>Signed Halfword:</string>
</property>
</widget>
</item>
<item row="8" column="0">
<widget class="QLabel" name="label_14">
<property name="text">
<string>Signed Doubleword:</string>
</property>
</widget>
</item>
<item row="8" column="1">
<widget class="QLineEdit" name="dataInspectorSignedDoubleWord">
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>MemoryViewWidget</class>
<extends>QWidget</extends>
<header>duckstation-qt/memoryviewwidget.h</header>
<container>1</container>
</customwidget>
</customwidgets>
<resources>
<include location="resources/duckstation-qt.qrc"/>
</resources>
<connections/>
<buttongroups>
<buttongroup name="baseButtonGroup"/>
<buttongroup name="memoryRegionButtonGroup"/>
<buttongroup name="endianButtonGroup"/>
</buttongroups>
</ui>

@ -6,6 +6,8 @@
#include <QtWidgets/QScrollBar>
#include <cstring>
#include "moc_memoryviewwidget.cpp"
MemoryViewWidget::MemoryViewWidget(QWidget* parent /* = nullptr */, size_t address_offset /* = 0 */,
void* data_ptr /* = nullptr */, size_t data_size /* = 0 */,
bool data_editable /* = false */, EditCallback edit_callback /* = nullptr */)
@ -46,6 +48,17 @@ void MemoryViewWidget::updateMetrics()
m_char_height = fm.height();
}
size_t MemoryViewWidget::selectedAddress() const
{
return (m_selected_address != INVALID_SELECTED_ADDRESS) ? (m_selected_address + m_address_offset) :
INVALID_SELECTED_ADDRESS;
}
size_t MemoryViewWidget::topAddress() const
{
return static_cast<size_t>(verticalScrollBar()->value()) * m_bytes_per_line + m_address_offset;
}
void MemoryViewWidget::setData(size_t address_offset, void* data_ptr, size_t data_size, bool data_editable,
EditCallback edit_callback)
{
@ -130,6 +143,7 @@ void MemoryViewWidget::keyPressEvent(QKeyEvent* event)
m_selected_address--;
m_editing_nibble = -1;
forceRefresh();
notifySelectedAddressChanged();
}
}
else
@ -152,6 +166,7 @@ void MemoryViewWidget::keyPressEvent(QKeyEvent* event)
m_selected_address = std::min(m_selected_address + 1, m_data_size - 1);
forceRefresh();
notifySelectedAddressChanged();
}
else
{
@ -182,6 +197,7 @@ void MemoryViewWidget::keyPressEvent(QKeyEvent* event)
{
m_editing_nibble = -1;
m_selected_address = std::min(m_selected_address + 1, m_data_size - 1);
notifySelectedAddressChanged();
}
forceRefresh();
@ -225,6 +241,7 @@ void MemoryViewWidget::keyPressEvent(QKeyEvent* event)
forceRefresh();
expandCurrentDataToInclude(m_selected_address);
adjustScrollToInclude(m_selected_address);
notifySelectedAddressChanged();
return;
}
@ -448,6 +465,7 @@ void MemoryViewWidget::setSelection(size_t new_selection, bool new_ascii)
m_selection_was_ascii = new_ascii;
m_editing_nibble = -1;
forceRefresh();
notifySelectedAddressChanged();
}
}
@ -508,12 +526,7 @@ void MemoryViewWidget::forceRefresh()
void MemoryViewWidget::adjustContent()
{
if (!m_data)
{
setEnabled(false);
return;
}
setEnabled(true);
int w = addressWidth() + hexWidth() + asciiWidth();
horizontalScrollBar()->setRange(0, w - viewport()->width());
@ -534,4 +547,11 @@ void MemoryViewWidget::adjustContent()
expandCurrentDataToInclude(m_end_offset);
forceRefresh();
}
emit topAddressChanged(topAddress());
}
void MemoryViewWidget::notifySelectedAddressChanged()
{
emit selectedAddressChanged(selectedAddress());
}

@ -9,7 +9,11 @@
class MemoryViewWidget : public QAbstractScrollArea
{
Q_OBJECT
public:
static constexpr size_t INVALID_SELECTED_ADDRESS = ~static_cast<size_t>(0);
using EditCallback = void (*)(size_t offset, size_t bytes);
MemoryViewWidget(QWidget* parent = nullptr, size_t address_offset = 0, void* data_ptr = nullptr, size_t data_size = 0,
@ -17,6 +21,8 @@ public:
~MemoryViewWidget();
size_t addressOffset() const { return m_address_offset; }
size_t selectedAddress() const;
size_t topAddress() const;
void setData(size_t address_offset, void* data_ptr, size_t data_size, bool data_editable, EditCallback edit_callback);
void setHighlightRange(size_t start, size_t end);
@ -28,6 +34,10 @@ public:
void saveCurrentData();
void forceRefresh();
Q_SIGNALS:
void topAddressChanged(size_t address);
void selectedAddressChanged(size_t address);
protected:
void paintEvent(QPaintEvent* event);
void resizeEvent(QResizeEvent* event);
@ -36,8 +46,6 @@ protected:
void keyPressEvent(QKeyEvent* event);
private:
static constexpr size_t INVALID_SELECTED_ADDRESS = ~static_cast<size_t>(0);
int addressWidth() const;
int hexWidth() const;
int asciiWidth() const;
@ -47,6 +55,7 @@ private:
void expandCurrentDataToInclude(size_t offset);
void adjustScrollToInclude(size_t offset);
void adjustContent();
void notifySelectedAddressChanged();
void* m_data;
size_t m_data_size;

Loading…
Cancel
Save