/* PCSX2 - PS2 Emulator for PCs * Copyright (C) 2002-2022 PCSX2 Dev Team * * PCSX2 is free software: you can redistribute it and/or modify it under the terms * of the GNU Lesser General Public License as published by the Free Software Found- * ation, either version 3 of the License, or (at your option) any later version. * * PCSX2 is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR * PURPOSE. See the GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along with PCSX2. * If not, see . */ #include "PrecompiledHeader.h" #include "MemoryViewWidget.h" #include "common/BitCast.h" #include "QtHost.h" #include "QtUtils.h" #include #include #include #include #include #include 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) { 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; u32 currentSegmentAddress = currentRowAddress; for (int j = 0; j < 16; j++) { const u32 currentByteAddress = currentRowAddress + j; if (!(j % (s32)displayType)) { valX += charWidth; currentSegmentAddress = currentByteAddress; } segmentXAxis[j] = valX; if ((selectedAddress & ~0xF) == currentRowAddress) { if (selectedAddress == currentByteAddress) { // If the current byte and row we are drawing is selected if (!selectedText) { painter.setPen(QColor::fromRgb(205, 165, 0)); // SELECTED NIBBLE LINE COLOUR const QPoint lineStart(valX + (selectedNibbleHI ? 0 : charWidth) + 1, y + (rowHeight * i)); painter.drawLine(lineStart, lineStart + QPoint(charWidth - 3, 0)); } painter.setPen(QColor::fromRgb(0xaa, 0x22, 0x22)); // SELECTED BYTE COLOUR } // If the current selected byte is in our current segment, highlight the entire segment else if (displayType != MemoryViewType::BYTE && currentSegmentAddress <= selectedAddress && (selectedAddress <= (currentSegmentAddress + (s32)displayType - 1))) { painter.setPen(palette.highlight().color()); // SELECTED SEGMENT COLOUR } else painter.setPen(palette.text().color()); // Default colour } else painter.setPen(palette.text().color()); // Default colour bool valid; const u8 val = static_cast(m_cpu->read8(currentByteAddress, valid)); painter.drawText(valX, y + (rowHeight * i), valid ? FilledQStringFromValue(val, 16) : "??"); valX += charWidth * 2; } // 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 = m_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) { const u32 selectedRow = (pos.y() - 2) / (rowHeight); const s32 x = pos.x(); const s32 avgSegmentWidth = segmentXAxis[1] - segmentXAxis[0]; selectedAddress = (selectedRow * 0x10) + startAddress; if (x <= segmentXAxis[0]) { selectedText = false; // The user clicked before the first segment 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 == 15 || (x >= segmentXAxis[i] && x < (segmentXAxis[i + 1]))) { selectedAddress = selectedAddress + i; selectedNibbleHI = ((x - segmentXAxis[i]) < ((avgSegmentWidth / 2) - 2)); // Subtract 2 units, makes selecting nibbles feel more natural break; } } } else if (x >= textXAxis) { selectedText = true; // The user clicked the text area selectedAddress += std::min((x - textXAxis) / 8, 15); } } u128 MemoryViewTable::GetSelectedSegment() { u128 val; switch (displayType) { case MemoryViewType::BYTE: val.lo = m_cpu->read8(selectedAddress); break; case MemoryViewType::BYTEHW: val.lo = qToBigEndian((u16)m_cpu->read16(selectedAddress & ~1)); break; case MemoryViewType::WORD: val.lo = qToBigEndian(m_cpu->read32(selectedAddress & ~3)); break; case MemoryViewType::DWORD: val._u64[0] = qToBigEndian(m_cpu->read64(selectedAddress & ~7)); break; } return val; } void MemoryViewTable::InsertIntoSelectedHexView(u8 value) { const u8 mask = selectedNibbleHI ? 0x0f : 0xf0; u8 curVal = m_cpu->read8(selectedAddress) & mask; u8 newVal = value << (selectedNibbleHI ? 4 : 0); curVal |= newVal; Host::RunOnCPUThread([this, address = selectedAddress, cpu = m_cpu, val = curVal] { cpu->write8(address, val); QtHost::RunOnUIThread([this] { parent->update(); }); }); } // We need both key and keychar because `key` is easy to use, but is case insensitive void MemoryViewTable::KeyPress(int key, QChar keychar) { if (!m_cpu->isValidAddress(selectedAddress)) return; const bool keyCharIsText = keychar.isLetterOrNumber() || keychar.isSpace(); if (selectedText) { if (keyCharIsText || (!keychar.isNonCharacter() && keychar.category() != QChar::Other_Control)) { Host::RunOnCPUThread([this, address = selectedAddress, cpu = m_cpu, val = keychar.toLatin1()] { cpu->write8(address, val); QtHost::RunOnUIThread([this] { UpdateSelectedAddress(selectedAddress + 1); parent->update(); }); }); } switch (key) { case Qt::Key::Key_Backspace: case Qt::Key::Key_Escape: Host::RunOnCPUThread([this, address = selectedAddress, cpu = m_cpu] { cpu->write8(address, 0); QtHost::RunOnUIThread([this] { UpdateSelectedAddress(selectedAddress - 1); parent->update(); }); }); break; case Qt::Key::Key_Right: UpdateSelectedAddress(selectedAddress + 1); break; case Qt::Key::Key_Left: UpdateSelectedAddress(selectedAddress - 1); break; } } else { // Hex view is selected if (keyCharIsText) { InsertIntoSelectedHexView(((u8)QString(QChar(key)).toInt(nullptr, 16))); // Increment to the next nibble or byte if ((selectedNibbleHI = !selectedNibbleHI)) UpdateSelectedAddress(selectedAddress + 1); } switch (key) { case Qt::Key::Key_Backspace: case Qt::Key::Key_Escape: InsertIntoSelectedHexView(0); // Move back a byte or nibble if it's backspace being pressed if (!(selectedNibbleHI = !selectedNibbleHI)) UpdateSelectedAddress(selectedAddress - 1); break; case Qt::Key::Key_Right: if ((selectedNibbleHI = !selectedNibbleHI)) UpdateSelectedAddress(selectedAddress + 1); break; case Qt::Key::Key_Left: if (!(selectedNibbleHI = !selectedNibbleHI)) UpdateSelectedAddress(selectedAddress - 1); break; } } // Keybinds that are the same for the text and hex view switch (key) { case Qt::Key::Key_Up: UpdateSelectedAddress(selectedAddress - 0x10); break; case Qt::Key::Key_PageUp: UpdateSelectedAddress(selectedAddress - (0x10 * rowVisible), true); break; case Qt::Key::Key_Down: UpdateSelectedAddress(selectedAddress + 0x10); break; case Qt::Key::Key_PageDown: UpdateSelectedAddress(selectedAddress + (0x10 * rowVisible), true); break; } } /* MemoryViewWidget */ MemoryViewWidget::MemoryViewWidget(QWidget* parent) : QWidget(parent) , m_table(this) { ui.setupUi(this); this->setFocusPolicy(Qt::FocusPolicy::ClickFocus); connect(this, &MemoryViewWidget::customContextMenuRequested, this, &MemoryViewWidget::customMenuRequested); } MemoryViewWidget::~MemoryViewWidget() = default; void MemoryViewWidget::SetCpu(DebugInterface* cpu) { m_cpu = cpu; m_table.SetCpu(cpu); m_table.UpdateStartAddress(0x480000); } void MemoryViewWidget::paintEvent(QPaintEvent* event) { if (!m_cpu->isAlive()) return; QPainter painter(this); m_table.DrawTable(painter, this->palette(), this->height()); } void MemoryViewWidget::mousePressEvent(QMouseEvent* event) { if (!m_cpu->isAlive()) return; m_table.SelectAt(event->pos()); repaint(); } void MemoryViewWidget::customMenuRequested(QPoint pos) { if (!m_cpu->isAlive()) return; if (!m_contextMenu) { m_contextMenu = new QMenu(this); QAction* action = new QAction(tr("Go to in disassembly")); m_contextMenu->addAction(action); connect(action, &QAction::triggered, this, [this]() { emit gotoInDisasm(m_table.selectedAddress); }); action = new QAction(tr("Go to address")); m_contextMenu->addAction(action); connect(action, &QAction::triggered, this, [this]() { contextGoToAddress(); }); m_contextMenu->addSeparator(); // View Types m_actionBYTE = new QAction(tr("Show as 1 byte")); m_actionBYTE->setCheckable(true); m_contextMenu->addAction(m_actionBYTE); connect(m_actionBYTE, &QAction::triggered, this, [this]() { m_table.SetViewType(MemoryViewType::BYTE); }); m_actionBYTEHW = new QAction(tr("Show as 2 bytes")); m_actionBYTEHW->setCheckable(true); m_contextMenu->addAction(m_actionBYTEHW); connect(m_actionBYTEHW, &QAction::triggered, this, [this]() { m_table.SetViewType(MemoryViewType::BYTEHW); }); m_actionWORD = new QAction(tr("Show as 4 bytes")); m_actionWORD->setCheckable(true); m_contextMenu->addAction(m_actionWORD); connect(m_actionWORD, &QAction::triggered, this, [this]() { m_table.SetViewType(MemoryViewType::WORD); }); m_actionDWORD = new QAction(tr("Show as 8 bytes")); m_actionDWORD->setCheckable(true); m_contextMenu->addAction(m_actionDWORD); connect(m_actionDWORD, &QAction::triggered, this, [this]() { m_table.SetViewType(MemoryViewType::DWORD); }); m_contextMenu->addSeparator(); action = new QAction(tr("Copy Byte")); m_contextMenu->addAction(action); connect(action, &QAction::triggered, this, [this]() { contextCopyByte(); }); action = new QAction(tr("Copy Segment")); m_contextMenu->addAction(action); connect(action, &QAction::triggered, this, [this]() { contextCopySegment(); }); action = new QAction(tr("Copy Character")); m_contextMenu->addAction(action); connect(action, &QAction::triggered, this, [this]() { contextCopyCharacter(); }); } const MemoryViewType currentViewType = m_table.GetViewType(); m_actionBYTE->setChecked(currentViewType == MemoryViewType::BYTE); m_actionBYTEHW->setChecked(currentViewType == MemoryViewType::BYTEHW); m_actionWORD->setChecked(currentViewType == MemoryViewType::WORD); m_actionDWORD->setChecked(currentViewType == MemoryViewType::DWORD); m_contextMenu->popup(this->mapToGlobal(pos)); this->repaint(); return; } void MemoryViewWidget::contextCopyByte() { QApplication::clipboard()->setText(QString::number(m_cpu->read8(m_table.selectedAddress), 16).toUpper()); } void MemoryViewWidget::contextCopySegment() { QApplication::clipboard()->setText(QString::number(m_table.GetSelectedSegment().lo, 16).toUpper()); } void MemoryViewWidget::contextCopyCharacter() { QApplication::clipboard()->setText(QChar::fromLatin1(m_cpu->read8(m_table.selectedAddress)).toUpper()); } void MemoryViewWidget::contextGoToAddress() { bool ok; QString targetString = QInputDialog::getText(this, tr("Go to address"), "", QLineEdit::Normal, "", &ok); if (!ok) return; const u32 targetAddress = targetString.toUInt(&ok, 16); if (!ok) { QMessageBox::warning(this, "Go to address error", "Invalid address"); return; } gotoAddress(targetAddress); } void MemoryViewWidget::mouseDoubleClickEvent(QMouseEvent* event) { } void MemoryViewWidget::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 MemoryViewWidget::keyPressEvent(QKeyEvent* event) { bool handledByWidget = true; switch (event->key()) { case Qt::Key_G: contextGoToAddress(); break; case Qt::Key_C: if (event->modifiers() & Qt::ControlModifier) contextCopySegment(); else handledByWidget = false; break; default: handledByWidget = false; break; } if (!handledByWidget) m_table.KeyPress(event->key(), event->text().size() ? event->text()[0] : '\0'); this->repaint(); VMUpdate(); } void MemoryViewWidget::gotoAddress(u32 address) { m_table.UpdateStartAddress(address & ~0xF); m_table.selectedAddress = address; this->repaint(); this->setFocus(); }