First Commit
This commit is contained in:
35
pcsx2-qt/Debugger/AnalysisOptionsDialog.cpp
Normal file
35
pcsx2-qt/Debugger/AnalysisOptionsDialog.cpp
Normal file
@@ -0,0 +1,35 @@
|
||||
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
|
||||
// SPDX-License-Identifier: GPL-3.0+
|
||||
|
||||
#include "AnalysisOptionsDialog.h"
|
||||
|
||||
#include "Host.h"
|
||||
#include "DebugTools/SymbolImporter.h"
|
||||
|
||||
AnalysisOptionsDialog::AnalysisOptionsDialog(QWidget* parent)
|
||||
: QDialog(parent)
|
||||
{
|
||||
m_ui.setupUi(this);
|
||||
|
||||
m_analysis_settings = new DebugAnalysisSettingsWidget();
|
||||
|
||||
m_ui.analysisSettings->setLayout(new QVBoxLayout());
|
||||
m_ui.analysisSettings->layout()->setContentsMargins(0, 0, 0, 0);
|
||||
m_ui.analysisSettings->layout()->addWidget(m_analysis_settings);
|
||||
|
||||
connect(m_ui.analyseButton, &QPushButton::clicked, this, &AnalysisOptionsDialog::analyse);
|
||||
connect(m_ui.closeButton, &QPushButton::clicked, this, &QDialog::reject);
|
||||
}
|
||||
|
||||
void AnalysisOptionsDialog::analyse()
|
||||
{
|
||||
Pcsx2Config::DebugAnalysisOptions options;
|
||||
m_analysis_settings->parseSettingsFromWidgets(options);
|
||||
|
||||
Host::RunOnCPUThread([options]() {
|
||||
R5900SymbolImporter.LoadAndAnalyseElf(options);
|
||||
});
|
||||
|
||||
if (m_ui.closeCheckBox->isChecked())
|
||||
accept();
|
||||
}
|
||||
25
pcsx2-qt/Debugger/AnalysisOptionsDialog.h
Normal file
25
pcsx2-qt/Debugger/AnalysisOptionsDialog.h
Normal file
@@ -0,0 +1,25 @@
|
||||
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
|
||||
// SPDX-License-Identifier: GPL-3.0+
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "Settings/DebugAnalysisSettingsWidget.h"
|
||||
|
||||
#include <QtWidgets/QDialog>
|
||||
|
||||
#include "ui_AnalysisOptionsDialog.h"
|
||||
|
||||
class AnalysisOptionsDialog : public QDialog
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
AnalysisOptionsDialog(QWidget* parent = nullptr);
|
||||
|
||||
protected:
|
||||
void analyse();
|
||||
|
||||
DebugAnalysisSettingsWidget* m_analysis_settings;
|
||||
|
||||
Ui::AnalysisOptionsDialog m_ui;
|
||||
};
|
||||
113
pcsx2-qt/Debugger/AnalysisOptionsDialog.ui
Normal file
113
pcsx2-qt/Debugger/AnalysisOptionsDialog.ui
Normal file
@@ -0,0 +1,113 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>AnalysisOptionsDialog</class>
|
||||
<widget class="QDialog" name="AnalysisOptionsDialog">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>500</width>
|
||||
<height>750</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>500</width>
|
||||
<height>650</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Analysis Options</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Changes made here won't be saved. Edit these settings from the global or per-game settings dialogs to have your changes take effect for future analysis runs.</string>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QScrollArea" name="scrollArea">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Expanding">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="frameShape">
|
||||
<enum>QFrame::NoFrame</enum>
|
||||
</property>
|
||||
<property name="horizontalScrollBarPolicy">
|
||||
<enum>Qt::ScrollBarAlwaysOff</enum>
|
||||
</property>
|
||||
<property name="widgetResizable">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<widget class="QWidget" name="analysisSettings">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>488</width>
|
||||
<height>636</height>
|
||||
</rect>
|
||||
</property>
|
||||
</widget>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="buttons">
|
||||
<item>
|
||||
<widget class="QCheckBox" name="closeCheckBox">
|
||||
<property name="text">
|
||||
<string>Close dialog after analysis has started</string>
|
||||
</property>
|
||||
<property name="checked">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="analyseButton">
|
||||
<property name="text">
|
||||
<string>Analyze</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="closeButton">
|
||||
<property name="text">
|
||||
<string>Close</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
||||
191
pcsx2-qt/Debugger/Breakpoints/BreakpointDialog.cpp
Normal file
191
pcsx2-qt/Debugger/Breakpoints/BreakpointDialog.cpp
Normal file
@@ -0,0 +1,191 @@
|
||||
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
|
||||
// SPDX-License-Identifier: GPL-3.0+
|
||||
|
||||
#include "BreakpointDialog.h"
|
||||
#include "DebugTools/Breakpoints.h"
|
||||
|
||||
#include "QtUtils.h"
|
||||
#include "QtHost.h"
|
||||
#include <QtWidgets/QDialog>
|
||||
#include <QtWidgets/QMessageBox>
|
||||
|
||||
BreakpointDialog::BreakpointDialog(QWidget* parent, DebugInterface* cpu, BreakpointModel& model)
|
||||
: QDialog(parent)
|
||||
, m_cpu(cpu)
|
||||
, m_purpose(PURPOSE::CREATE)
|
||||
, m_bpModel(model)
|
||||
{
|
||||
m_ui.setupUi(this);
|
||||
|
||||
m_ui.grpType->setEnabled(true);
|
||||
m_ui.txtAddress->setEnabled(true);
|
||||
|
||||
connect(m_ui.rdoExecute, &QRadioButton::toggled, this, &BreakpointDialog::onRdoButtonToggled);
|
||||
connect(m_ui.rdoMemory, &QRadioButton::toggled, this, &BreakpointDialog::onRdoButtonToggled);
|
||||
}
|
||||
|
||||
BreakpointDialog::BreakpointDialog(QWidget* parent, DebugInterface* cpu, BreakpointModel& model, BreakpointMemcheck bp_mc, int rowIndex)
|
||||
: QDialog(parent)
|
||||
, m_cpu(cpu)
|
||||
, m_purpose(PURPOSE::EDIT)
|
||||
, m_bpModel(model)
|
||||
, m_bp_mc(bp_mc)
|
||||
, m_rowIndex(rowIndex)
|
||||
{
|
||||
m_ui.setupUi(this);
|
||||
|
||||
connect(m_ui.rdoExecute, &QRadioButton::toggled, this, &BreakpointDialog::onRdoButtonToggled);
|
||||
connect(m_ui.rdoMemory, &QRadioButton::toggled, this, &BreakpointDialog::onRdoButtonToggled);
|
||||
|
||||
if (const auto* bp = std::get_if<BreakPoint>(&bp_mc))
|
||||
{
|
||||
m_ui.rdoExecute->setChecked(true);
|
||||
m_ui.chkEnable->setChecked(bp->enabled);
|
||||
m_ui.txtAddress->setText(QtUtils::FilledQStringFromValue(bp->addr, 16));
|
||||
m_ui.txtDescription->setText(QString::fromStdString(bp->description));
|
||||
|
||||
if (bp->hasCond)
|
||||
m_ui.txtCondition->setText(QString::fromStdString(bp->cond.expressionString));
|
||||
}
|
||||
else if (const auto* mc = std::get_if<MemCheck>(&bp_mc))
|
||||
{
|
||||
m_ui.rdoMemory->setChecked(true);
|
||||
|
||||
m_ui.txtAddress->setText(QtUtils::FilledQStringFromValue(mc->start, 16));
|
||||
m_ui.txtSize->setText(QtUtils::FilledQStringFromValue(mc->end - mc->start, 16));
|
||||
|
||||
m_ui.txtDescription->setText(QString::fromStdString(mc->description));
|
||||
|
||||
m_ui.chkRead->setChecked(mc->memCond & MEMCHECK_READ);
|
||||
m_ui.chkWrite->setChecked(mc->memCond & MEMCHECK_WRITE);
|
||||
m_ui.chkChange->setChecked(mc->memCond & MEMCHECK_WRITE_ONCHANGE);
|
||||
|
||||
m_ui.chkEnable->setChecked(mc->result & MEMCHECK_BREAK);
|
||||
m_ui.chkLog->setChecked(mc->result & MEMCHECK_LOG);
|
||||
|
||||
if (mc->hasCond)
|
||||
m_ui.txtCondition->setText(QString::fromStdString(mc->cond.expressionString));
|
||||
}
|
||||
}
|
||||
|
||||
BreakpointDialog::~BreakpointDialog()
|
||||
{
|
||||
}
|
||||
|
||||
void BreakpointDialog::onRdoButtonToggled()
|
||||
{
|
||||
const bool isExecute = m_ui.rdoExecute->isChecked();
|
||||
|
||||
m_ui.grpMemory->setEnabled(!isExecute);
|
||||
|
||||
m_ui.chkLog->setEnabled(!isExecute);
|
||||
}
|
||||
|
||||
void BreakpointDialog::accept()
|
||||
{
|
||||
std::string error;
|
||||
|
||||
if (m_purpose == PURPOSE::CREATE)
|
||||
{
|
||||
if (m_ui.rdoExecute->isChecked())
|
||||
m_bp_mc = BreakPoint();
|
||||
else if (m_ui.rdoMemory->isChecked())
|
||||
m_bp_mc = MemCheck();
|
||||
}
|
||||
|
||||
if (auto* bp = std::get_if<BreakPoint>(&m_bp_mc))
|
||||
{
|
||||
PostfixExpression expr;
|
||||
|
||||
u64 address;
|
||||
if (!m_cpu->evaluateExpression(m_ui.txtAddress->text().toStdString().c_str(), address, error))
|
||||
{
|
||||
QMessageBox::warning(this, tr("Invalid Address"), QString::fromStdString(error));
|
||||
return;
|
||||
}
|
||||
|
||||
bp->addr = address;
|
||||
bp->description = m_ui.txtDescription->text().toStdString();
|
||||
|
||||
bp->enabled = m_ui.chkEnable->isChecked();
|
||||
|
||||
if (!m_ui.txtCondition->text().isEmpty())
|
||||
{
|
||||
bp->hasCond = true;
|
||||
bp->cond.debug = m_cpu;
|
||||
|
||||
if (!m_cpu->initExpression(m_ui.txtCondition->text().toStdString().c_str(), expr, error))
|
||||
{
|
||||
QMessageBox::warning(this, tr("Invalid Condition"), QString::fromStdString(error));
|
||||
return;
|
||||
}
|
||||
|
||||
bp->cond.expression = expr;
|
||||
bp->cond.expressionString = m_ui.txtCondition->text().toStdString();
|
||||
}
|
||||
}
|
||||
if (auto* mc = std::get_if<MemCheck>(&m_bp_mc))
|
||||
{
|
||||
u64 startAddress;
|
||||
if (!m_cpu->evaluateExpression(m_ui.txtAddress->text().toStdString().c_str(), startAddress, error))
|
||||
{
|
||||
QMessageBox::warning(this, tr("Invalid Address"), QString::fromStdString(error));
|
||||
return;
|
||||
}
|
||||
|
||||
u64 size;
|
||||
if (!m_cpu->evaluateExpression(m_ui.txtSize->text().toStdString().c_str(), size, error) || !size)
|
||||
{
|
||||
QMessageBox::warning(this, tr("Invalid Size"), QString::fromStdString(error));
|
||||
return;
|
||||
}
|
||||
|
||||
mc->start = startAddress;
|
||||
mc->end = startAddress + size;
|
||||
mc->description = m_ui.txtDescription->text().toStdString();
|
||||
|
||||
if (!m_ui.txtCondition->text().isEmpty())
|
||||
{
|
||||
mc->hasCond = true;
|
||||
mc->cond.debug = m_cpu;
|
||||
|
||||
PostfixExpression expr;
|
||||
if (!m_cpu->initExpression(m_ui.txtCondition->text().toStdString().c_str(), expr, error))
|
||||
{
|
||||
QMessageBox::warning(this, tr("Invalid Condition"), QString::fromStdString(error));
|
||||
return;
|
||||
}
|
||||
|
||||
mc->cond.expression = expr;
|
||||
mc->cond.expressionString = m_ui.txtCondition->text().toStdString();
|
||||
}
|
||||
|
||||
int condition = 0;
|
||||
if (m_ui.chkRead->isChecked())
|
||||
condition |= MEMCHECK_READ;
|
||||
if (m_ui.chkWrite->isChecked())
|
||||
condition |= MEMCHECK_WRITE;
|
||||
if (m_ui.chkChange->isChecked())
|
||||
condition |= MEMCHECK_WRITE_ONCHANGE;
|
||||
|
||||
mc->memCond = static_cast<MemCheckCondition>(condition);
|
||||
|
||||
int result = 0;
|
||||
if (m_ui.chkEnable->isChecked())
|
||||
result |= MEMCHECK_BREAK;
|
||||
if (m_ui.chkLog->isChecked())
|
||||
result |= MEMCHECK_LOG;
|
||||
|
||||
mc->result = static_cast<MemCheckResult>(result);
|
||||
}
|
||||
|
||||
|
||||
if (m_purpose == PURPOSE::EDIT)
|
||||
{
|
||||
m_bpModel.removeRows(m_rowIndex, 1);
|
||||
}
|
||||
|
||||
m_bpModel.insertBreakpointRows(0, 1, {m_bp_mc});
|
||||
|
||||
QDialog::accept();
|
||||
}
|
||||
41
pcsx2-qt/Debugger/Breakpoints/BreakpointDialog.h
Normal file
41
pcsx2-qt/Debugger/Breakpoints/BreakpointDialog.h
Normal file
@@ -0,0 +1,41 @@
|
||||
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
|
||||
// SPDX-License-Identifier: GPL-3.0+
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "ui_BreakpointDialog.h"
|
||||
|
||||
#include "BreakpointModel.h"
|
||||
|
||||
#include "DebugTools/Breakpoints.h"
|
||||
|
||||
#include <QtWidgets/QDialog>
|
||||
|
||||
class BreakpointDialog final : public QDialog
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
BreakpointDialog(QWidget* parent, DebugInterface* cpu, BreakpointModel& model);
|
||||
BreakpointDialog(QWidget* parent, DebugInterface* cpu, BreakpointModel& model, BreakpointMemcheck bpmc, int rowIndex);
|
||||
~BreakpointDialog();
|
||||
|
||||
public slots:
|
||||
void onRdoButtonToggled();
|
||||
void accept() override;
|
||||
|
||||
private:
|
||||
enum class PURPOSE
|
||||
{
|
||||
CREATE,
|
||||
EDIT
|
||||
};
|
||||
|
||||
Ui::BreakpointDialog m_ui;
|
||||
DebugInterface* m_cpu;
|
||||
|
||||
const PURPOSE m_purpose;
|
||||
BreakpointModel& m_bpModel;
|
||||
BreakpointMemcheck m_bp_mc;
|
||||
int m_rowIndex;
|
||||
};
|
||||
348
pcsx2-qt/Debugger/Breakpoints/BreakpointDialog.ui
Normal file
348
pcsx2-qt/Debugger/Breakpoints/BreakpointDialog.ui
Normal file
@@ -0,0 +1,348 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>BreakpointDialog</class>
|
||||
<widget class="QDialog" name="BreakpointDialog">
|
||||
<property name="windowModality">
|
||||
<enum>Qt::ApplicationModal</enum>
|
||||
</property>
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>375</width>
|
||||
<height>300</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>375</width>
|
||||
<height>300</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>375</width>
|
||||
<height>300</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="baseSize">
|
||||
<size>
|
||||
<width>375</width>
|
||||
<height>300</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Create / Modify Breakpoint</string>
|
||||
</property>
|
||||
<widget class="QDialogButtonBox" name="buttonBox">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>20</x>
|
||||
<y>260</y>
|
||||
<width>341</width>
|
||||
<height>32</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="standardButtons">
|
||||
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
|
||||
</property>
|
||||
</widget>
|
||||
<widget class="QGroupBox" name="grpType">
|
||||
<property name="enabled">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>10</x>
|
||||
<y>0</y>
|
||||
<width>91</width>
|
||||
<height>81</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="title">
|
||||
<string>Type</string>
|
||||
</property>
|
||||
<layout class="QFormLayout" name="formLayout_2">
|
||||
<item row="1" column="0">
|
||||
<widget class="QRadioButton" name="rdoExecute">
|
||||
<property name="text">
|
||||
<string>Execute</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="0">
|
||||
<widget class="QRadioButton" name="rdoMemory">
|
||||
<property name="text">
|
||||
<string>Memory</string>
|
||||
</property>
|
||||
<property name="checked">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QGroupBox" name="groupBox_2">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>110</x>
|
||||
<y>10</y>
|
||||
<width>250</width>
|
||||
<height>79</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="title">
|
||||
<string/>
|
||||
</property>
|
||||
<layout class="QFormLayout" name="formLayout">
|
||||
<property name="formAlignment">
|
||||
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set>
|
||||
</property>
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="text">
|
||||
<string>Address</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1">
|
||||
<widget class="QLineEdit" name="txtAddress">
|
||||
<property name="enabled">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>19</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>16777215</width>
|
||||
<height>19</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>0</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QLabel" name="label_4">
|
||||
<property name="text">
|
||||
<string>Description</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<widget class="QLineEdit" name="txtDescription">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>19</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>16777215</width>
|
||||
<height>19</height>
|
||||
</size>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QGroupBox" name="grpMemory">
|
||||
<property name="enabled">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>110</x>
|
||||
<y>100</y>
|
||||
<width>251</width>
|
||||
<height>91</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="title">
|
||||
<string>Memory</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_2">
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<item>
|
||||
<widget class="QCheckBox" name="chkRead">
|
||||
<property name="text">
|
||||
<string>Read</string>
|
||||
</property>
|
||||
<property name="checked">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="chkWrite">
|
||||
<property name="text">
|
||||
<string>Write</string>
|
||||
</property>
|
||||
<property name="checked">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="chkChange">
|
||||
<property name="text">
|
||||
<string>Change</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QFormLayout" name="formLayout_4">
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="label_2">
|
||||
<property name="text">
|
||||
<string>Size</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1">
|
||||
<widget class="QLineEdit" name="txtSize">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>19</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>16777215</width>
|
||||
<height>19</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>1</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QGroupBox" name="grpExecute">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>110</x>
|
||||
<y>190</y>
|
||||
<width>251</width>
|
||||
<height>61</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="title">
|
||||
<string/>
|
||||
</property>
|
||||
<layout class="QFormLayout" name="formLayout_3">
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="label_3">
|
||||
<property name="text">
|
||||
<string>Condition</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1">
|
||||
<widget class="QLineEdit" name="txtCondition"/>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QGroupBox" name="groupBox_5">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>10</x>
|
||||
<y>90</y>
|
||||
<width>91</width>
|
||||
<height>71</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="title">
|
||||
<string/>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<widget class="QCheckBox" name="chkLog">
|
||||
<property name="enabled">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Log</string>
|
||||
</property>
|
||||
<property name="checked">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="chkEnable">
|
||||
<property name="text">
|
||||
<string>Enable</string>
|
||||
</property>
|
||||
<property name="checked">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections>
|
||||
<connection>
|
||||
<sender>buttonBox</sender>
|
||||
<signal>accepted()</signal>
|
||||
<receiver>BreakpointDialog</receiver>
|
||||
<slot>accept()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>248</x>
|
||||
<y>254</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>157</x>
|
||||
<y>274</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
<connection>
|
||||
<sender>buttonBox</sender>
|
||||
<signal>rejected()</signal>
|
||||
<receiver>BreakpointDialog</receiver>
|
||||
<slot>reject()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>316</x>
|
||||
<y>260</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>286</x>
|
||||
<y>274</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
</connections>
|
||||
</ui>
|
||||
694
pcsx2-qt/Debugger/Breakpoints/BreakpointModel.cpp
Normal file
694
pcsx2-qt/Debugger/Breakpoints/BreakpointModel.cpp
Normal file
@@ -0,0 +1,694 @@
|
||||
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
|
||||
// SPDX-License-Identifier: GPL-3.0+
|
||||
|
||||
#include "BreakpointModel.h"
|
||||
|
||||
#include "QtHost.h"
|
||||
#include "QtUtils.h"
|
||||
#include "Debugger/DebuggerSettingsManager.h"
|
||||
|
||||
#include "DebugTools/DebugInterface.h"
|
||||
#include "DebugTools/Breakpoints.h"
|
||||
#include "DebugTools/DisassemblyManager.h"
|
||||
#include "common/Console.h"
|
||||
|
||||
#include <QtWidgets/QMessageBox>
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
std::map<BreakPointCpu, BreakpointModel*> BreakpointModel::s_instances;
|
||||
|
||||
BreakpointModel::BreakpointModel(DebugInterface& cpu, QObject* parent)
|
||||
: QAbstractTableModel(parent)
|
||||
, m_cpu(cpu)
|
||||
{
|
||||
if (m_cpu.getCpuType() == BREAKPOINT_EE)
|
||||
{
|
||||
connect(g_emu_thread, &EmuThread::onGameChanged, this, [this](const QString& title) {
|
||||
if (title.isEmpty())
|
||||
return;
|
||||
|
||||
if (rowCount() == 0)
|
||||
DebuggerSettingsManager::loadGameSettings(this);
|
||||
});
|
||||
|
||||
DebuggerSettingsManager::loadGameSettings(this);
|
||||
}
|
||||
|
||||
connect(this, &BreakpointModel::dataChanged, this, &BreakpointModel::refreshData);
|
||||
}
|
||||
|
||||
BreakpointModel* BreakpointModel::getInstance(DebugInterface& cpu)
|
||||
{
|
||||
auto iterator = s_instances.find(cpu.getCpuType());
|
||||
if (iterator == s_instances.end())
|
||||
iterator = s_instances.emplace(cpu.getCpuType(), new BreakpointModel(cpu)).first;
|
||||
|
||||
return iterator->second;
|
||||
}
|
||||
|
||||
int BreakpointModel::rowCount(const QModelIndex&) const
|
||||
{
|
||||
return m_breakpoints.size();
|
||||
}
|
||||
|
||||
int BreakpointModel::columnCount(const QModelIndex&) const
|
||||
{
|
||||
return BreakpointColumns::COLUMN_COUNT;
|
||||
}
|
||||
|
||||
QVariant BreakpointModel::data(const QModelIndex& index, int role) const
|
||||
{
|
||||
const size_t row = static_cast<size_t>(index.row());
|
||||
if (!index.isValid() || row >= m_breakpoints.size())
|
||||
return QVariant();
|
||||
|
||||
const BreakpointMemcheck& bp_mc = m_breakpoints[row];
|
||||
|
||||
if (role == Qt::DisplayRole)
|
||||
{
|
||||
if (const auto* bp = std::get_if<BreakPoint>(&bp_mc))
|
||||
{
|
||||
switch (index.column())
|
||||
{
|
||||
case BreakpointColumns::ENABLED:
|
||||
return (bp->enabled) ? tr("Enabled") : tr("Disabled");
|
||||
case BreakpointColumns::TYPE:
|
||||
return tr("Execute");
|
||||
case BreakpointColumns::OFFSET:
|
||||
return QtUtils::FilledQStringFromValue(bp->addr, 16);
|
||||
case BreakpointColumns::DESCRIPTION:
|
||||
return QString::fromStdString(bp->description);
|
||||
case BreakpointColumns::SIZE_LABEL:
|
||||
return QString::fromStdString(m_cpu.GetSymbolGuardian().FunctionStartingAtAddress(bp->addr).name);
|
||||
case BreakpointColumns::OPCODE:
|
||||
// Note: Fix up the disassemblymanager so we can use it here, instead of calling a function through the disassemblyview (yuck)
|
||||
return m_cpu.disasm(bp->addr, true).c_str();
|
||||
case BreakpointColumns::CONDITION:
|
||||
return bp->hasCond ? QString::fromStdString(bp->cond.expressionString) : "";
|
||||
case BreakpointColumns::HITS:
|
||||
return tr("--");
|
||||
}
|
||||
}
|
||||
else if (const auto* mc = std::get_if<MemCheck>(&bp_mc))
|
||||
{
|
||||
switch (index.column())
|
||||
{
|
||||
case BreakpointColumns::ENABLED:
|
||||
return (mc->result & MEMCHECK_BREAK) ? tr("Enabled") : tr("Disabled");
|
||||
case BreakpointColumns::TYPE:
|
||||
{
|
||||
QString type("");
|
||||
type += (mc->memCond & MEMCHECK_READ) ? tr("Read") : "";
|
||||
type += ((mc->memCond & MEMCHECK_READWRITE) == MEMCHECK_READWRITE) ? ", " : " ";
|
||||
//: (C) = changes, as in "look for changes".
|
||||
type += (mc->memCond & MEMCHECK_WRITE) ? (mc->memCond & MEMCHECK_WRITE_ONCHANGE) ? tr("Write(C)") : tr("Write") : "";
|
||||
return type;
|
||||
}
|
||||
case BreakpointColumns::OFFSET:
|
||||
return QtUtils::FilledQStringFromValue(mc->start, 16);
|
||||
case BreakpointColumns::DESCRIPTION:
|
||||
return QString::fromStdString(mc->description);
|
||||
case BreakpointColumns::SIZE_LABEL:
|
||||
return QString::number(mc->end - mc->start, 16);
|
||||
case BreakpointColumns::OPCODE:
|
||||
return tr("--"); // Our address is going to point to memory, no purpose in printing the op
|
||||
case BreakpointColumns::CONDITION:
|
||||
return mc->hasCond ? QString::fromStdString(mc->cond.expressionString) : "";
|
||||
case BreakpointColumns::HITS:
|
||||
return QString::number(mc->numHits);
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (role == Qt::EditRole)
|
||||
{
|
||||
if (const auto* bp = std::get_if<BreakPoint>(&bp_mc))
|
||||
{
|
||||
switch (index.column())
|
||||
{
|
||||
case BreakpointColumns::CONDITION:
|
||||
return bp->hasCond ? QString::fromStdString(bp->cond.expressionString) : "";
|
||||
case BreakpointColumns::DESCRIPTION:
|
||||
return QString::fromStdString(bp->description);
|
||||
}
|
||||
}
|
||||
else if (const auto* mc = std::get_if<MemCheck>(&bp_mc))
|
||||
{
|
||||
switch (index.column())
|
||||
{
|
||||
case BreakpointColumns::CONDITION:
|
||||
return mc->hasCond ? QString::fromStdString(mc->cond.expressionString) : "";
|
||||
case BreakpointColumns::DESCRIPTION:
|
||||
return QString::fromStdString(mc->description);
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (role == BreakpointModel::DataRole)
|
||||
{
|
||||
if (const auto* bp = std::get_if<BreakPoint>(&bp_mc))
|
||||
{
|
||||
switch (index.column())
|
||||
{
|
||||
case BreakpointColumns::ENABLED:
|
||||
return static_cast<int>(bp->enabled);
|
||||
case BreakpointColumns::TYPE:
|
||||
return MEMCHECK_INVALID;
|
||||
case BreakpointColumns::OFFSET:
|
||||
return bp->addr;
|
||||
case BreakpointColumns::DESCRIPTION:
|
||||
return QString::fromStdString(bp->description);
|
||||
case BreakpointColumns::SIZE_LABEL:
|
||||
return QString::fromStdString(m_cpu.GetSymbolGuardian().FunctionStartingAtAddress(bp->addr).name);
|
||||
case BreakpointColumns::OPCODE:
|
||||
// Note: Fix up the disassemblymanager so we can use it here, instead of calling a function through the disassemblyview (yuck)
|
||||
return m_cpu.disasm(bp->addr, false).c_str();
|
||||
case BreakpointColumns::CONDITION:
|
||||
return bp->hasCond ? QString::fromStdString(bp->cond.expressionString) : "";
|
||||
case BreakpointColumns::HITS:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
else if (const auto* mc = std::get_if<MemCheck>(&bp_mc))
|
||||
{
|
||||
switch (index.column())
|
||||
{
|
||||
case BreakpointColumns::ENABLED:
|
||||
return (mc->result & MEMCHECK_BREAK);
|
||||
case BreakpointColumns::TYPE:
|
||||
return mc->memCond;
|
||||
case BreakpointColumns::OFFSET:
|
||||
return mc->start;
|
||||
case BreakpointColumns::DESCRIPTION:
|
||||
return QString::fromStdString(mc->description);
|
||||
case BreakpointColumns::SIZE_LABEL:
|
||||
return mc->end - mc->start;
|
||||
case BreakpointColumns::OPCODE:
|
||||
return "";
|
||||
case BreakpointColumns::CONDITION:
|
||||
return mc->hasCond ? QString::fromStdString(mc->cond.expressionString) : "";
|
||||
case BreakpointColumns::HITS:
|
||||
return mc->numHits;
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (role == BreakpointModel::ExportRole)
|
||||
{
|
||||
if (const auto* bp = std::get_if<BreakPoint>(&bp_mc))
|
||||
{
|
||||
switch (index.column())
|
||||
{
|
||||
case BreakpointColumns::ENABLED:
|
||||
return static_cast<int>(bp->enabled);
|
||||
case BreakpointColumns::TYPE:
|
||||
return MEMCHECK_INVALID;
|
||||
case BreakpointColumns::OFFSET:
|
||||
return QtUtils::FilledQStringFromValue(bp->addr, 16);
|
||||
case BreakpointColumns::DESCRIPTION:
|
||||
return QString::fromStdString(bp->description);
|
||||
case BreakpointColumns::SIZE_LABEL:
|
||||
return QString::fromStdString(m_cpu.GetSymbolGuardian().FunctionStartingAtAddress(bp->addr).name);
|
||||
case BreakpointColumns::OPCODE:
|
||||
// Note: Fix up the disassemblymanager so we can use it here, instead of calling a function through the disassemblyview (yuck)
|
||||
return m_cpu.disasm(bp->addr, false).c_str();
|
||||
case BreakpointColumns::CONDITION:
|
||||
return bp->hasCond ? QString::fromStdString(bp->cond.expressionString) : "";
|
||||
case BreakpointColumns::HITS:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
else if (const auto* mc = std::get_if<MemCheck>(&bp_mc))
|
||||
{
|
||||
switch (index.column())
|
||||
{
|
||||
case BreakpointColumns::TYPE:
|
||||
return mc->memCond;
|
||||
case BreakpointColumns::OFFSET:
|
||||
return QtUtils::FilledQStringFromValue(mc->start, 16);
|
||||
case BreakpointColumns::DESCRIPTION:
|
||||
return QString::fromStdString(mc->description);
|
||||
case BreakpointColumns::SIZE_LABEL:
|
||||
return mc->end - mc->start;
|
||||
case BreakpointColumns::OPCODE:
|
||||
return "";
|
||||
case BreakpointColumns::CONDITION:
|
||||
return mc->hasCond ? QString::fromStdString(mc->cond.expressionString) : "";
|
||||
case BreakpointColumns::HITS:
|
||||
return mc->numHits;
|
||||
case BreakpointColumns::ENABLED:
|
||||
return (mc->result & MEMCHECK_BREAK);
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (role == Qt::CheckStateRole)
|
||||
{
|
||||
if (index.column() == 0)
|
||||
{
|
||||
if (const auto* bp = std::get_if<BreakPoint>(&bp_mc))
|
||||
{
|
||||
return bp->enabled ? Qt::CheckState::Checked : Qt::CheckState::Unchecked;
|
||||
}
|
||||
else if (const auto* mc = std::get_if<MemCheck>(&bp_mc))
|
||||
{
|
||||
return (mc->result & MEMCHECK_BREAK) ? Qt::CheckState::Checked : Qt::CheckState::Unchecked;
|
||||
}
|
||||
}
|
||||
}
|
||||
return QVariant();
|
||||
}
|
||||
|
||||
QVariant BreakpointModel::headerData(int section, Qt::Orientation orientation, int role) const
|
||||
{
|
||||
if (role == Qt::DisplayRole && orientation == Qt::Horizontal)
|
||||
{
|
||||
switch (section)
|
||||
{
|
||||
case BreakpointColumns::TYPE:
|
||||
//: Warning: limited space available. Abbreviate if needed.
|
||||
return tr("TYPE");
|
||||
case BreakpointColumns::OFFSET:
|
||||
//: Warning: limited space available. Abbreviate if needed.
|
||||
return tr("OFFSET");
|
||||
case BreakpointColumns::DESCRIPTION:
|
||||
return "DESCRIPTION";
|
||||
case BreakpointColumns::SIZE_LABEL:
|
||||
//: Warning: limited space available. Abbreviate if needed.
|
||||
return tr("SIZE / LABEL");
|
||||
case BreakpointColumns::OPCODE:
|
||||
//: Warning: limited space available. Abbreviate if needed.
|
||||
return tr("INSTRUCTION");
|
||||
case BreakpointColumns::CONDITION:
|
||||
//: Warning: limited space available. Abbreviate if needed.
|
||||
return tr("CONDITION");
|
||||
case BreakpointColumns::HITS:
|
||||
//: Warning: limited space available. Abbreviate if needed.
|
||||
return tr("HITS");
|
||||
case BreakpointColumns::ENABLED:
|
||||
//: Warning: limited space available. Abbreviate if needed.
|
||||
return tr("X");
|
||||
default:
|
||||
return QVariant();
|
||||
}
|
||||
}
|
||||
if (role == Qt::UserRole && orientation == Qt::Horizontal)
|
||||
{
|
||||
switch (section)
|
||||
{
|
||||
case BreakpointColumns::TYPE:
|
||||
return "TYPE";
|
||||
case BreakpointColumns::OFFSET:
|
||||
return "OFFSET";
|
||||
case BreakpointColumns::DESCRIPTION:
|
||||
return "DESCRIPTION";
|
||||
case BreakpointColumns::SIZE_LABEL:
|
||||
return "SIZE / LABEL";
|
||||
case BreakpointColumns::OPCODE:
|
||||
return "INSTRUCTION";
|
||||
case BreakpointColumns::CONDITION:
|
||||
return "CONDITION";
|
||||
case BreakpointColumns::HITS:
|
||||
return "HITS";
|
||||
case BreakpointColumns::ENABLED:
|
||||
return "X";
|
||||
default:
|
||||
return QVariant();
|
||||
}
|
||||
}
|
||||
return QVariant();
|
||||
}
|
||||
|
||||
Qt::ItemFlags BreakpointModel::flags(const QModelIndex& index) const
|
||||
{
|
||||
switch (index.column())
|
||||
{
|
||||
case BreakpointColumns::CONDITION:
|
||||
case BreakpointColumns::DESCRIPTION:
|
||||
return Qt::ItemFlag::ItemIsEnabled | Qt::ItemFlag::ItemIsSelectable | Qt::ItemFlag::ItemIsEditable;
|
||||
case BreakpointColumns::TYPE:
|
||||
case BreakpointColumns::OPCODE:
|
||||
case BreakpointColumns::HITS:
|
||||
case BreakpointColumns::OFFSET:
|
||||
case BreakpointColumns::SIZE_LABEL:
|
||||
return Qt::ItemFlag::ItemIsEnabled | Qt::ItemFlag::ItemIsSelectable;
|
||||
case BreakpointColumns::ENABLED:
|
||||
return Qt::ItemFlag::ItemIsUserCheckable | Qt::ItemFlag::ItemIsEnabled | Qt::ItemFlag::ItemIsSelectable;
|
||||
}
|
||||
|
||||
return index.flags();
|
||||
}
|
||||
|
||||
bool BreakpointModel::setData(const QModelIndex& index, const QVariant& value, int role)
|
||||
{
|
||||
const size_t row = static_cast<size_t>(index.row());
|
||||
if (!index.isValid() || row >= m_breakpoints.size())
|
||||
return false;
|
||||
|
||||
const BreakpointMemcheck& bp_mc = m_breakpoints[row];
|
||||
|
||||
if (role == Qt::CheckStateRole && index.column() == BreakpointColumns::ENABLED)
|
||||
{
|
||||
if (const auto* bp = std::get_if<BreakPoint>(&bp_mc))
|
||||
{
|
||||
Host::RunOnCPUThread([cpu = this->m_cpu.getCpuType(), bp = *bp, enabled = value.toBool()] {
|
||||
CBreakPoints::ChangeBreakPoint(cpu, bp.addr, enabled);
|
||||
});
|
||||
}
|
||||
else if (const auto* mc = std::get_if<MemCheck>(&bp_mc))
|
||||
{
|
||||
Host::RunOnCPUThread([cpu = this->m_cpu.getCpuType(), mc = *mc] {
|
||||
CBreakPoints::ChangeMemCheck(cpu, mc.start, mc.end, mc.memCond,
|
||||
MemCheckResult(mc.result ^ MEMCHECK_BREAK));
|
||||
});
|
||||
}
|
||||
emit dataChanged(index, index);
|
||||
return true;
|
||||
}
|
||||
else if (role == Qt::EditRole && index.column() == BreakpointColumns::CONDITION)
|
||||
{
|
||||
if (auto* bp = std::get_if<BreakPoint>(&bp_mc))
|
||||
{
|
||||
const QString condValue = value.toString();
|
||||
|
||||
if (condValue.isEmpty())
|
||||
{
|
||||
if (bp->hasCond)
|
||||
{
|
||||
Host::RunOnCPUThread([cpu = m_cpu.getCpuType(), bp] {
|
||||
CBreakPoints::ChangeBreakPointRemoveCond(cpu, bp->addr);
|
||||
});
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
PostfixExpression expr;
|
||||
|
||||
std::string error;
|
||||
if (!m_cpu.initExpression(condValue.toLocal8Bit().constData(), expr, error))
|
||||
{
|
||||
QMessageBox::warning(nullptr, "Condition Error", QString::fromStdString(error));
|
||||
return false;
|
||||
}
|
||||
|
||||
BreakPointCond cond;
|
||||
cond.debug = &m_cpu;
|
||||
cond.expression = expr;
|
||||
cond.expressionString = condValue.toStdString();
|
||||
|
||||
Host::RunOnCPUThread([cpu = m_cpu.getCpuType(), bp, cond] {
|
||||
CBreakPoints::ChangeBreakPointAddCond(cpu, bp->addr, cond);
|
||||
});
|
||||
}
|
||||
}
|
||||
else if (auto* mc = std::get_if<MemCheck>(&bp_mc))
|
||||
{
|
||||
const QString condValue = value.toString();
|
||||
|
||||
if (condValue.isEmpty())
|
||||
{
|
||||
if (mc->hasCond)
|
||||
{
|
||||
Host::RunOnCPUThread([cpu = m_cpu.getCpuType(), mc] {
|
||||
CBreakPoints::ChangeMemCheckRemoveCond(cpu, mc->start, mc->end);
|
||||
});
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
PostfixExpression expr;
|
||||
|
||||
std::string error;
|
||||
if (!m_cpu.initExpression(condValue.toLocal8Bit().constData(), expr, error))
|
||||
{
|
||||
QMessageBox::warning(nullptr, "Condition Error", QString::fromStdString(error));
|
||||
return false;
|
||||
}
|
||||
|
||||
BreakPointCond cond;
|
||||
cond.debug = &m_cpu;
|
||||
cond.expression = expr;
|
||||
cond.expressionString = condValue.toStdString();
|
||||
|
||||
Host::RunOnCPUThread([cpu = m_cpu.getCpuType(), mc, cond] {
|
||||
CBreakPoints::ChangeMemCheckAddCond(cpu, mc->start, mc->end, cond);
|
||||
});
|
||||
}
|
||||
}
|
||||
emit dataChanged(index, index);
|
||||
return true;
|
||||
}
|
||||
else if (role == Qt::EditRole && index.column() == BreakpointColumns::DESCRIPTION)
|
||||
{
|
||||
// Update BreakPoint description
|
||||
if (auto* bp = std::get_if<BreakPoint>(&bp_mc))
|
||||
{
|
||||
const QString descValue = value.toString();
|
||||
Host::RunOnCPUThread([cpu = m_cpu.getCpuType(), bp, descValue] {
|
||||
CBreakPoints::ChangeBreakPointDescription(cpu, bp->addr, descValue.toStdString());
|
||||
});
|
||||
}
|
||||
// Update MemCheck description
|
||||
else if (auto* mc = std::get_if<MemCheck>(&bp_mc))
|
||||
{
|
||||
const QString descValue = value.toString();
|
||||
Host::RunOnCPUThread([cpu = m_cpu.getCpuType(), mc, descValue] {
|
||||
CBreakPoints::ChangeMemCheckDescription(cpu, mc->start, mc->end, descValue.toStdString());
|
||||
});
|
||||
}
|
||||
emit dataChanged(index, index);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool BreakpointModel::removeRows(int row, int count, const QModelIndex& index)
|
||||
{
|
||||
const size_t begin_index = static_cast<size_t>(row);
|
||||
const size_t end_index = static_cast<size_t>(row + count);
|
||||
if (end_index > m_breakpoints.size())
|
||||
return false;
|
||||
|
||||
beginRemoveRows(index, row, row + count - 1);
|
||||
|
||||
for (size_t i = begin_index; i < end_index; i++)
|
||||
{
|
||||
auto bp_mc = m_breakpoints.at(i);
|
||||
|
||||
if (const auto* bp = std::get_if<BreakPoint>(&bp_mc))
|
||||
{
|
||||
Host::RunOnCPUThread([cpu = m_cpu.getCpuType(), addr = bp->addr] {
|
||||
CBreakPoints::RemoveBreakPoint(cpu, addr);
|
||||
});
|
||||
}
|
||||
else if (const auto* mc = std::get_if<MemCheck>(&bp_mc))
|
||||
{
|
||||
Host::RunOnCPUThread([cpu = m_cpu.getCpuType(), start = mc->start, end = mc->end] {
|
||||
CBreakPoints::RemoveMemCheck(cpu, start, end);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const auto begin = m_breakpoints.begin() + row;
|
||||
const auto end = begin + count;
|
||||
m_breakpoints.erase(begin, end);
|
||||
|
||||
endRemoveRows();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool BreakpointModel::insertBreakpointRows(int row, int count, std::vector<BreakpointMemcheck> breakpoints, const QModelIndex& index)
|
||||
{
|
||||
if (breakpoints.size() != static_cast<size_t>(count))
|
||||
return false;
|
||||
|
||||
beginInsertRows(index, row, row + (count - 1));
|
||||
|
||||
// After endInsertRows, Qt will try and validate our new rows
|
||||
// Because we add the breakpoints off of the UI thread, our new rows may not be visible yet
|
||||
// To prevent the (seemingly harmless?) warning emitted by enderInsertRows, add the breakpoints manually here as well
|
||||
m_breakpoints.insert(m_breakpoints.begin(), breakpoints.begin(), breakpoints.end());
|
||||
for (const auto& bp_mc : breakpoints)
|
||||
{
|
||||
if (const auto* bp = std::get_if<BreakPoint>(&bp_mc))
|
||||
{
|
||||
Host::RunOnCPUThread([cpu = m_cpu.getCpuType(), bp = *bp] {
|
||||
CBreakPoints::AddBreakPoint(cpu, bp.addr, false, bp.enabled);
|
||||
CBreakPoints::ChangeBreakPointDescription(cpu, bp.addr, bp.description);
|
||||
if (bp.hasCond)
|
||||
{
|
||||
CBreakPoints::ChangeBreakPointAddCond(cpu, bp.addr, bp.cond);
|
||||
}
|
||||
});
|
||||
}
|
||||
else if (const auto* mc = std::get_if<MemCheck>(&bp_mc))
|
||||
{
|
||||
Host::RunOnCPUThread([cpu = m_cpu.getCpuType(), mc = *mc] {
|
||||
CBreakPoints::AddMemCheck(cpu, mc.start, mc.end, mc.memCond, mc.result);
|
||||
CBreakPoints::ChangeMemCheckDescription(cpu, mc.start, mc.end, mc.description);
|
||||
if (mc.hasCond)
|
||||
{
|
||||
CBreakPoints::ChangeMemCheckAddCond(cpu, mc.start, mc.end, mc.cond);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
endInsertRows();
|
||||
return true;
|
||||
}
|
||||
|
||||
void BreakpointModel::refreshData()
|
||||
{
|
||||
Host::RunOnCPUThread([this]() mutable {
|
||||
std::vector<BreakpointMemcheck> all_breakpoints;
|
||||
std::ranges::move(CBreakPoints::GetBreakpoints(m_cpu.getCpuType(), false), std::back_inserter(all_breakpoints));
|
||||
std::ranges::move(CBreakPoints::GetMemChecks(m_cpu.getCpuType()), std::back_inserter(all_breakpoints));
|
||||
|
||||
QtHost::RunOnUIThread([this, breakpoints = std::move(all_breakpoints)]() mutable {
|
||||
beginResetModel();
|
||||
m_breakpoints = std::move(breakpoints);
|
||||
endResetModel();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
void BreakpointModel::loadBreakpointFromFieldList(QStringList fields)
|
||||
{
|
||||
std::string error;
|
||||
|
||||
bool ok;
|
||||
if (fields.size() != BreakpointColumns::COLUMN_COUNT)
|
||||
{
|
||||
Console.WriteLn("Debugger Breakpoint Model: Invalid number of columns, skipping");
|
||||
return;
|
||||
}
|
||||
|
||||
const int type = fields[BreakpointColumns::TYPE].toUInt(&ok);
|
||||
if (!ok)
|
||||
{
|
||||
Console.WriteLn("Debugger Breakpoint Model: Failed to parse type '%s', skipping",
|
||||
fields[BreakpointColumns::TYPE].toUtf8().constData());
|
||||
return;
|
||||
}
|
||||
|
||||
// This is how we differentiate between breakpoints and memchecks
|
||||
if (type == MEMCHECK_INVALID)
|
||||
{
|
||||
BreakPoint bp;
|
||||
|
||||
// Address
|
||||
bp.addr = fields[BreakpointColumns::OFFSET].toUInt(&ok, 16);
|
||||
if (!ok)
|
||||
{
|
||||
Console.WriteLn("Debugger Breakpoint Model: Failed to parse address '%s', skipping",
|
||||
fields[BreakpointColumns::OFFSET].toUtf8().constData());
|
||||
return;
|
||||
}
|
||||
|
||||
// Condition
|
||||
if (!fields[BreakpointColumns::CONDITION].isEmpty())
|
||||
{
|
||||
PostfixExpression expr;
|
||||
bp.hasCond = true;
|
||||
bp.cond.debug = &m_cpu;
|
||||
|
||||
if (!m_cpu.initExpression(fields[BreakpointColumns::CONDITION].toUtf8().constData(), expr, error))
|
||||
{
|
||||
Console.WriteLn("Debugger Breakpoint Model: Failed to parse cond '%s', skipping",
|
||||
fields[BreakpointModel::CONDITION].toUtf8().constData());
|
||||
return;
|
||||
}
|
||||
bp.cond.expression = expr;
|
||||
bp.cond.expressionString = fields[BreakpointColumns::CONDITION].toStdString();
|
||||
}
|
||||
|
||||
// Enabled
|
||||
bp.enabled = fields[BreakpointColumns::ENABLED].toUInt(&ok);
|
||||
if (!ok)
|
||||
{
|
||||
Console.WriteLn("Debugger Breakpoint Model: Failed to parse enable flag '%s', skipping",
|
||||
fields[BreakpointColumns::ENABLED].toUtf8().constData());
|
||||
return;
|
||||
}
|
||||
|
||||
// Description
|
||||
if (!fields[BreakpointColumns::DESCRIPTION].isEmpty())
|
||||
{
|
||||
bp.description = fields[BreakpointColumns::DESCRIPTION].toStdString();
|
||||
}
|
||||
|
||||
insertBreakpointRows(0, 1, {bp});
|
||||
}
|
||||
else
|
||||
{
|
||||
MemCheck mc;
|
||||
// Mode
|
||||
if (type >= MEMCHECK_INVALID)
|
||||
{
|
||||
Console.WriteLn("Debugger Breakpoint Model: Failed to parse cond type '%s', skipping",
|
||||
fields[BreakpointColumns::TYPE].toUtf8().constData());
|
||||
return;
|
||||
}
|
||||
mc.memCond = static_cast<MemCheckCondition>(type);
|
||||
|
||||
// Address
|
||||
QString test = fields[BreakpointColumns::OFFSET];
|
||||
mc.start = fields[BreakpointColumns::OFFSET].toUInt(&ok, 16);
|
||||
if (!ok)
|
||||
{
|
||||
Console.WriteLn("Debugger Breakpoint Model: Failed to parse address '%s', skipping",
|
||||
fields[BreakpointColumns::OFFSET].toUtf8().constData());
|
||||
return;
|
||||
}
|
||||
|
||||
// Size
|
||||
mc.end = fields[BreakpointColumns::SIZE_LABEL].toUInt(&ok) + mc.start;
|
||||
if (!ok)
|
||||
{
|
||||
Console.WriteLn("Debugger Breakpoint Model: Failed to parse length '%s', skipping",
|
||||
fields[BreakpointColumns::SIZE_LABEL].toUtf8().constData());
|
||||
return;
|
||||
}
|
||||
|
||||
// Condition
|
||||
if (!fields[BreakpointColumns::CONDITION].isEmpty())
|
||||
{
|
||||
PostfixExpression expr;
|
||||
mc.hasCond = true;
|
||||
mc.cond.debug = &m_cpu;
|
||||
|
||||
if (!m_cpu.initExpression(fields[BreakpointColumns::CONDITION].toUtf8().constData(), expr, error))
|
||||
{
|
||||
Console.WriteLn("Debugger Breakpoint Model: Failed to parse cond '%s', skipping",
|
||||
fields[BreakpointColumns::CONDITION].toUtf8().constData());
|
||||
return;
|
||||
}
|
||||
mc.cond.expression = expr;
|
||||
mc.cond.expressionString = fields[BreakpointColumns::CONDITION].toStdString();
|
||||
}
|
||||
|
||||
// Result
|
||||
const int result = fields[BreakpointColumns::ENABLED].toUInt(&ok);
|
||||
if (!ok)
|
||||
{
|
||||
Console.WriteLn("Debugger Breakpoint Model: Failed to parse result flag '%s', skipping",
|
||||
fields[BreakpointColumns::ENABLED].toUtf8().constData());
|
||||
return;
|
||||
}
|
||||
mc.result = static_cast<MemCheckResult>(result);
|
||||
|
||||
// Description
|
||||
if (!fields[BreakpointColumns::DESCRIPTION].isEmpty())
|
||||
{
|
||||
mc.description = fields[BreakpointColumns::DESCRIPTION].toStdString();
|
||||
}
|
||||
|
||||
insertBreakpointRows(0, 1, {mc});
|
||||
}
|
||||
}
|
||||
|
||||
void BreakpointModel::clear()
|
||||
{
|
||||
beginResetModel();
|
||||
m_breakpoints.clear();
|
||||
endResetModel();
|
||||
}
|
||||
73
pcsx2-qt/Debugger/Breakpoints/BreakpointModel.h
Normal file
73
pcsx2-qt/Debugger/Breakpoints/BreakpointModel.h
Normal file
@@ -0,0 +1,73 @@
|
||||
// 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"
|
||||
#include "DebugTools/Breakpoints.h"
|
||||
|
||||
using BreakpointMemcheck = std::variant<BreakPoint, MemCheck>;
|
||||
|
||||
class BreakpointModel : public QAbstractTableModel
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
enum BreakpointColumns : int
|
||||
{
|
||||
ENABLED = 0,
|
||||
TYPE,
|
||||
OFFSET,
|
||||
DESCRIPTION,
|
||||
SIZE_LABEL,
|
||||
OPCODE,
|
||||
CONDITION,
|
||||
HITS,
|
||||
COLUMN_COUNT
|
||||
};
|
||||
|
||||
enum BreakpointRoles : int
|
||||
{
|
||||
DataRole = Qt::UserRole,
|
||||
ExportRole = Qt::UserRole + 1,
|
||||
};
|
||||
|
||||
static constexpr QHeaderView::ResizeMode HeaderResizeModes[BreakpointColumns::COLUMN_COUNT] = {
|
||||
QHeaderView::ResizeMode::ResizeToContents,
|
||||
QHeaderView::ResizeMode::ResizeToContents,
|
||||
QHeaderView::ResizeMode::ResizeToContents,
|
||||
QHeaderView::ResizeMode::ResizeToContents,
|
||||
QHeaderView::ResizeMode::Stretch,
|
||||
QHeaderView::ResizeMode::Stretch,
|
||||
QHeaderView::ResizeMode::ResizeToContents,
|
||||
QHeaderView::ResizeMode::ResizeToContents,
|
||||
};
|
||||
|
||||
static BreakpointModel* getInstance(DebugInterface& cpu);
|
||||
|
||||
int rowCount(const QModelIndex& parent = QModelIndex()) const override;
|
||||
int columnCount(const QModelIndex& parent = QModelIndex()) const override;
|
||||
QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override;
|
||||
QVariant headerData(int section, Qt::Orientation orientation, int role) const override;
|
||||
Qt::ItemFlags flags(const QModelIndex& index) const override;
|
||||
bool setData(const QModelIndex& index, const QVariant& value, int role) override;
|
||||
bool removeRows(int row, int count, const QModelIndex& index = QModelIndex()) override;
|
||||
bool insertBreakpointRows(int row, int count, std::vector<BreakpointMemcheck> breakpoints, const QModelIndex& index = QModelIndex());
|
||||
void loadBreakpointFromFieldList(QStringList breakpointFields);
|
||||
|
||||
BreakpointMemcheck at(int row) const { return m_breakpoints.at(row); };
|
||||
|
||||
void refreshData();
|
||||
void clear();
|
||||
|
||||
private:
|
||||
BreakpointModel(DebugInterface& cpu, QObject* parent = nullptr);
|
||||
|
||||
DebugInterface& m_cpu;
|
||||
std::vector<BreakpointMemcheck> m_breakpoints;
|
||||
|
||||
static std::map<BreakPointCpu, BreakpointModel*> s_instances;
|
||||
};
|
||||
181
pcsx2-qt/Debugger/Breakpoints/BreakpointView.cpp
Normal file
181
pcsx2-qt/Debugger/Breakpoints/BreakpointView.cpp
Normal file
@@ -0,0 +1,181 @@
|
||||
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
|
||||
// SPDX-License-Identifier: GPL-3.0+
|
||||
|
||||
#include "BreakpointView.h"
|
||||
|
||||
#include "QtUtils.h"
|
||||
#include "Debugger/DebuggerSettingsManager.h"
|
||||
#include "BreakpointDialog.h"
|
||||
#include "BreakpointModel.h"
|
||||
|
||||
#include <QtGui/QClipboard>
|
||||
|
||||
BreakpointView::BreakpointView(const DebuggerViewParameters& parameters)
|
||||
: DebuggerView(parameters, DISALLOW_MULTIPLE_INSTANCES)
|
||||
, m_model(BreakpointModel::getInstance(cpu()))
|
||||
{
|
||||
m_ui.setupUi(this);
|
||||
|
||||
m_ui.breakpointList->setContextMenuPolicy(Qt::CustomContextMenu);
|
||||
connect(m_ui.breakpointList, &QTableView::customContextMenuRequested, this, &BreakpointView::openContextMenu);
|
||||
connect(m_ui.breakpointList, &QTableView::doubleClicked, this, &BreakpointView::onDoubleClicked);
|
||||
|
||||
m_ui.breakpointList->setModel(m_model);
|
||||
this->resizeColumns();
|
||||
}
|
||||
|
||||
void BreakpointView::onDoubleClicked(const QModelIndex& index)
|
||||
{
|
||||
if (index.isValid() && index.column() == BreakpointModel::OFFSET)
|
||||
goToInDisassembler(m_model->data(index, BreakpointModel::DataRole).toUInt(), true);
|
||||
}
|
||||
|
||||
void BreakpointView::openContextMenu(QPoint pos)
|
||||
{
|
||||
QMenu* menu = new QMenu(m_ui.breakpointList);
|
||||
menu->setAttribute(Qt::WA_DeleteOnClose);
|
||||
|
||||
if (cpu().isAlive())
|
||||
{
|
||||
QAction* newAction = menu->addAction(tr("New"));
|
||||
connect(newAction, &QAction::triggered, this, &BreakpointView::contextNew);
|
||||
|
||||
const QItemSelectionModel* selModel = m_ui.breakpointList->selectionModel();
|
||||
|
||||
if (selModel->hasSelection())
|
||||
{
|
||||
QAction* editAction = menu->addAction(tr("Edit"));
|
||||
connect(editAction, &QAction::triggered, this, &BreakpointView::contextEdit);
|
||||
|
||||
if (selModel->selectedIndexes().count() == 1)
|
||||
{
|
||||
QAction* copyAction = menu->addAction(tr("Copy"));
|
||||
connect(copyAction, &QAction::triggered, this, &BreakpointView::contextCopy);
|
||||
}
|
||||
|
||||
QAction* deleteAction = menu->addAction(tr("Delete"));
|
||||
connect(deleteAction, &QAction::triggered, this, &BreakpointView::contextDelete);
|
||||
}
|
||||
}
|
||||
|
||||
menu->addSeparator();
|
||||
if (m_model->rowCount() > 0)
|
||||
{
|
||||
QAction* actionExport = menu->addAction(tr("Copy all as CSV"));
|
||||
connect(actionExport, &QAction::triggered, [this]() {
|
||||
// It's important to use the Export Role here to allow pasting to be translation agnostic
|
||||
QGuiApplication::clipboard()->setText(
|
||||
QtUtils::AbstractItemModelToCSV(m_model, BreakpointModel::ExportRole, true));
|
||||
});
|
||||
}
|
||||
|
||||
if (cpu().isAlive())
|
||||
{
|
||||
QAction* actionImport = menu->addAction(tr("Paste from CSV"));
|
||||
connect(actionImport, &QAction::triggered, this, &BreakpointView::contextPasteCSV);
|
||||
|
||||
if (cpu().getCpuType() == BREAKPOINT_EE)
|
||||
{
|
||||
QAction* actionLoad = menu->addAction(tr("Load from Settings"));
|
||||
connect(actionLoad, &QAction::triggered, [this]() {
|
||||
m_model->clear();
|
||||
DebuggerSettingsManager::loadGameSettings(m_model);
|
||||
});
|
||||
|
||||
QAction* actionSave = menu->addAction(tr("Save to Settings"));
|
||||
connect(actionSave, &QAction::triggered, this, &BreakpointView::saveBreakpointsToDebuggerSettings);
|
||||
}
|
||||
}
|
||||
|
||||
menu->popup(m_ui.breakpointList->viewport()->mapToGlobal(pos));
|
||||
}
|
||||
|
||||
void BreakpointView::contextCopy()
|
||||
{
|
||||
const QItemSelectionModel* selModel = m_ui.breakpointList->selectionModel();
|
||||
|
||||
if (!selModel->hasSelection())
|
||||
return;
|
||||
|
||||
QGuiApplication::clipboard()->setText(m_model->data(selModel->currentIndex()).toString());
|
||||
}
|
||||
|
||||
void BreakpointView::contextDelete()
|
||||
{
|
||||
const QItemSelectionModel* selModel = m_ui.breakpointList->selectionModel();
|
||||
|
||||
if (!selModel->hasSelection())
|
||||
return;
|
||||
|
||||
QModelIndexList indices = selModel->selectedIndexes();
|
||||
|
||||
std::set<int> rows;
|
||||
for (QModelIndex index : indices)
|
||||
rows.emplace(index.row());
|
||||
|
||||
for (auto row = rows.rbegin(); row != rows.rend(); row++)
|
||||
m_model->removeRows(*row, 1);
|
||||
}
|
||||
|
||||
void BreakpointView::contextNew()
|
||||
{
|
||||
BreakpointDialog* bpDialog = new BreakpointDialog(this, &cpu(), *m_model);
|
||||
connect(bpDialog, &QDialog::accepted, this, &BreakpointView::resizeColumns);
|
||||
bpDialog->setAttribute(Qt::WA_DeleteOnClose);
|
||||
bpDialog->show();
|
||||
}
|
||||
|
||||
void BreakpointView::contextEdit()
|
||||
{
|
||||
const QItemSelectionModel* selModel = m_ui.breakpointList->selectionModel();
|
||||
|
||||
if (!selModel->hasSelection())
|
||||
return;
|
||||
|
||||
const int selectedRow = selModel->selectedIndexes().first().row();
|
||||
|
||||
auto bpObject = m_model->at(selectedRow);
|
||||
|
||||
BreakpointDialog* bpDialog = new BreakpointDialog(this, &cpu(), *m_model, bpObject, selectedRow);
|
||||
connect(bpDialog, &QDialog::accepted, this, &BreakpointView::resizeColumns);
|
||||
bpDialog->setAttribute(Qt::WA_DeleteOnClose);
|
||||
bpDialog->show();
|
||||
}
|
||||
|
||||
void BreakpointView::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 eachQuotePair(R"("([^"]|\\.)*")");
|
||||
QRegularExpressionMatchIterator it = eachQuotePair.globalMatch(line);
|
||||
while (it.hasNext())
|
||||
{
|
||||
QRegularExpressionMatch match = it.next();
|
||||
QString matchedValue = match.captured(0);
|
||||
fields << matchedValue.mid(1, matchedValue.length() - 2);
|
||||
}
|
||||
m_model->loadBreakpointFromFieldList(fields);
|
||||
}
|
||||
}
|
||||
|
||||
void BreakpointView::saveBreakpointsToDebuggerSettings()
|
||||
{
|
||||
DebuggerSettingsManager::saveGameSettings(m_model);
|
||||
}
|
||||
|
||||
void BreakpointView::resizeColumns()
|
||||
{
|
||||
for (std::size_t i = 0; auto mode : BreakpointModel::HeaderResizeModes)
|
||||
{
|
||||
m_ui.breakpointList->horizontalHeader()->setSectionResizeMode(i, mode);
|
||||
i++;
|
||||
}
|
||||
}
|
||||
43
pcsx2-qt/Debugger/Breakpoints/BreakpointView.h
Normal file
43
pcsx2-qt/Debugger/Breakpoints/BreakpointView.h
Normal file
@@ -0,0 +1,43 @@
|
||||
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
|
||||
// SPDX-License-Identifier: GPL-3.0+
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "ui_BreakpointView.h"
|
||||
|
||||
#include "BreakpointModel.h"
|
||||
|
||||
#include "Debugger/DebuggerView.h"
|
||||
|
||||
#include "DebugTools/DebugInterface.h"
|
||||
#include "DebugTools/DisassemblyManager.h"
|
||||
|
||||
#include <QtWidgets/QMenu>
|
||||
#include <QtWidgets/QTabBar>
|
||||
#include <QtGui/QPainter>
|
||||
|
||||
class BreakpointView : public DebuggerView
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
BreakpointView(const DebuggerViewParameters& parameters);
|
||||
|
||||
void onDoubleClicked(const QModelIndex& index);
|
||||
void openContextMenu(QPoint pos);
|
||||
|
||||
void contextCopy();
|
||||
void contextDelete();
|
||||
void contextNew();
|
||||
void contextEdit();
|
||||
void contextPasteCSV();
|
||||
|
||||
void resizeColumns();
|
||||
|
||||
void saveBreakpointsToDebuggerSettings();
|
||||
|
||||
private:
|
||||
Ui::BreakpointView m_ui;
|
||||
|
||||
BreakpointModel* m_model;
|
||||
};
|
||||
39
pcsx2-qt/Debugger/Breakpoints/BreakpointView.ui
Normal file
39
pcsx2-qt/Debugger/Breakpoints/BreakpointView.ui
Normal file
@@ -0,0 +1,39 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>BreakpointView</class>
|
||||
<widget class="QWidget" name="BreakpointView">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>400</width>
|
||||
<height>300</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Form</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="breakpointList"/>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
||||
56
pcsx2-qt/Debugger/DebuggerEvents.h
Normal file
56
pcsx2-qt/Debugger/DebuggerEvents.h
Normal file
@@ -0,0 +1,56 @@
|
||||
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
|
||||
// SPDX-License-Identifier: GPL-3.0+
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <common/Pcsx2Types.h>
|
||||
|
||||
namespace DebuggerEvents
|
||||
{
|
||||
struct Event
|
||||
{
|
||||
virtual ~Event() = default;
|
||||
};
|
||||
|
||||
// Sent when a debugger view is first created, and subsequently broadcast to
|
||||
// all debugger views at regular intervals.
|
||||
struct Refresh : Event
|
||||
{
|
||||
};
|
||||
|
||||
// Go to the address in a disassembly or memory view and switch to that tab.
|
||||
struct GoToAddress : Event
|
||||
{
|
||||
enum Filter
|
||||
{
|
||||
NONE,
|
||||
DISASSEMBLER,
|
||||
MEMORY_VIEW
|
||||
};
|
||||
|
||||
u32 address = 0;
|
||||
|
||||
// Prevent the memory view from handling events for jumping to functions
|
||||
// and vice versa.
|
||||
Filter filter = NONE;
|
||||
|
||||
bool switch_to_tab = true;
|
||||
|
||||
static constexpr const char* ACTION_PREFIX = QT_TRANSLATE_NOOP("DebuggerEvents", "Go to in");
|
||||
};
|
||||
|
||||
// The state of the VM has changed and views should be updated to reflect
|
||||
// the new state (e.g. the VM has been paused).
|
||||
struct VMUpdate : Event
|
||||
{
|
||||
};
|
||||
|
||||
// Add the address to the saved addresses list and switch to that tab.
|
||||
struct AddToSavedAddresses : Event
|
||||
{
|
||||
u32 address = 0;
|
||||
bool switch_to_tab = true;
|
||||
|
||||
static constexpr const char* ACTION_PREFIX = QT_TRANSLATE_NOOP("DebuggerEvents", "Add to");
|
||||
};
|
||||
} // namespace DebuggerEvents
|
||||
180
pcsx2-qt/Debugger/DebuggerSettingsManager.cpp
Normal file
180
pcsx2-qt/Debugger/DebuggerSettingsManager.cpp
Normal file
@@ -0,0 +1,180 @@
|
||||
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
|
||||
// SPDX-License-Identifier: GPL-3.0+
|
||||
|
||||
#include "DebuggerSettingsManager.h"
|
||||
|
||||
#include <QtCore/QJsonDocument>
|
||||
#include <QtCore/QJsonObject>
|
||||
#include <QtCore/QJsonArray>
|
||||
#include <QtCore/QFile>
|
||||
|
||||
#include "common/Console.h"
|
||||
#include "VMManager.h"
|
||||
|
||||
std::mutex DebuggerSettingsManager::writeLock;
|
||||
const QString DebuggerSettingsManager::settingsFileVersion = "0.01";
|
||||
|
||||
QJsonObject DebuggerSettingsManager::loadGameSettingsJSON()
|
||||
{
|
||||
std::string path = VMManager::GetDebuggerSettingsFilePathForCurrentGame();
|
||||
QFile file(QString::fromStdString(path));
|
||||
if (!file.open(QIODevice::ReadOnly))
|
||||
{
|
||||
Console.WriteLnFmt("Debugger Settings Manager: No Debugger Settings file found for game at: '{}'", path);
|
||||
return QJsonObject();
|
||||
}
|
||||
QByteArray fileContent = file.readAll();
|
||||
file.close();
|
||||
|
||||
const QJsonDocument jsonDoc(QJsonDocument::fromJson(fileContent));
|
||||
if (jsonDoc.isNull() || !jsonDoc.isObject())
|
||||
{
|
||||
Console.WriteLnFmt("Debugger Settings Manager: Failed to load contents of settings file for file at: '{}'", path);
|
||||
return QJsonObject();
|
||||
}
|
||||
|
||||
return jsonDoc.object();
|
||||
}
|
||||
|
||||
void DebuggerSettingsManager::writeJSONToPath(std::string path, QJsonDocument jsonDocument)
|
||||
{
|
||||
QFile file(QString::fromStdString(path));
|
||||
if (!file.open(QIODevice::WriteOnly))
|
||||
{
|
||||
Console.WriteLnFmt("Debugger Settings Manager: Failed to write Debugger Settings file to path: '{}'", path);
|
||||
return;
|
||||
}
|
||||
file.write(jsonDocument.toJson(QJsonDocument::Indented));
|
||||
file.close();
|
||||
}
|
||||
|
||||
void DebuggerSettingsManager::loadGameSettings(BreakpointModel* bpModel)
|
||||
{
|
||||
const std::string path = VMManager::GetDebuggerSettingsFilePathForCurrentGame();
|
||||
if (path.empty())
|
||||
return;
|
||||
|
||||
const QJsonValue breakpointsValue = loadGameSettingsJSON().value("Breakpoints");
|
||||
const QString valueToLoad = breakpointsValue.toString();
|
||||
if (breakpointsValue.isUndefined() || !breakpointsValue.isArray())
|
||||
{
|
||||
Console.WriteLnFmt("Debugger Settings Manager: Failed to read Breakpoints array from settings file: '{}'", path);
|
||||
return;
|
||||
}
|
||||
|
||||
// Breakpoint descriptions were added at debugger settings file version 0.01. If loading
|
||||
// saved breakpoints from a previous version (only 0.00 existed prior), the breakpoints will be
|
||||
// missing a description. This code will add in an empty description so that the previous
|
||||
// version, 0.00, is compatible with 0.01.
|
||||
bool isMissingDescription = false;
|
||||
const QJsonValue savedVersionValue = loadGameSettingsJSON().value("Version");
|
||||
if (!savedVersionValue.isUndefined())
|
||||
{
|
||||
isMissingDescription = savedVersionValue.toString().toStdString() == "0.00";
|
||||
}
|
||||
|
||||
const QJsonArray breakpointsArray = breakpointsValue.toArray();
|
||||
for (u32 row = 0; row < breakpointsArray.size(); row++)
|
||||
{
|
||||
const QJsonValue rowValue = breakpointsArray.at(row);
|
||||
if (rowValue.isUndefined() || !rowValue.isObject())
|
||||
{
|
||||
Console.WriteLn("Debugger Settings Manager: Failed to load invalid Breakpoint object.");
|
||||
continue;
|
||||
}
|
||||
QJsonObject rowObject = rowValue.toObject();
|
||||
|
||||
// Add empty description for saved breakpoints from debugger settings versions prior to 0.01
|
||||
if (isMissingDescription)
|
||||
{
|
||||
rowObject.insert(QString("DESCRIPTION"), QJsonValue(""));
|
||||
}
|
||||
|
||||
QStringList fields;
|
||||
u32 col = 0;
|
||||
for (auto iter = rowObject.begin(); iter != rowObject.end(); iter++, col++)
|
||||
{
|
||||
QString headerColKey = bpModel->headerData(col, Qt::Horizontal, Qt::UserRole).toString();
|
||||
fields << rowObject.value(headerColKey).toString();
|
||||
}
|
||||
bpModel->loadBreakpointFromFieldList(fields);
|
||||
}
|
||||
}
|
||||
|
||||
void DebuggerSettingsManager::loadGameSettings(SavedAddressesModel* savedAddressesModel)
|
||||
{
|
||||
const std::string path = VMManager::GetDebuggerSettingsFilePathForCurrentGame();
|
||||
if (path.empty())
|
||||
return;
|
||||
|
||||
const QJsonValue savedAddressesValue = loadGameSettingsJSON().value("SavedAddresses");
|
||||
QString valueToLoad = savedAddressesValue.toString();
|
||||
if (savedAddressesValue.isUndefined() || !savedAddressesValue.isArray())
|
||||
{
|
||||
Console.WriteLnFmt("Debugger Settings Manager: Failed to read Saved Addresses array from settings file: '{}'", path);
|
||||
return;
|
||||
}
|
||||
|
||||
const QJsonArray breakpointsArray = savedAddressesValue.toArray();
|
||||
|
||||
for (u32 row = 0; row < breakpointsArray.size(); row++)
|
||||
{
|
||||
const QJsonValue rowValue = breakpointsArray.at(row);
|
||||
if (rowValue.isUndefined() || !rowValue.isObject())
|
||||
{
|
||||
Console.WriteLn("Debugger Settings Manager: Failed to load invalid Breakpoint object.");
|
||||
continue;
|
||||
}
|
||||
const QJsonObject rowObject = rowValue.toObject();
|
||||
QStringList fields;
|
||||
u32 col = 0;
|
||||
for (auto iter = rowObject.begin(); iter != rowObject.end(); iter++, col++)
|
||||
{
|
||||
QString headerColKey = savedAddressesModel->headerData(col, Qt::Horizontal, Qt::UserRole).toString();
|
||||
fields << rowObject.value(headerColKey).toString();
|
||||
}
|
||||
savedAddressesModel->loadSavedAddressFromFieldList(fields);
|
||||
}
|
||||
}
|
||||
|
||||
void DebuggerSettingsManager::saveGameSettings(BreakpointModel* bpModel)
|
||||
{
|
||||
saveGameSettings(bpModel, "Breakpoints", BreakpointModel::ExportRole);
|
||||
}
|
||||
|
||||
void DebuggerSettingsManager::saveGameSettings(SavedAddressesModel* savedAddressesModel)
|
||||
{
|
||||
saveGameSettings(savedAddressesModel, "SavedAddresses", Qt::DisplayRole);
|
||||
}
|
||||
|
||||
void DebuggerSettingsManager::saveGameSettings(QAbstractTableModel* abstractTableModel, QString settingsKey, u32 role)
|
||||
{
|
||||
const std::string path = VMManager::GetDebuggerSettingsFilePathForCurrentGame();
|
||||
if (path.empty())
|
||||
return;
|
||||
|
||||
const std::lock_guard<std::mutex> lock(writeLock);
|
||||
QJsonObject loadedSettings = loadGameSettingsJSON();
|
||||
QJsonArray rowsArray;
|
||||
QStringList keys;
|
||||
for (int col = 0; col < abstractTableModel->columnCount(); ++col)
|
||||
{
|
||||
keys << abstractTableModel->headerData(col, Qt::Horizontal, Qt::UserRole).toString();
|
||||
}
|
||||
|
||||
for (int row = 0; row < abstractTableModel->rowCount(); row++)
|
||||
{
|
||||
QJsonObject rowObject;
|
||||
for (int col = 0; col < abstractTableModel->columnCount(); col++)
|
||||
{
|
||||
const QModelIndex index = abstractTableModel->index(row, col);
|
||||
const QString data = abstractTableModel->data(index, role).toString();
|
||||
rowObject.insert(keys[col], QJsonValue::fromVariant(data));
|
||||
}
|
||||
rowsArray.append(rowObject);
|
||||
}
|
||||
loadedSettings.insert(settingsKey, rowsArray);
|
||||
loadedSettings.insert("Version", settingsFileVersion);
|
||||
QJsonDocument doc(loadedSettings);
|
||||
writeJSONToPath(path, doc);
|
||||
}
|
||||
29
pcsx2-qt/Debugger/DebuggerSettingsManager.h
Normal file
29
pcsx2-qt/Debugger/DebuggerSettingsManager.h
Normal file
@@ -0,0 +1,29 @@
|
||||
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
|
||||
// SPDX-License-Identifier: GPL-3.0+
|
||||
|
||||
#pragma once
|
||||
#include <QtWidgets/QDialog>
|
||||
|
||||
#include <mutex>
|
||||
|
||||
#include "Breakpoints/BreakpointModel.h"
|
||||
#include "Memory/SavedAddressesModel.h"
|
||||
|
||||
class DebuggerSettingsManager final
|
||||
{
|
||||
public:
|
||||
DebuggerSettingsManager(QWidget* parent = nullptr);
|
||||
~DebuggerSettingsManager();
|
||||
|
||||
static void loadGameSettings(BreakpointModel* bpModel);
|
||||
static void loadGameSettings(SavedAddressesModel* savedAddressesModel);
|
||||
static void saveGameSettings(BreakpointModel* bpModel);
|
||||
static void saveGameSettings(SavedAddressesModel* savedAddressesModel);
|
||||
static void saveGameSettings(QAbstractTableModel* abstractTableModel, QString settingsKey, u32 role);
|
||||
|
||||
private:
|
||||
static std::mutex writeLock;
|
||||
static void writeJSONToPath(std::string path, QJsonDocument jsonDocument);
|
||||
static QJsonObject loadGameSettingsJSON();
|
||||
const static QString settingsFileVersion;
|
||||
};
|
||||
318
pcsx2-qt/Debugger/DebuggerView.cpp
Normal file
318
pcsx2-qt/Debugger/DebuggerView.cpp
Normal file
@@ -0,0 +1,318 @@
|
||||
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
|
||||
// SPDX-License-Identifier: GPL-3.0+
|
||||
|
||||
#include "DebuggerView.h"
|
||||
|
||||
#include "Debugger/DebuggerWindow.h"
|
||||
#include "Debugger/JsonValueWrapper.h"
|
||||
#include "Debugger/Docking/DockManager.h"
|
||||
#include "Debugger/Docking/DockTables.h"
|
||||
|
||||
#include "DebugTools/DebugInterface.h"
|
||||
|
||||
#include "common/Assertions.h"
|
||||
|
||||
DebuggerView::DebuggerView(const DebuggerViewParameters& parameters, u32 flags)
|
||||
: QWidget(parameters.parent)
|
||||
, m_id(parameters.id)
|
||||
, m_unique_name(parameters.unique_name)
|
||||
, m_cpu(parameters.cpu)
|
||||
, m_cpu_override(parameters.cpu_override)
|
||||
, m_flags(flags)
|
||||
{
|
||||
updateStyleSheet();
|
||||
}
|
||||
|
||||
DebugInterface& DebuggerView::cpu() const
|
||||
{
|
||||
if (m_cpu_override.has_value())
|
||||
return DebugInterface::get(*m_cpu_override);
|
||||
|
||||
pxAssertRel(m_cpu, "DebuggerView::cpu called on object with null cpu.");
|
||||
return *m_cpu;
|
||||
}
|
||||
|
||||
QString DebuggerView::uniqueName() const
|
||||
{
|
||||
return m_unique_name;
|
||||
}
|
||||
|
||||
u64 DebuggerView::id() const
|
||||
{
|
||||
return m_id;
|
||||
}
|
||||
|
||||
QString DebuggerView::displayName() const
|
||||
{
|
||||
QString name = displayNameWithoutSuffix();
|
||||
|
||||
// If there are multiple debugger views with the same name, append a number
|
||||
// to the display name.
|
||||
if (m_display_name_suffix_number.has_value())
|
||||
name = tr("%1 #%2").arg(name).arg(*m_display_name_suffix_number);
|
||||
|
||||
if (m_cpu_override)
|
||||
name = tr("%1 (%2)").arg(name).arg(DebugInterface::cpuName(*m_cpu_override));
|
||||
|
||||
return name;
|
||||
}
|
||||
|
||||
QString DebuggerView::displayNameWithoutSuffix() const
|
||||
{
|
||||
return m_translated_display_name;
|
||||
}
|
||||
|
||||
QString DebuggerView::customDisplayName() const
|
||||
{
|
||||
return m_custom_display_name;
|
||||
}
|
||||
|
||||
bool DebuggerView::setCustomDisplayName(QString display_name)
|
||||
{
|
||||
if (display_name.size() > DockUtils::MAX_DOCK_WIDGET_NAME_SIZE)
|
||||
return false;
|
||||
|
||||
m_custom_display_name = display_name;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool DebuggerView::isPrimary() const
|
||||
{
|
||||
return m_is_primary;
|
||||
}
|
||||
|
||||
void DebuggerView::setPrimary(bool is_primary)
|
||||
{
|
||||
m_is_primary = is_primary;
|
||||
}
|
||||
|
||||
bool DebuggerView::setCpu(DebugInterface& new_cpu)
|
||||
{
|
||||
BreakPointCpu before = cpu().getCpuType();
|
||||
m_cpu = &new_cpu;
|
||||
BreakPointCpu after = cpu().getCpuType();
|
||||
return before == after;
|
||||
}
|
||||
|
||||
std::optional<BreakPointCpu> DebuggerView::cpuOverride() const
|
||||
{
|
||||
return m_cpu_override;
|
||||
}
|
||||
|
||||
bool DebuggerView::setCpuOverride(std::optional<BreakPointCpu> new_cpu)
|
||||
{
|
||||
BreakPointCpu before = cpu().getCpuType();
|
||||
m_cpu_override = new_cpu;
|
||||
BreakPointCpu after = cpu().getCpuType();
|
||||
return before == after;
|
||||
}
|
||||
|
||||
bool DebuggerView::handleEvent(const DebuggerEvents::Event& event)
|
||||
{
|
||||
auto [begin, end] = m_event_handlers.equal_range(typeid(event).name());
|
||||
for (auto handler = begin; handler != end; handler++)
|
||||
if (handler->second(event))
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool DebuggerView::acceptsEventType(const char* event_type)
|
||||
{
|
||||
auto [begin, end] = m_event_handlers.equal_range(event_type);
|
||||
return begin != end;
|
||||
}
|
||||
|
||||
|
||||
void DebuggerView::toJson(JsonValueWrapper& json)
|
||||
{
|
||||
std::string custom_display_name_str = m_custom_display_name.toStdString();
|
||||
rapidjson::Value custom_display_name;
|
||||
custom_display_name.SetString(custom_display_name_str.c_str(), custom_display_name_str.size(), json.allocator());
|
||||
json.value().AddMember("customDisplayName", custom_display_name, json.allocator());
|
||||
|
||||
json.value().AddMember("isPrimary", m_is_primary, json.allocator());
|
||||
}
|
||||
|
||||
bool DebuggerView::fromJson(const JsonValueWrapper& json)
|
||||
{
|
||||
auto custom_display_name = json.value().FindMember("customDisplayName");
|
||||
if (custom_display_name != json.value().MemberEnd() && custom_display_name->value.IsString())
|
||||
{
|
||||
m_custom_display_name = QString(custom_display_name->value.GetString());
|
||||
m_custom_display_name.truncate(DockUtils::MAX_DOCK_WIDGET_NAME_SIZE);
|
||||
}
|
||||
|
||||
auto is_primary = json.value().FindMember("isPrimary");
|
||||
if (is_primary != json.value().MemberEnd() && is_primary->value.IsBool())
|
||||
m_is_primary = is_primary->value.GetBool();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void DebuggerView::switchToThisTab()
|
||||
{
|
||||
g_debugger_window->dockManager().switchToDebuggerView(this);
|
||||
}
|
||||
|
||||
bool DebuggerView::supportsMultipleInstances()
|
||||
{
|
||||
return !(m_flags & DISALLOW_MULTIPLE_INSTANCES);
|
||||
}
|
||||
|
||||
void DebuggerView::retranslateDisplayName()
|
||||
{
|
||||
if (!m_custom_display_name.isEmpty())
|
||||
{
|
||||
m_translated_display_name = m_custom_display_name;
|
||||
}
|
||||
else
|
||||
{
|
||||
auto description = DockTables::DEBUGGER_VIEWS.find(metaObject()->className());
|
||||
if (description != DockTables::DEBUGGER_VIEWS.end())
|
||||
m_translated_display_name = QCoreApplication::translate("DebuggerView", description->second.display_name);
|
||||
else
|
||||
m_translated_display_name = QString();
|
||||
}
|
||||
}
|
||||
|
||||
std::optional<int> DebuggerView::displayNameSuffixNumber() const
|
||||
{
|
||||
return m_display_name_suffix_number;
|
||||
}
|
||||
|
||||
void DebuggerView::setDisplayNameSuffixNumber(std::optional<int> suffix_number)
|
||||
{
|
||||
m_display_name_suffix_number = suffix_number;
|
||||
}
|
||||
|
||||
void DebuggerView::updateStyleSheet()
|
||||
{
|
||||
QString stylesheet;
|
||||
|
||||
if (m_flags & MONOSPACE_FONT)
|
||||
{
|
||||
// Easiest way to handle cross platform monospace fonts
|
||||
// There are issues related to TabWidget -> Children font inheritance otherwise
|
||||
#if defined(WIN32)
|
||||
stylesheet += QStringLiteral("font-family: 'Lucida Console';");
|
||||
#elif defined(__APPLE__)
|
||||
stylesheet += QStringLiteral("font-family: 'Monaco';");
|
||||
#else
|
||||
stylesheet += QStringLiteral("font-family: 'Monospace';");
|
||||
#endif
|
||||
}
|
||||
|
||||
// HACK: Make the font size smaller without applying a stylesheet to the
|
||||
// whole window (which would impact performance).
|
||||
if (g_debugger_window)
|
||||
stylesheet += QString("font-size: %1pt;").arg(g_debugger_window->fontSize());
|
||||
|
||||
setStyleSheet(stylesheet);
|
||||
}
|
||||
|
||||
void DebuggerView::goToInDisassembler(u32 address, bool switch_to_tab)
|
||||
{
|
||||
DebuggerEvents::GoToAddress event;
|
||||
event.address = address;
|
||||
event.filter = DebuggerEvents::GoToAddress::DISASSEMBLER;
|
||||
event.switch_to_tab = switch_to_tab;
|
||||
DebuggerView::sendEvent(std::move(event));
|
||||
}
|
||||
|
||||
void DebuggerView::goToInMemoryView(u32 address, bool switch_to_tab)
|
||||
{
|
||||
DebuggerEvents::GoToAddress event;
|
||||
event.address = address;
|
||||
event.filter = DebuggerEvents::GoToAddress::MEMORY_VIEW;
|
||||
event.switch_to_tab = switch_to_tab;
|
||||
DebuggerView::sendEvent(std::move(event));
|
||||
}
|
||||
|
||||
void DebuggerView::sendEventImplementation(const DebuggerEvents::Event& event)
|
||||
{
|
||||
if (!g_debugger_window)
|
||||
return;
|
||||
|
||||
for (const auto& [unique_name, widget] : g_debugger_window->dockManager().debuggerViews())
|
||||
if (widget->isPrimary() && widget->handleEvent(event))
|
||||
return;
|
||||
|
||||
for (const auto& [unique_name, widget] : g_debugger_window->dockManager().debuggerViews())
|
||||
if (!widget->isPrimary() && widget->handleEvent(event))
|
||||
return;
|
||||
}
|
||||
|
||||
void DebuggerView::broadcastEventImplementation(const DebuggerEvents::Event& event)
|
||||
{
|
||||
if (!g_debugger_window)
|
||||
return;
|
||||
|
||||
for (const auto& [unique_name, widget] : g_debugger_window->dockManager().debuggerViews())
|
||||
widget->handleEvent(event);
|
||||
}
|
||||
|
||||
std::vector<QAction*> DebuggerView::createEventActionsImplementation(
|
||||
QMenu* menu,
|
||||
u32 max_top_level_actions,
|
||||
bool skip_self,
|
||||
const char* event_type,
|
||||
const char* action_prefix,
|
||||
std::function<const DebuggerEvents::Event*()> event_func)
|
||||
{
|
||||
if (!g_debugger_window)
|
||||
return {};
|
||||
|
||||
std::vector<DebuggerView*> receivers;
|
||||
for (const auto& [unique_name, widget] : g_debugger_window->dockManager().debuggerViews())
|
||||
if ((!skip_self || widget != this) && widget->acceptsEventType(event_type))
|
||||
receivers.emplace_back(widget);
|
||||
|
||||
std::sort(receivers.begin(), receivers.end(), [&](const DebuggerView* lhs, const DebuggerView* rhs) {
|
||||
if (lhs->displayNameWithoutSuffix() == rhs->displayNameWithoutSuffix())
|
||||
return lhs->displayNameSuffixNumber() < rhs->displayNameSuffixNumber();
|
||||
|
||||
return lhs->displayNameWithoutSuffix() < rhs->displayNameWithoutSuffix();
|
||||
});
|
||||
|
||||
QMenu* submenu = nullptr;
|
||||
if (receivers.size() > max_top_level_actions)
|
||||
{
|
||||
QString title_format = QCoreApplication::translate("DebuggerEvent", "%1...");
|
||||
submenu = new QMenu(title_format.arg(QCoreApplication::translate("DebuggerEvent", action_prefix)), menu);
|
||||
}
|
||||
|
||||
std::vector<QAction*> actions;
|
||||
for (size_t i = 0; i < receivers.size(); i++)
|
||||
{
|
||||
DebuggerView* receiver = receivers[i];
|
||||
|
||||
QAction* action;
|
||||
if (!submenu || i + 1 < max_top_level_actions)
|
||||
{
|
||||
QString title_format = QCoreApplication::translate("DebuggerEvent", "%1 %2");
|
||||
QString event_title = QCoreApplication::translate("DebuggerEvent", action_prefix);
|
||||
QString title = title_format.arg(event_title).arg(receiver->displayName());
|
||||
action = new QAction(title, menu);
|
||||
menu->addAction(action);
|
||||
}
|
||||
else
|
||||
{
|
||||
action = new QAction(receiver->displayName(), submenu);
|
||||
submenu->addAction(action);
|
||||
}
|
||||
|
||||
connect(action, &QAction::triggered, receiver, [receiver, event_func]() {
|
||||
const DebuggerEvents::Event* event = event_func();
|
||||
if (event)
|
||||
receiver->handleEvent(*event);
|
||||
});
|
||||
|
||||
actions.emplace_back(action);
|
||||
}
|
||||
|
||||
if (submenu)
|
||||
menu->addMenu(submenu);
|
||||
|
||||
return actions;
|
||||
}
|
||||
207
pcsx2-qt/Debugger/DebuggerView.h
Normal file
207
pcsx2-qt/Debugger/DebuggerView.h
Normal file
@@ -0,0 +1,207 @@
|
||||
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
|
||||
// SPDX-License-Identifier: GPL-3.0+
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "QtHost.h"
|
||||
#include "Debugger/DebuggerEvents.h"
|
||||
|
||||
#include "DebugTools/DebugInterface.h"
|
||||
|
||||
#include <QtWidgets/QWidget>
|
||||
#include <QtWidgets/QMenu>
|
||||
|
||||
class JsonValueWrapper;
|
||||
|
||||
// Container for variables to be passed to the constructor of DebuggerView.
|
||||
struct DebuggerViewParameters
|
||||
{
|
||||
QString unique_name;
|
||||
u64 id = 0;
|
||||
DebugInterface* cpu = nullptr;
|
||||
std::optional<BreakPointCpu> cpu_override;
|
||||
QWidget* parent = nullptr;
|
||||
};
|
||||
|
||||
// The base class for the contents of the dock widgets in the debugger.
|
||||
class DebuggerView : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
QString uniqueName() const;
|
||||
u64 id() const;
|
||||
|
||||
// Get the translated name that should be displayed for this view.
|
||||
QString displayName() const;
|
||||
QString displayNameWithoutSuffix() const;
|
||||
|
||||
QString customDisplayName() const;
|
||||
bool setCustomDisplayName(QString display_name);
|
||||
|
||||
bool isPrimary() const;
|
||||
void setPrimary(bool is_primary);
|
||||
|
||||
// Get the effective debug interface associated with this particular view
|
||||
// if it's set, otherwise return the one associated with the layout that
|
||||
// contains this view.
|
||||
DebugInterface& cpu() const;
|
||||
|
||||
// Set the debug interface associated with the layout. If false is returned,
|
||||
// we have to recreate the object.
|
||||
bool setCpu(DebugInterface& new_cpu);
|
||||
|
||||
// Get the CPU associated with this particular view.
|
||||
std::optional<BreakPointCpu> cpuOverride() const;
|
||||
|
||||
// Set the CPU associated with the individual dock widget. If false is
|
||||
// returned, we have to recreate the object.
|
||||
bool setCpuOverride(std::optional<BreakPointCpu> new_cpu);
|
||||
|
||||
// Send each open debugger view an event in turn, until one handles it.
|
||||
template <typename Event>
|
||||
static void sendEvent(Event event)
|
||||
{
|
||||
if (!QtHost::IsOnUIThread())
|
||||
{
|
||||
QtHost::RunOnUIThread([event = std::move(event)]() {
|
||||
DebuggerView::sendEventImplementation(event);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
sendEventImplementation(event);
|
||||
}
|
||||
|
||||
// Send all open debugger views an event.
|
||||
template <typename Event>
|
||||
static void broadcastEvent(Event event)
|
||||
{
|
||||
if (!QtHost::IsOnUIThread())
|
||||
{
|
||||
QtHost::RunOnUIThread([event = std::move(event)]() {
|
||||
DebuggerView::broadcastEventImplementation(event);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
broadcastEventImplementation(event);
|
||||
}
|
||||
|
||||
// Register a handler callback for the specified type of event.
|
||||
template <typename Event>
|
||||
void receiveEvent(std::function<bool(const Event&)> callback)
|
||||
{
|
||||
m_event_handlers.emplace(
|
||||
typeid(Event).name(),
|
||||
[callback](const DebuggerEvents::Event& event) -> bool {
|
||||
return callback(static_cast<const Event&>(event));
|
||||
});
|
||||
}
|
||||
|
||||
// Register a handler member function for the specified type of event.
|
||||
template <typename Event, typename SubClass>
|
||||
void receiveEvent(bool (SubClass::*function)(const Event& event))
|
||||
{
|
||||
m_event_handlers.emplace(
|
||||
typeid(Event).name(),
|
||||
[this, function](const DebuggerEvents::Event& event) -> bool {
|
||||
return (*static_cast<SubClass*>(this).*function)(static_cast<const Event&>(event));
|
||||
});
|
||||
}
|
||||
|
||||
// Call the handler callback for the specified event.
|
||||
bool handleEvent(const DebuggerEvents::Event& event);
|
||||
|
||||
// Check if this debugger view can receive the specified type of event.
|
||||
bool acceptsEventType(const char* event_type);
|
||||
|
||||
// Generates context menu actions to send an event to each debugger view
|
||||
// that can receive it. A submenu is generated if the number of possible
|
||||
// receivers exceeds max_top_level_actions. If skip_self is true, actions
|
||||
// are only generated if the sender and receiver aren't the same object.
|
||||
template <typename Event>
|
||||
std::vector<QAction*> createEventActions(
|
||||
QMenu* menu,
|
||||
std::function<std::optional<Event>()> event_func,
|
||||
bool skip_self = true,
|
||||
u32 max_top_level_actions = 5)
|
||||
{
|
||||
return createEventActionsImplementation(
|
||||
menu, max_top_level_actions, skip_self, typeid(Event).name(), Event::ACTION_PREFIX,
|
||||
[event_func]() -> DebuggerEvents::Event* {
|
||||
static std::optional<Event> event;
|
||||
event = event_func();
|
||||
if (!event.has_value())
|
||||
return nullptr;
|
||||
|
||||
return static_cast<DebuggerEvents::Event*>(&(*event));
|
||||
});
|
||||
}
|
||||
|
||||
virtual void toJson(JsonValueWrapper& json);
|
||||
virtual bool fromJson(const JsonValueWrapper& json);
|
||||
|
||||
void switchToThisTab();
|
||||
|
||||
bool supportsMultipleInstances();
|
||||
|
||||
void retranslateDisplayName();
|
||||
|
||||
std::optional<int> displayNameSuffixNumber() const;
|
||||
void setDisplayNameSuffixNumber(std::optional<int> suffix_number);
|
||||
|
||||
void updateStyleSheet();
|
||||
|
||||
static void goToInDisassembler(u32 address, bool switch_to_tab);
|
||||
static void goToInMemoryView(u32 address, bool switch_to_tab);
|
||||
|
||||
protected:
|
||||
enum Flags
|
||||
{
|
||||
NO_DEBUGGER_FLAGS = 0,
|
||||
// Prevent the user from opening multiple dock widgets of this type.
|
||||
DISALLOW_MULTIPLE_INSTANCES = 1 << 0,
|
||||
// Apply a stylesheet that gives all the text a monospace font.
|
||||
MONOSPACE_FONT = 1 << 1
|
||||
};
|
||||
|
||||
DebuggerView(const DebuggerViewParameters& parameters, u32 flags);
|
||||
|
||||
private:
|
||||
static void sendEventImplementation(const DebuggerEvents::Event& event);
|
||||
static void broadcastEventImplementation(const DebuggerEvents::Event& event);
|
||||
|
||||
std::vector<QAction*> createEventActionsImplementation(
|
||||
QMenu* menu,
|
||||
u32 max_top_level_actions,
|
||||
bool skip_self,
|
||||
const char* event_type,
|
||||
const char* action_prefix,
|
||||
std::function<const DebuggerEvents::Event*()> event_func);
|
||||
|
||||
// Used for sorting debugger views that have the same display name. Unique
|
||||
// within a single layout.
|
||||
u64 m_id;
|
||||
|
||||
// Identifier for the dock widget used by KDDockWidgets. Unique within a
|
||||
// single layout.
|
||||
QString m_unique_name;
|
||||
|
||||
// A user-defined name, or an empty string if no name was specified so that
|
||||
// the default names can be retranslated on the fly.
|
||||
QString m_custom_display_name;
|
||||
|
||||
QString m_translated_display_name;
|
||||
std::optional<int> m_display_name_suffix_number;
|
||||
|
||||
// Primary debugger views will be chosen to handle events first. For
|
||||
// example, clicking on an address to go to it in the primary memory view.
|
||||
bool m_is_primary = false;
|
||||
|
||||
DebugInterface* m_cpu;
|
||||
std::optional<BreakPointCpu> m_cpu_override;
|
||||
u32 m_flags;
|
||||
|
||||
std::multimap<std::string, std::function<bool(const DebuggerEvents::Event&)>> m_event_handlers;
|
||||
};
|
||||
562
pcsx2-qt/Debugger/DebuggerWindow.cpp
Normal file
562
pcsx2-qt/Debugger/DebuggerWindow.cpp
Normal file
@@ -0,0 +1,562 @@
|
||||
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
|
||||
// SPDX-License-Identifier: GPL-3.0+
|
||||
|
||||
#include "DebuggerWindow.h"
|
||||
|
||||
#include "Debugger/DebuggerView.h"
|
||||
#include "Debugger/Docking/DockManager.h"
|
||||
|
||||
#include "DebugTools/DebugInterface.h"
|
||||
#include "DebugTools/Breakpoints.h"
|
||||
#include "DebugTools/MIPSAnalyst.h"
|
||||
#include "DebugTools/MipsStackWalk.h"
|
||||
#include "DebugTools/SymbolImporter.h"
|
||||
#include "QtHost.h"
|
||||
#include "MainWindow.h"
|
||||
#include "AnalysisOptionsDialog.h"
|
||||
|
||||
#include <QtWidgets/QMessageBox>
|
||||
|
||||
DebuggerWindow* g_debugger_window = nullptr;
|
||||
|
||||
DebuggerWindow::DebuggerWindow(QWidget* parent)
|
||||
: KDDockWidgets::QtWidgets::MainWindow(QStringLiteral("DebuggerWindow"), {}, parent)
|
||||
, m_dock_manager(new DockManager(this))
|
||||
{
|
||||
m_ui.setupUi(this);
|
||||
|
||||
g_debugger_window = this;
|
||||
|
||||
setupDefaultToolBarState();
|
||||
setupFonts();
|
||||
restoreWindowGeometry();
|
||||
|
||||
m_dock_manager->loadLayouts();
|
||||
|
||||
connect(m_ui.actionAnalyse, &QAction::triggered, this, &DebuggerWindow::onAnalyse);
|
||||
connect(m_ui.actionSettings, &QAction::triggered, this, &DebuggerWindow::onSettings);
|
||||
connect(m_ui.actionGameSettings, &QAction::triggered, this, &DebuggerWindow::onGameSettings);
|
||||
connect(m_ui.actionClose, &QAction::triggered, this, &DebuggerWindow::close);
|
||||
|
||||
connect(m_ui.actionOnTop, &QAction::triggered, this, [this](bool checked) {
|
||||
if (checked)
|
||||
setWindowFlags(windowFlags() | Qt::WindowStaysOnTopHint);
|
||||
else
|
||||
setWindowFlags(windowFlags() & ~Qt::WindowStaysOnTopHint);
|
||||
show();
|
||||
});
|
||||
|
||||
connect(m_ui.actionRun, &QAction::triggered, this, &DebuggerWindow::onRunPause);
|
||||
connect(m_ui.actionStepInto, &QAction::triggered, this, &DebuggerWindow::onStepInto);
|
||||
connect(m_ui.actionStepOver, &QAction::triggered, this, &DebuggerWindow::onStepOver);
|
||||
connect(m_ui.actionStepOut, &QAction::triggered, this, &DebuggerWindow::onStepOut);
|
||||
|
||||
connect(m_ui.actionShutDown, &QAction::triggered, [this]() {
|
||||
if (currentCPU() && currentCPU()->isAlive())
|
||||
g_emu_thread->shutdownVM(false);
|
||||
});
|
||||
|
||||
connect(m_ui.actionReset, &QAction::triggered, [this]() {
|
||||
if (currentCPU() && currentCPU()->isAlive())
|
||||
g_emu_thread->resetVM();
|
||||
});
|
||||
|
||||
connect(m_ui.menuTools, &QMenu::aboutToShow, this, [this]() {
|
||||
m_dock_manager->createToolsMenu(m_ui.menuTools);
|
||||
});
|
||||
|
||||
connect(m_ui.menuWindows, &QMenu::aboutToShow, this, [this]() {
|
||||
m_dock_manager->createWindowsMenu(m_ui.menuWindows);
|
||||
});
|
||||
|
||||
connect(m_ui.actionResetAllLayouts, &QAction::triggered, this, [this]() {
|
||||
QString text = tr("Are you sure you want to reset all layouts?");
|
||||
if (QMessageBox::question(g_debugger_window, tr("Confirmation"), text) != QMessageBox::Yes)
|
||||
return;
|
||||
|
||||
m_dock_manager->resetAllLayouts();
|
||||
});
|
||||
|
||||
connect(m_ui.actionResetDefaultLayouts, &QAction::triggered, this, [this]() {
|
||||
QString text = tr("Are you sure you want to reset the default layouts?");
|
||||
if (QMessageBox::question(g_debugger_window, tr("Confirmation"), text) != QMessageBox::Yes)
|
||||
return;
|
||||
|
||||
m_dock_manager->resetDefaultLayouts();
|
||||
});
|
||||
|
||||
connect(g_emu_thread, &EmuThread::onVMPaused, this, []() {
|
||||
DebuggerView::broadcastEvent(DebuggerEvents::VMUpdate());
|
||||
});
|
||||
|
||||
connect(g_emu_thread, &EmuThread::onVMStarting, this, &DebuggerWindow::onVMStarting);
|
||||
connect(g_emu_thread, &EmuThread::onVMPaused, this, &DebuggerWindow::onVMPaused);
|
||||
connect(g_emu_thread, &EmuThread::onVMResumed, this, &DebuggerWindow::onVMResumed);
|
||||
connect(g_emu_thread, &EmuThread::onVMStopped, this, &DebuggerWindow::onVMStopped);
|
||||
|
||||
if (QtHost::IsVMValid())
|
||||
{
|
||||
onVMStarting();
|
||||
|
||||
if (QtHost::IsVMPaused())
|
||||
onVMPaused();
|
||||
else
|
||||
onVMResumed();
|
||||
}
|
||||
else
|
||||
{
|
||||
onVMStopped();
|
||||
}
|
||||
|
||||
m_dock_manager->switchToLayout(0);
|
||||
|
||||
QMenuBar* menu_bar = menuBar();
|
||||
|
||||
setMenuWidget(m_dock_manager->createMenuBar(menu_bar));
|
||||
|
||||
updateTheme();
|
||||
|
||||
Host::RunOnCPUThread([]() {
|
||||
R5900SymbolImporter.OnDebuggerOpened();
|
||||
});
|
||||
|
||||
updateFromSettings();
|
||||
}
|
||||
|
||||
DebuggerWindow* DebuggerWindow::getInstance()
|
||||
{
|
||||
if (!g_debugger_window)
|
||||
createInstance();
|
||||
|
||||
return g_debugger_window;
|
||||
}
|
||||
|
||||
DebuggerWindow* DebuggerWindow::createInstance()
|
||||
{
|
||||
// Setup KDDockWidgets.
|
||||
DockManager::configureDockingSystem();
|
||||
|
||||
if (g_debugger_window)
|
||||
destroyInstance();
|
||||
|
||||
return new DebuggerWindow(nullptr);
|
||||
}
|
||||
|
||||
void DebuggerWindow::destroyInstance()
|
||||
{
|
||||
if (g_debugger_window)
|
||||
g_debugger_window->close();
|
||||
}
|
||||
|
||||
bool DebuggerWindow::shouldShowOnStartup()
|
||||
{
|
||||
return Host::GetBaseBoolSettingValue("Debugger/UserInterface", "ShowOnStartup", false);
|
||||
}
|
||||
|
||||
DockManager& DebuggerWindow::dockManager()
|
||||
{
|
||||
return *m_dock_manager;
|
||||
}
|
||||
|
||||
void DebuggerWindow::setupDefaultToolBarState()
|
||||
{
|
||||
// Hiding all the toolbars lets us save the default state of the window with
|
||||
// all the toolbars hidden. The DockManager will show the appropriate ones
|
||||
// later anyway.
|
||||
for (QToolBar* toolbar : findChildren<QToolBar*>())
|
||||
toolbar->hide();
|
||||
|
||||
m_default_toolbar_state = saveState();
|
||||
|
||||
for (QToolBar* toolbar : findChildren<QToolBar*>())
|
||||
connect(toolbar, &QToolBar::topLevelChanged, m_dock_manager, &DockManager::updateToolBarLockState);
|
||||
}
|
||||
|
||||
void DebuggerWindow::clearToolBarState()
|
||||
{
|
||||
restoreState(m_default_toolbar_state);
|
||||
}
|
||||
|
||||
void DebuggerWindow::setupFonts()
|
||||
{
|
||||
m_font_size = Host::GetBaseIntSettingValue("Debugger/UserInterface", "FontSize", DEFAULT_FONT_SIZE);
|
||||
if (m_font_size < MINIMUM_FONT_SIZE || m_font_size > MAXIMUM_FONT_SIZE)
|
||||
m_font_size = DEFAULT_FONT_SIZE;
|
||||
|
||||
m_ui.actionIncreaseFontSize->setShortcuts(QKeySequence::ZoomIn);
|
||||
connect(m_ui.actionIncreaseFontSize, &QAction::triggered, this, [this]() {
|
||||
if (m_font_size >= MAXIMUM_FONT_SIZE)
|
||||
return;
|
||||
|
||||
m_font_size++;
|
||||
|
||||
updateFontActions();
|
||||
updateTheme();
|
||||
saveFontSize();
|
||||
});
|
||||
|
||||
m_ui.actionDecreaseFontSize->setShortcut(QKeySequence::ZoomOut);
|
||||
connect(m_ui.actionDecreaseFontSize, &QAction::triggered, this, [this]() {
|
||||
if (m_font_size <= MINIMUM_FONT_SIZE)
|
||||
return;
|
||||
|
||||
m_font_size--;
|
||||
|
||||
updateFontActions();
|
||||
updateTheme();
|
||||
saveFontSize();
|
||||
});
|
||||
|
||||
connect(m_ui.actionResetFontSize, &QAction::triggered, this, [this]() {
|
||||
m_font_size = DEFAULT_FONT_SIZE;
|
||||
|
||||
updateFontActions();
|
||||
updateTheme();
|
||||
saveFontSize();
|
||||
});
|
||||
|
||||
updateFontActions();
|
||||
}
|
||||
|
||||
void DebuggerWindow::updateFontActions()
|
||||
{
|
||||
m_ui.actionIncreaseFontSize->setEnabled(m_font_size < MAXIMUM_FONT_SIZE);
|
||||
m_ui.actionDecreaseFontSize->setEnabled(m_font_size > MINIMUM_FONT_SIZE);
|
||||
m_ui.actionResetFontSize->setEnabled(m_font_size != DEFAULT_FONT_SIZE);
|
||||
}
|
||||
|
||||
void DebuggerWindow::saveFontSize()
|
||||
{
|
||||
Host::SetBaseIntSettingValue("Debugger/UserInterface", "FontSize", m_font_size);
|
||||
Host::CommitBaseSettingChanges();
|
||||
}
|
||||
|
||||
int DebuggerWindow::fontSize()
|
||||
{
|
||||
return m_font_size;
|
||||
}
|
||||
|
||||
void DebuggerWindow::updateTheme()
|
||||
{
|
||||
// TODO: Migrate away from stylesheets to improve performance.
|
||||
if (m_font_size != DEFAULT_FONT_SIZE)
|
||||
{
|
||||
int size = m_font_size + QApplication::font().pointSize() - DEFAULT_FONT_SIZE;
|
||||
setStyleSheet(QString("* { font-size: %1pt; } QTabBar { font-size: %2pt; }").arg(size).arg(size + 1));
|
||||
}
|
||||
else
|
||||
{
|
||||
setStyleSheet(QString());
|
||||
}
|
||||
|
||||
dockManager().updateTheme();
|
||||
}
|
||||
|
||||
void DebuggerWindow::saveWindowGeometry()
|
||||
{
|
||||
std::string old_geometry = Host::GetBaseStringSettingValue("Debugger/UserInterface", "WindowGeometry");
|
||||
|
||||
std::string geometry;
|
||||
if (shouldSaveWindowGeometry())
|
||||
geometry = saveGeometry().toBase64().toStdString();
|
||||
|
||||
if (geometry != old_geometry)
|
||||
{
|
||||
Host::SetBaseStringSettingValue("Debugger/UserInterface", "WindowGeometry", geometry.c_str());
|
||||
Host::CommitBaseSettingChanges();
|
||||
}
|
||||
}
|
||||
|
||||
void DebuggerWindow::restoreWindowGeometry()
|
||||
{
|
||||
if (!shouldSaveWindowGeometry())
|
||||
return;
|
||||
|
||||
std::string geometry = Host::GetBaseStringSettingValue("Debugger/UserInterface", "WindowGeometry");
|
||||
restoreGeometry(QByteArray::fromBase64(QByteArray::fromStdString(geometry)));
|
||||
}
|
||||
|
||||
bool DebuggerWindow::shouldSaveWindowGeometry()
|
||||
{
|
||||
return Host::GetBaseBoolSettingValue("Debugger/UserInterface", "SaveWindowGeometry", true);
|
||||
}
|
||||
|
||||
void DebuggerWindow::updateFromSettings()
|
||||
{
|
||||
const int refresh_interval = Host::GetBaseIntSettingValue("Debugger/UserInterface", "RefreshInterval", 1000);
|
||||
const int effective_refresh_interval = std::clamp(refresh_interval, 10, 100000);
|
||||
|
||||
if (!m_refresh_timer)
|
||||
{
|
||||
m_refresh_timer = new QTimer(this);
|
||||
connect(m_refresh_timer, &QTimer::timeout, this, []() {
|
||||
DebuggerView::broadcastEvent(DebuggerEvents::Refresh());
|
||||
});
|
||||
m_refresh_timer->start(effective_refresh_interval);
|
||||
}
|
||||
else
|
||||
{
|
||||
m_refresh_timer->setInterval(effective_refresh_interval);
|
||||
}
|
||||
}
|
||||
|
||||
void DebuggerWindow::onVMStarting()
|
||||
{
|
||||
m_ui.actionRun->setEnabled(true);
|
||||
m_ui.actionStepInto->setEnabled(true);
|
||||
m_ui.actionStepOver->setEnabled(true);
|
||||
m_ui.actionStepOut->setEnabled(true);
|
||||
|
||||
m_ui.actionAnalyse->setEnabled(true);
|
||||
m_ui.actionGameSettings->setEnabled(true);
|
||||
|
||||
m_ui.actionShutDown->setEnabled(true);
|
||||
m_ui.actionReset->setEnabled(true);
|
||||
}
|
||||
|
||||
void DebuggerWindow::onVMPaused()
|
||||
{
|
||||
m_ui.actionRun->setText(tr("Run"));
|
||||
m_ui.actionRun->setIcon(QIcon::fromTheme(QStringLiteral("play-line")));
|
||||
m_ui.actionStepInto->setEnabled(true);
|
||||
m_ui.actionStepOver->setEnabled(true);
|
||||
m_ui.actionStepOut->setEnabled(true);
|
||||
|
||||
if (CBreakPoints::GetBreakpointTriggered())
|
||||
{
|
||||
// Select a layout tab corresponding to the CPU that triggered the
|
||||
// breakpoint and make it start blinking unless said breakpoint was
|
||||
// generated as a result of stepping.
|
||||
const BreakPointCpu cpu_type = CBreakPoints::GetBreakpointTriggeredCpu();
|
||||
if (cpu_type == BREAKPOINT_EE || cpu_type == BREAKPOINT_IOP)
|
||||
{
|
||||
DebugInterface& cpu = DebugInterface::get(cpu_type);
|
||||
bool blink_tab = !CBreakPoints::IsSteppingBreakPoint(cpu_type, cpu.getPC());
|
||||
m_dock_manager->switchToLayoutWithCPU(cpu_type, blink_tab);
|
||||
}
|
||||
|
||||
Host::RunOnCPUThread([] {
|
||||
CBreakPoints::ClearTemporaryBreakPoints();
|
||||
CBreakPoints::SetBreakpointTriggered(false, BREAKPOINT_IOP_AND_EE);
|
||||
|
||||
// Our current PC is on a breakpoint.
|
||||
// When we run the core again, we want to skip this breakpoint and run.
|
||||
CBreakPoints::SetSkipFirst(BREAKPOINT_EE, r5900Debug.getPC());
|
||||
CBreakPoints::SetSkipFirst(BREAKPOINT_IOP, r3000Debug.getPC());
|
||||
});
|
||||
}
|
||||
|
||||
// Stops us from telling the disassembly view to jump somwhere because
|
||||
// breakpoint code paused the core.
|
||||
if (!CBreakPoints::GetCorePaused())
|
||||
emit onVMActuallyPaused();
|
||||
else
|
||||
CBreakPoints::SetCorePaused(false);
|
||||
}
|
||||
|
||||
void DebuggerWindow::onVMResumed()
|
||||
{
|
||||
m_ui.actionRun->setText(tr("Pause"));
|
||||
m_ui.actionRun->setIcon(QIcon::fromTheme(QStringLiteral("pause-line")));
|
||||
m_ui.actionStepInto->setEnabled(false);
|
||||
m_ui.actionStepOver->setEnabled(false);
|
||||
m_ui.actionStepOut->setEnabled(false);
|
||||
}
|
||||
|
||||
void DebuggerWindow::onVMStopped()
|
||||
{
|
||||
m_ui.actionRun->setEnabled(false);
|
||||
m_ui.actionStepInto->setEnabled(false);
|
||||
m_ui.actionStepOver->setEnabled(false);
|
||||
m_ui.actionStepOut->setEnabled(false);
|
||||
|
||||
m_ui.actionAnalyse->setEnabled(false);
|
||||
m_ui.actionGameSettings->setEnabled(false);
|
||||
|
||||
m_ui.actionShutDown->setEnabled(false);
|
||||
m_ui.actionReset->setEnabled(false);
|
||||
}
|
||||
|
||||
void DebuggerWindow::onAnalyse()
|
||||
{
|
||||
AnalysisOptionsDialog* dialog = new AnalysisOptionsDialog(this);
|
||||
dialog->setAttribute(Qt::WA_DeleteOnClose);
|
||||
dialog->show();
|
||||
}
|
||||
|
||||
void DebuggerWindow::onSettings()
|
||||
{
|
||||
g_main_window->doSettings("Debug");
|
||||
}
|
||||
|
||||
void DebuggerWindow::onGameSettings()
|
||||
{
|
||||
g_main_window->doGameSettings("Debug");
|
||||
}
|
||||
|
||||
void DebuggerWindow::onRunPause()
|
||||
{
|
||||
g_emu_thread->setVMPaused(!QtHost::IsVMPaused());
|
||||
}
|
||||
|
||||
void DebuggerWindow::onStepInto()
|
||||
{
|
||||
DebugInterface* cpu = currentCPU();
|
||||
if (!cpu)
|
||||
return;
|
||||
|
||||
if (!cpu->isAlive() || !cpu->isCpuPaused())
|
||||
return;
|
||||
|
||||
// Allow the cpu to skip this pc if it is a breakpoint
|
||||
CBreakPoints::SetSkipFirst(cpu->getCpuType(), cpu->getPC());
|
||||
|
||||
const u32 pc = cpu->getPC();
|
||||
const MIPSAnalyst::MipsOpcodeInfo info = MIPSAnalyst::GetOpcodeInfo(cpu, pc);
|
||||
|
||||
u32 bpAddr = pc + 0x4; // Default to the next instruction
|
||||
|
||||
if (info.isBranch)
|
||||
{
|
||||
if (!info.isConditional)
|
||||
{
|
||||
bpAddr = info.branchTarget;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (info.conditionMet)
|
||||
{
|
||||
bpAddr = info.branchTarget;
|
||||
}
|
||||
else
|
||||
{
|
||||
bpAddr = pc + (2 * 4); // Skip branch delay slot
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (info.isSyscall)
|
||||
bpAddr = info.branchTarget; // Syscalls are always taken
|
||||
|
||||
Host::RunOnCPUThread([cpu, bpAddr] {
|
||||
CBreakPoints::AddBreakPoint(cpu->getCpuType(), bpAddr, true, true, true);
|
||||
cpu->resumeCpu();
|
||||
});
|
||||
|
||||
repaint();
|
||||
}
|
||||
|
||||
void DebuggerWindow::onStepOver()
|
||||
{
|
||||
DebugInterface* cpu = currentCPU();
|
||||
if (!cpu)
|
||||
return;
|
||||
|
||||
if (!cpu->isAlive() || !cpu->isCpuPaused())
|
||||
return;
|
||||
|
||||
const u32 pc = cpu->getPC();
|
||||
const MIPSAnalyst::MipsOpcodeInfo info = MIPSAnalyst::GetOpcodeInfo(cpu, pc);
|
||||
|
||||
u32 bpAddr = pc + 0x4; // Default to the next instruction
|
||||
|
||||
if (info.isBranch)
|
||||
{
|
||||
if (!info.isConditional)
|
||||
{
|
||||
if (info.isLinkedBranch) // jal, jalr
|
||||
{
|
||||
// it's a function call with a delay slot - skip that too
|
||||
bpAddr += 4;
|
||||
}
|
||||
else // j, ...
|
||||
{
|
||||
// in case of absolute branches, set the breakpoint at the branch target
|
||||
bpAddr = info.branchTarget;
|
||||
}
|
||||
}
|
||||
else // beq, ...
|
||||
{
|
||||
if (info.conditionMet)
|
||||
{
|
||||
bpAddr = info.branchTarget;
|
||||
}
|
||||
else
|
||||
{
|
||||
bpAddr = pc + (2 * 4); // Skip branch delay slot
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Host::RunOnCPUThread([cpu, bpAddr] {
|
||||
CBreakPoints::AddBreakPoint(cpu->getCpuType(), bpAddr, true, true, true);
|
||||
cpu->resumeCpu();
|
||||
});
|
||||
|
||||
this->repaint();
|
||||
}
|
||||
|
||||
void DebuggerWindow::onStepOut()
|
||||
{
|
||||
DebugInterface* cpu = currentCPU();
|
||||
if (!cpu)
|
||||
return;
|
||||
|
||||
if (!cpu->isAlive() || !cpu->isCpuPaused())
|
||||
return;
|
||||
|
||||
// Allow the cpu to skip this pc if it is a breakpoint
|
||||
CBreakPoints::SetSkipFirst(cpu->getCpuType(), cpu->getPC());
|
||||
|
||||
std::vector<MipsStackWalk::StackFrame> stack_frames;
|
||||
for (const auto& thread : cpu->GetThreadList())
|
||||
{
|
||||
if (thread->Status() == ThreadStatus::THS_RUN)
|
||||
{
|
||||
stack_frames = MipsStackWalk::Walk(
|
||||
cpu,
|
||||
cpu->getPC(),
|
||||
cpu->getRegister(0, 31),
|
||||
cpu->getRegister(0, 29),
|
||||
thread->EntryPoint(),
|
||||
thread->StackTop());
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (stack_frames.size() < 2)
|
||||
return;
|
||||
|
||||
u32 breakpoint_pc = stack_frames.at(1).pc;
|
||||
|
||||
Host::RunOnCPUThread([cpu, breakpoint_pc] {
|
||||
CBreakPoints::AddBreakPoint(cpu->getCpuType(), breakpoint_pc, true, true, true);
|
||||
cpu->resumeCpu();
|
||||
});
|
||||
|
||||
this->repaint();
|
||||
}
|
||||
|
||||
void DebuggerWindow::closeEvent(QCloseEvent* event)
|
||||
{
|
||||
dockManager().saveCurrentLayout();
|
||||
saveWindowGeometry();
|
||||
|
||||
Host::RunOnCPUThread([]() {
|
||||
R5900SymbolImporter.OnDebuggerClosed();
|
||||
});
|
||||
|
||||
KDDockWidgets::QtWidgets::MainWindow::closeEvent(event);
|
||||
|
||||
g_debugger_window = nullptr;
|
||||
deleteLater();
|
||||
}
|
||||
|
||||
DebugInterface* DebuggerWindow::currentCPU()
|
||||
{
|
||||
std::optional<BreakPointCpu> maybe_cpu = m_dock_manager->cpu();
|
||||
if (!maybe_cpu.has_value())
|
||||
return nullptr;
|
||||
|
||||
return &DebugInterface::get(*maybe_cpu);
|
||||
}
|
||||
81
pcsx2-qt/Debugger/DebuggerWindow.h
Normal file
81
pcsx2-qt/Debugger/DebuggerWindow.h
Normal file
@@ -0,0 +1,81 @@
|
||||
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
|
||||
// SPDX-License-Identifier: GPL-3.0+
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "ui_DebuggerWindow.h"
|
||||
|
||||
#include "DebugTools/DebugInterface.h"
|
||||
|
||||
#include <kddockwidgets/MainWindow.h>
|
||||
#include <QtCore/QTimer>
|
||||
|
||||
class DockManager;
|
||||
|
||||
class DebuggerWindow : public KDDockWidgets::QtWidgets::MainWindow
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
DebuggerWindow(QWidget* parent);
|
||||
|
||||
static DebuggerWindow* getInstance();
|
||||
static DebuggerWindow* createInstance();
|
||||
static void destroyInstance();
|
||||
static bool shouldShowOnStartup();
|
||||
|
||||
DockManager& dockManager();
|
||||
|
||||
void setupDefaultToolBarState();
|
||||
void clearToolBarState();
|
||||
void setupFonts();
|
||||
void updateFontActions();
|
||||
void saveFontSize();
|
||||
int fontSize();
|
||||
void updateTheme();
|
||||
|
||||
void saveWindowGeometry();
|
||||
void restoreWindowGeometry();
|
||||
bool shouldSaveWindowGeometry();
|
||||
|
||||
void updateFromSettings();
|
||||
|
||||
public slots:
|
||||
void onVMStarting();
|
||||
void onVMPaused();
|
||||
void onVMResumed();
|
||||
void onVMStopped();
|
||||
|
||||
void onAnalyse();
|
||||
void onSettings();
|
||||
void onGameSettings();
|
||||
void onRunPause();
|
||||
void onStepInto();
|
||||
void onStepOver();
|
||||
void onStepOut();
|
||||
|
||||
Q_SIGNALS:
|
||||
// Only emitted if the pause wasn't a temporary one triggered by the
|
||||
// breakpoint code.
|
||||
void onVMActuallyPaused();
|
||||
|
||||
protected:
|
||||
void closeEvent(QCloseEvent* event);
|
||||
|
||||
private:
|
||||
DebugInterface* currentCPU();
|
||||
|
||||
Ui::DebuggerWindow m_ui;
|
||||
|
||||
DockManager* m_dock_manager;
|
||||
|
||||
QByteArray m_default_toolbar_state;
|
||||
QTimer* m_refresh_timer = nullptr;
|
||||
|
||||
int m_font_size;
|
||||
static const constexpr int DEFAULT_FONT_SIZE = 10;
|
||||
static const constexpr int MINIMUM_FONT_SIZE = 5;
|
||||
static const constexpr int MAXIMUM_FONT_SIZE = 30;
|
||||
};
|
||||
|
||||
extern DebuggerWindow* g_debugger_window;
|
||||
336
pcsx2-qt/Debugger/DebuggerWindow.ui
Normal file
336
pcsx2-qt/Debugger/DebuggerWindow.ui
Normal file
@@ -0,0 +1,336 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>DebuggerWindow</class>
|
||||
<widget class="QMainWindow" name="DebuggerWindow">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>1000</width>
|
||||
<height>750</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>PCSX2 Debugger</string>
|
||||
</property>
|
||||
<property name="windowIcon">
|
||||
<iconset>
|
||||
<normalon>:/icons/AppIcon64.png</normalon>
|
||||
</iconset>
|
||||
</property>
|
||||
<widget class="QMenuBar" name="menuBar">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>1000</width>
|
||||
<height>21</height>
|
||||
</rect>
|
||||
</property>
|
||||
<widget class="QMenu" name="menuFile">
|
||||
<property name="title">
|
||||
<string>File</string>
|
||||
</property>
|
||||
<addaction name="actionAnalyse"/>
|
||||
<addaction name="actionSettings"/>
|
||||
<addaction name="actionGameSettings"/>
|
||||
<addaction name="separator"/>
|
||||
<addaction name="actionClose"/>
|
||||
</widget>
|
||||
<widget class="QMenu" name="menuDebug">
|
||||
<property name="title">
|
||||
<string>Debug</string>
|
||||
</property>
|
||||
<addaction name="actionRun"/>
|
||||
<addaction name="actionStepInto"/>
|
||||
<addaction name="actionStepOver"/>
|
||||
<addaction name="actionStepOut"/>
|
||||
</widget>
|
||||
<widget class="QMenu" name="menuWindows">
|
||||
<property name="title">
|
||||
<string>Windows</string>
|
||||
</property>
|
||||
<addaction name="actionWindowsDummy"/>
|
||||
</widget>
|
||||
<widget class="QMenu" name="menuView">
|
||||
<property name="title">
|
||||
<string>View</string>
|
||||
</property>
|
||||
<addaction name="actionOnTop"/>
|
||||
<addaction name="separator"/>
|
||||
<addaction name="actionIncreaseFontSize"/>
|
||||
<addaction name="actionDecreaseFontSize"/>
|
||||
<addaction name="actionResetFontSize"/>
|
||||
</widget>
|
||||
<widget class="QMenu" name="menuLayouts">
|
||||
<property name="title">
|
||||
<string>Layouts</string>
|
||||
</property>
|
||||
<addaction name="actionResetAllLayouts"/>
|
||||
<addaction name="actionResetDefaultLayouts"/>
|
||||
</widget>
|
||||
<widget class="QMenu" name="menuTools">
|
||||
<property name="title">
|
||||
<string>Tools</string>
|
||||
</property>
|
||||
<addaction name="actionToolsDummy"/>
|
||||
</widget>
|
||||
<addaction name="menuFile"/>
|
||||
<addaction name="menuView"/>
|
||||
<addaction name="menuDebug"/>
|
||||
<addaction name="menuTools"/>
|
||||
<addaction name="menuWindows"/>
|
||||
<addaction name="menuLayouts"/>
|
||||
</widget>
|
||||
<widget class="QToolBar" name="toolBarDebug">
|
||||
<property name="windowTitle">
|
||||
<string>Debug</string>
|
||||
</property>
|
||||
<property name="toolButtonStyle">
|
||||
<enum>Qt::ToolButtonTextBesideIcon</enum>
|
||||
</property>
|
||||
<attribute name="toolBarArea">
|
||||
<enum>TopToolBarArea</enum>
|
||||
</attribute>
|
||||
<attribute name="toolBarBreak">
|
||||
<bool>false</bool>
|
||||
</attribute>
|
||||
<addaction name="actionRun"/>
|
||||
<addaction name="actionStepInto"/>
|
||||
<addaction name="actionStepOver"/>
|
||||
<addaction name="actionStepOut"/>
|
||||
</widget>
|
||||
<widget class="QToolBar" name="toolBarFile">
|
||||
<property name="windowTitle">
|
||||
<string>File</string>
|
||||
</property>
|
||||
<property name="toolButtonStyle">
|
||||
<enum>Qt::ToolButtonTextBesideIcon</enum>
|
||||
</property>
|
||||
<attribute name="toolBarArea">
|
||||
<enum>TopToolBarArea</enum>
|
||||
</attribute>
|
||||
<attribute name="toolBarBreak">
|
||||
<bool>false</bool>
|
||||
</attribute>
|
||||
<addaction name="actionAnalyse"/>
|
||||
<addaction name="actionSettings"/>
|
||||
<addaction name="actionGameSettings"/>
|
||||
</widget>
|
||||
<widget class="QToolBar" name="toolBarSystem">
|
||||
<property name="windowTitle">
|
||||
<string>System</string>
|
||||
</property>
|
||||
<property name="toolButtonStyle">
|
||||
<enum>Qt::ToolButtonTextBesideIcon</enum>
|
||||
</property>
|
||||
<attribute name="toolBarArea">
|
||||
<enum>TopToolBarArea</enum>
|
||||
</attribute>
|
||||
<attribute name="toolBarBreak">
|
||||
<bool>false</bool>
|
||||
</attribute>
|
||||
<addaction name="actionShutDown"/>
|
||||
<addaction name="actionReset"/>
|
||||
</widget>
|
||||
<widget class="QToolBar" name="toolBarView">
|
||||
<property name="windowTitle">
|
||||
<string>View</string>
|
||||
</property>
|
||||
<property name="toolButtonStyle">
|
||||
<enum>Qt::ToolButtonTextBesideIcon</enum>
|
||||
</property>
|
||||
<attribute name="toolBarArea">
|
||||
<enum>TopToolBarArea</enum>
|
||||
</attribute>
|
||||
<attribute name="toolBarBreak">
|
||||
<bool>false</bool>
|
||||
</attribute>
|
||||
<addaction name="actionOnTop"/>
|
||||
<addaction name="actionIncreaseFontSize"/>
|
||||
<addaction name="actionDecreaseFontSize"/>
|
||||
<addaction name="actionResetFontSize"/>
|
||||
</widget>
|
||||
<action name="actionRun">
|
||||
<property name="icon">
|
||||
<iconset theme="play-line"/>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Run</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actionStepInto">
|
||||
<property name="icon">
|
||||
<iconset theme="debug-step-into-line"/>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Step Into</string>
|
||||
</property>
|
||||
<property name="shortcut">
|
||||
<string>F11</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actionStepOver">
|
||||
<property name="icon">
|
||||
<iconset theme="debug-step-over-line"/>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Step Over</string>
|
||||
</property>
|
||||
<property name="shortcut">
|
||||
<string>F10</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actionStepOut">
|
||||
<property name="icon">
|
||||
<iconset theme="debug-step-out-line"/>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Step Out</string>
|
||||
</property>
|
||||
<property name="shortcut">
|
||||
<string>Shift+F11</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actionOnTop">
|
||||
<property name="checkable">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="icon">
|
||||
<iconset theme="pin-filled"/>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Always On Top</string>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>Show this window on top</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actionAnalyse">
|
||||
<property name="icon">
|
||||
<iconset theme="magnifier-line"/>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Analyze</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actionResetAllLayouts">
|
||||
<property name="text">
|
||||
<string>Reset All Layouts</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actionResetDefaultLayouts">
|
||||
<property name="text">
|
||||
<string>Reset Default Layouts</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actionResetSplitterPositions">
|
||||
<property name="text">
|
||||
<string>Reset Splitter Positions</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actionShutDown">
|
||||
<property name="icon">
|
||||
<iconset theme="shut-down-line"/>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Shut Down</string>
|
||||
</property>
|
||||
<property name="menuRole">
|
||||
<enum>QAction::NoRole</enum>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actionReset">
|
||||
<property name="icon">
|
||||
<iconset theme="restart-line"/>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Reset</string>
|
||||
</property>
|
||||
<property name="menuRole">
|
||||
<enum>QAction::NoRole</enum>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actionClose">
|
||||
<property name="icon">
|
||||
<iconset theme="close-line"/>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Close</string>
|
||||
</property>
|
||||
<property name="menuRole">
|
||||
<enum>QAction::NoRole</enum>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actionIncreaseFontSize">
|
||||
<property name="icon">
|
||||
<iconset theme="zoom-in-line"/>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Increase Font Size</string>
|
||||
</property>
|
||||
<property name="menuRole">
|
||||
<enum>QAction::NoRole</enum>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actionDecreaseFontSize">
|
||||
<property name="icon">
|
||||
<iconset theme="zoom-out-line"/>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Decrease Font Size</string>
|
||||
</property>
|
||||
<property name="shortcut">
|
||||
<string>Ctrl+-</string>
|
||||
</property>
|
||||
<property name="menuRole">
|
||||
<enum>QAction::NoRole</enum>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actionResetFontSize">
|
||||
<property name="icon">
|
||||
<iconset theme="refresh-line"/>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Reset Font Size</string>
|
||||
</property>
|
||||
<property name="menuRole">
|
||||
<enum>QAction::NoRole</enum>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actionSettings">
|
||||
<property name="icon">
|
||||
<iconset theme="settings-3-line"/>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Settings</string>
|
||||
</property>
|
||||
<property name="menuRole">
|
||||
<enum>QAction::NoRole</enum>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actionGameSettings">
|
||||
<property name="icon">
|
||||
<iconset theme="file-settings-line"/>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Game Settings</string>
|
||||
</property>
|
||||
<property name="menuRole">
|
||||
<enum>QAction::NoRole</enum>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actionToolsDummy">
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actionWindowsDummy">
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
</action>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
||||
1012
pcsx2-qt/Debugger/DisassemblyView.cpp
Normal file
1012
pcsx2-qt/Debugger/DisassemblyView.cpp
Normal file
File diff suppressed because it is too large
Load Diff
97
pcsx2-qt/Debugger/DisassemblyView.h
Normal file
97
pcsx2-qt/Debugger/DisassemblyView.h
Normal file
@@ -0,0 +1,97 @@
|
||||
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
|
||||
// SPDX-License-Identifier: GPL-3.0+
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "ui_DisassemblyView.h"
|
||||
|
||||
#include "DebuggerView.h"
|
||||
|
||||
#include "pcsx2/DebugTools/DisassemblyManager.h"
|
||||
|
||||
#include <QtWidgets/QMenu>
|
||||
#include <QtGui/QPainter>
|
||||
|
||||
class DisassemblyView final : public DebuggerView
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
DisassemblyView(const DebuggerViewParameters& parameters);
|
||||
~DisassemblyView();
|
||||
|
||||
void toJson(JsonValueWrapper& json) override;
|
||||
bool fromJson(const JsonValueWrapper& json) override;
|
||||
|
||||
// Required for the breakpoint list (ugh wtf)
|
||||
QString GetLineDisasm(u32 address);
|
||||
|
||||
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);
|
||||
|
||||
// Context menu actions
|
||||
// When called, m_selectedAddressStart will be the 'selected' instruction
|
||||
// Of course, m_selectedAddressEnd will be the end of the selection when required
|
||||
void contextCopyAddress();
|
||||
void contextCopyInstructionHex();
|
||||
void contextCopyInstructionText();
|
||||
void contextCopyFunctionName();
|
||||
void contextAssembleInstruction();
|
||||
void contextNoopInstruction();
|
||||
void contextRestoreInstruction();
|
||||
void contextRunToCursor();
|
||||
void contextJumpToCursor();
|
||||
void contextToggleBreakpoint();
|
||||
void contextFollowBranch();
|
||||
void contextGoToAddress();
|
||||
void contextAddFunction();
|
||||
void contextRenameFunction();
|
||||
void contextRemoveFunction();
|
||||
void contextStubFunction();
|
||||
void contextRestoreFunction();
|
||||
void contextShowInstructionBytes();
|
||||
|
||||
void gotoAddressAndSetFocus(u32 address);
|
||||
void gotoProgramCounterOnPause();
|
||||
void gotoAddress(u32 address, bool should_set_focus);
|
||||
|
||||
void toggleBreakpoint(u32 address);
|
||||
|
||||
private:
|
||||
Ui::DisassemblyView m_ui;
|
||||
|
||||
u32 m_visibleStart = 0x100000; // The address of the first instruction shown.
|
||||
u32 m_visibleRows;
|
||||
u32 m_selectedAddressStart = 0;
|
||||
u32 m_selectedAddressEnd = 0;
|
||||
u32 m_rowHeight = 0;
|
||||
|
||||
std::map<u32, u32> m_nopedInstructions;
|
||||
std::map<u32, std::tuple<u32, u32>> m_stubbedFunctions;
|
||||
|
||||
bool m_showInstructionBytes = true;
|
||||
bool m_goToProgramCounterOnPause = true;
|
||||
DisassemblyManager m_disassemblyManager;
|
||||
|
||||
QString GetDisassemblyTitleLine();
|
||||
QColor GetDisassemblyTitleLineColor();
|
||||
inline QString DisassemblyStringFromAddress(u32 address, QFont font, u32 pc, bool selected);
|
||||
QColor GetAddressFunctionColor(u32 address);
|
||||
enum class SelectionInfo
|
||||
{
|
||||
ADDRESS,
|
||||
INSTRUCTIONHEX,
|
||||
INSTRUCTIONTEXT,
|
||||
};
|
||||
QString FetchSelectionInfo(SelectionInfo selInfo);
|
||||
|
||||
bool AddressCanRestore(u32 start, u32 end);
|
||||
bool FunctionCanRestore(u32 address);
|
||||
};
|
||||
19
pcsx2-qt/Debugger/DisassemblyView.ui
Normal file
19
pcsx2-qt/Debugger/DisassemblyView.ui
Normal file
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>DisassemblyView</class>
|
||||
<widget class="QWidget" name="DisassemblyView">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>400</width>
|
||||
<height>300</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Disassembly</string>
|
||||
</property>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
||||
926
pcsx2-qt/Debugger/Docking/DockLayout.cpp
Normal file
926
pcsx2-qt/Debugger/Docking/DockLayout.cpp
Normal file
@@ -0,0 +1,926 @@
|
||||
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
|
||||
// SPDX-License-Identifier: GPL-3.0+
|
||||
|
||||
#include "DockLayout.h"
|
||||
|
||||
#include "Debugger/DebuggerView.h"
|
||||
#include "Debugger/DebuggerWindow.h"
|
||||
#include "Debugger/JsonValueWrapper.h"
|
||||
|
||||
#include "common/Assertions.h"
|
||||
#include "common/Console.h"
|
||||
#include "common/FileSystem.h"
|
||||
#include "common/Path.h"
|
||||
|
||||
#include <kddockwidgets/Config.h>
|
||||
#include <kddockwidgets/DockWidget.h>
|
||||
#include <kddockwidgets/LayoutSaver.h>
|
||||
#include <kddockwidgets/core/DockRegistry.h>
|
||||
#include <kddockwidgets/core/DockWidget.h>
|
||||
#include <kddockwidgets/core/Group.h>
|
||||
#include <kddockwidgets/core/Layout.h>
|
||||
#include <kddockwidgets/core/ViewFactory.h>
|
||||
#include <kddockwidgets/qtwidgets/Group.h>
|
||||
#include <kddockwidgets/qtwidgets/MainWindow.h>
|
||||
|
||||
#include "rapidjson/document.h"
|
||||
#include "rapidjson/prettywriter.h"
|
||||
|
||||
const char* DEBUGGER_LAYOUT_FILE_FORMAT = "PCSX2 Debugger User Interface Layout";
|
||||
|
||||
// Increment this whenever there is a breaking change to the JSON format.
|
||||
const u32 DEBUGGER_LAYOUT_FILE_VERSION_MAJOR = 2;
|
||||
|
||||
// Increment this whenever there is a non-breaking change to the JSON format.
|
||||
const u32 DEBUGGER_LAYOUT_FILE_VERSION_MINOR = 0;
|
||||
|
||||
DockLayout::DockLayout(
|
||||
QString name,
|
||||
BreakPointCpu cpu,
|
||||
bool is_default,
|
||||
const std::string& base_name,
|
||||
DockLayout::Index index)
|
||||
: m_name(name)
|
||||
, m_cpu(cpu)
|
||||
, m_is_default(is_default)
|
||||
, m_base_layout(base_name)
|
||||
{
|
||||
reset();
|
||||
save(index);
|
||||
}
|
||||
|
||||
DockLayout::DockLayout(
|
||||
QString name,
|
||||
BreakPointCpu cpu,
|
||||
bool is_default,
|
||||
DockLayout::Index index)
|
||||
: m_name(name)
|
||||
, m_cpu(cpu)
|
||||
, m_is_default(is_default)
|
||||
{
|
||||
save(index);
|
||||
}
|
||||
|
||||
DockLayout::DockLayout(
|
||||
QString name,
|
||||
BreakPointCpu cpu,
|
||||
bool is_default,
|
||||
const DockLayout& layout_to_clone,
|
||||
DockLayout::Index index)
|
||||
: m_name(name)
|
||||
, m_cpu(cpu)
|
||||
, m_is_default(is_default)
|
||||
, m_next_id(layout_to_clone.m_next_id)
|
||||
, m_base_layout(layout_to_clone.m_base_layout)
|
||||
, m_toolbars(layout_to_clone.m_toolbars)
|
||||
, m_geometry(layout_to_clone.m_geometry)
|
||||
{
|
||||
for (const auto& [unique_name, widget_to_clone] : layout_to_clone.m_widgets)
|
||||
{
|
||||
auto widget_description = DockTables::DEBUGGER_VIEWS.find(widget_to_clone->metaObject()->className());
|
||||
if (widget_description == DockTables::DEBUGGER_VIEWS.end())
|
||||
continue;
|
||||
|
||||
DebuggerViewParameters parameters;
|
||||
parameters.unique_name = unique_name;
|
||||
parameters.id = widget_to_clone->id();
|
||||
parameters.cpu = &DebugInterface::get(cpu);
|
||||
parameters.cpu_override = widget_to_clone->cpuOverride();
|
||||
|
||||
DebuggerView* new_widget = widget_description->second.create_widget(parameters);
|
||||
new_widget->setCustomDisplayName(widget_to_clone->customDisplayName());
|
||||
new_widget->setPrimary(widget_to_clone->isPrimary());
|
||||
m_widgets.emplace(unique_name, new_widget);
|
||||
}
|
||||
|
||||
save(index);
|
||||
}
|
||||
|
||||
DockLayout::DockLayout(
|
||||
const std::string& path,
|
||||
DockLayout::LoadResult& result,
|
||||
DockLayout::Index& index_last_session,
|
||||
DockLayout::Index index)
|
||||
{
|
||||
load(path, result, index_last_session);
|
||||
}
|
||||
|
||||
DockLayout::~DockLayout()
|
||||
{
|
||||
for (auto& [unique_name, widget] : m_widgets)
|
||||
{
|
||||
pxAssert(widget.get());
|
||||
|
||||
delete widget;
|
||||
}
|
||||
}
|
||||
|
||||
const QString& DockLayout::name() const
|
||||
{
|
||||
return m_name;
|
||||
}
|
||||
|
||||
void DockLayout::setName(QString name)
|
||||
{
|
||||
m_name = std::move(name);
|
||||
}
|
||||
|
||||
BreakPointCpu DockLayout::cpu() const
|
||||
{
|
||||
return m_cpu;
|
||||
}
|
||||
|
||||
bool DockLayout::isDefault() const
|
||||
{
|
||||
return m_is_default;
|
||||
}
|
||||
|
||||
void DockLayout::setCpu(BreakPointCpu cpu)
|
||||
{
|
||||
m_cpu = cpu;
|
||||
|
||||
for (auto& [unique_name, widget] : m_widgets)
|
||||
{
|
||||
pxAssert(widget.get());
|
||||
|
||||
if (!widget->setCpu(DebugInterface::get(cpu)))
|
||||
recreateDebuggerView(unique_name);
|
||||
}
|
||||
}
|
||||
|
||||
void DockLayout::freeze()
|
||||
{
|
||||
pxAssert(m_is_active);
|
||||
m_is_active = false;
|
||||
|
||||
if (g_debugger_window)
|
||||
m_toolbars = g_debugger_window->saveState();
|
||||
|
||||
// Store the geometry of all the dock widgets as JSON.
|
||||
KDDockWidgets::LayoutSaver saver(KDDockWidgets::RestoreOption_RelativeToMainWindow);
|
||||
m_geometry = saver.serializeLayout();
|
||||
|
||||
// Delete the dock widgets.
|
||||
for (KDDockWidgets::Core::DockWidget* dock : KDDockWidgets::DockRegistry::self()->dockwidgets())
|
||||
{
|
||||
// Make sure the dock widget releases ownership of its content.
|
||||
auto view = static_cast<KDDockWidgets::QtWidgets::DockWidget*>(dock->view());
|
||||
view->setWidget(new QWidget());
|
||||
|
||||
delete dock;
|
||||
}
|
||||
}
|
||||
|
||||
void DockLayout::thaw()
|
||||
{
|
||||
pxAssert(!m_is_active);
|
||||
m_is_active = true;
|
||||
|
||||
if (!g_debugger_window)
|
||||
return;
|
||||
|
||||
// Restore the state of the toolbars.
|
||||
if (m_toolbars.isEmpty())
|
||||
{
|
||||
const DockTables::DefaultDockLayout* base_layout = DockTables::defaultLayout(m_base_layout);
|
||||
if (base_layout)
|
||||
{
|
||||
for (QToolBar* toolbar : g_debugger_window->findChildren<QToolBar*>())
|
||||
if (base_layout->toolbars.contains(toolbar->objectName().toStdString()))
|
||||
toolbar->show();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
g_debugger_window->restoreState(m_toolbars);
|
||||
}
|
||||
|
||||
if (m_geometry.isEmpty())
|
||||
{
|
||||
// This is a newly created layout with no geometry information.
|
||||
setupDefaultLayout();
|
||||
}
|
||||
else
|
||||
{
|
||||
// Create all the dock widgets.
|
||||
KDDockWidgets::LayoutSaver saver(KDDockWidgets::RestoreOption_RelativeToMainWindow);
|
||||
if (!saver.restoreLayout(m_geometry))
|
||||
{
|
||||
// We've failed to restore the geometry, so just tear down whatever
|
||||
// dock widgets may exist and then setup the default layout.
|
||||
for (KDDockWidgets::Core::DockWidget* dock : KDDockWidgets::DockRegistry::self()->dockwidgets())
|
||||
{
|
||||
// Make sure the dock widget releases ownership of its content.
|
||||
auto view = static_cast<KDDockWidgets::QtWidgets::DockWidget*>(dock->view());
|
||||
view->setWidget(new QWidget());
|
||||
|
||||
delete dock;
|
||||
}
|
||||
|
||||
setupDefaultLayout();
|
||||
}
|
||||
}
|
||||
|
||||
// Check that all the dock widgets have been restored correctly.
|
||||
std::vector<QString> orphaned_debugger_views;
|
||||
for (auto& [unique_name, widget] : m_widgets)
|
||||
{
|
||||
auto [controller, view] = DockUtils::dockWidgetFromName(unique_name);
|
||||
if (!controller || !view)
|
||||
{
|
||||
Console.Error("Debugger: Failed to restore dock widget '%s'.", unique_name.toStdString().c_str());
|
||||
orphaned_debugger_views.emplace_back(unique_name);
|
||||
}
|
||||
}
|
||||
|
||||
// Delete any debugger views that haven't been restored correctly.
|
||||
for (const QString& unique_name : orphaned_debugger_views)
|
||||
{
|
||||
auto widget_iterator = m_widgets.find(unique_name);
|
||||
pxAssert(widget_iterator != m_widgets.end());
|
||||
|
||||
setPrimaryDebuggerView(widget_iterator->second.get(), false);
|
||||
delete widget_iterator->second.get();
|
||||
m_widgets.erase(widget_iterator);
|
||||
}
|
||||
|
||||
updateDockWidgetTitles();
|
||||
}
|
||||
|
||||
bool DockLayout::canReset()
|
||||
{
|
||||
return DockTables::defaultLayout(m_base_layout) != nullptr;
|
||||
}
|
||||
|
||||
void DockLayout::reset()
|
||||
{
|
||||
pxAssert(!m_is_active);
|
||||
|
||||
for (auto& [unique_name, widget] : m_widgets)
|
||||
{
|
||||
pxAssert(widget.get());
|
||||
|
||||
delete widget;
|
||||
}
|
||||
|
||||
m_next_id = 0;
|
||||
m_toolbars.clear();
|
||||
m_widgets.clear();
|
||||
m_geometry.clear();
|
||||
|
||||
const DockTables::DefaultDockLayout* base_layout = DockTables::defaultLayout(m_base_layout);
|
||||
if (!base_layout)
|
||||
return;
|
||||
|
||||
for (size_t i = 0; i < base_layout->widgets.size(); i++)
|
||||
{
|
||||
auto iterator = DockTables::DEBUGGER_VIEWS.find(base_layout->widgets[i].type);
|
||||
pxAssertRel(iterator != DockTables::DEBUGGER_VIEWS.end(), "Invalid default layout.");
|
||||
const DockTables::DebuggerViewDescription& dock_description = iterator->second;
|
||||
|
||||
DebuggerViewParameters parameters;
|
||||
std::tie(parameters.unique_name, parameters.id) =
|
||||
generateNewUniqueName(base_layout->widgets[i].type.c_str());
|
||||
parameters.cpu = &DebugInterface::get(m_cpu);
|
||||
|
||||
if (parameters.unique_name.isEmpty())
|
||||
continue;
|
||||
|
||||
DebuggerView* widget = dock_description.create_widget(parameters);
|
||||
widget->setPrimary(true);
|
||||
m_widgets.emplace(parameters.unique_name, widget);
|
||||
}
|
||||
}
|
||||
|
||||
KDDockWidgets::Core::DockWidget* DockLayout::createDockWidget(const QString& name)
|
||||
{
|
||||
pxAssert(m_is_active);
|
||||
pxAssert(KDDockWidgets::LayoutSaver::restoreInProgress());
|
||||
|
||||
auto widget_iterator = m_widgets.find(name);
|
||||
if (widget_iterator == m_widgets.end())
|
||||
return nullptr;
|
||||
|
||||
DebuggerView* widget = widget_iterator->second;
|
||||
if (!widget)
|
||||
return nullptr;
|
||||
|
||||
pxAssert(widget->uniqueName() == name);
|
||||
|
||||
auto view = static_cast<KDDockWidgets::QtWidgets::DockWidget*>(
|
||||
KDDockWidgets::Config::self().viewFactory()->createDockWidget(name));
|
||||
view->setWidget(widget);
|
||||
|
||||
return view->asController<KDDockWidgets::Core::DockWidget>();
|
||||
}
|
||||
|
||||
void DockLayout::updateDockWidgetTitles()
|
||||
{
|
||||
if (!m_is_active)
|
||||
return;
|
||||
|
||||
// Translate default debugger view names.
|
||||
for (auto& [unique_name, widget] : m_widgets)
|
||||
widget->retranslateDisplayName();
|
||||
|
||||
// Determine if any widgets have duplicate display names.
|
||||
std::map<QString, std::vector<DebuggerView*>> display_name_to_widgets;
|
||||
for (auto& [unique_name, widget] : m_widgets)
|
||||
display_name_to_widgets[widget->displayNameWithoutSuffix()].emplace_back(widget.get());
|
||||
|
||||
for (auto& [display_name, widgets] : display_name_to_widgets)
|
||||
{
|
||||
std::sort(widgets.begin(), widgets.end(),
|
||||
[&](const DebuggerView* lhs, const DebuggerView* rhs) {
|
||||
return lhs->id() < rhs->id();
|
||||
});
|
||||
|
||||
for (size_t i = 0; i < widgets.size(); i++)
|
||||
{
|
||||
std::optional<int> suffix_number;
|
||||
if (widgets.size() != 1)
|
||||
suffix_number = static_cast<int>(i + 1);
|
||||
|
||||
widgets[i]->setDisplayNameSuffixNumber(suffix_number);
|
||||
}
|
||||
}
|
||||
|
||||
// Propagate the new names from the debugger views to the dock widgets.
|
||||
for (auto& [unique_name, widget] : m_widgets)
|
||||
{
|
||||
auto [controller, view] = DockUtils::dockWidgetFromName(widget->uniqueName());
|
||||
if (!controller)
|
||||
continue;
|
||||
|
||||
controller->setTitle(widget->displayName());
|
||||
}
|
||||
}
|
||||
|
||||
const std::map<QString, QPointer<DebuggerView>>& DockLayout::debuggerViews()
|
||||
{
|
||||
return m_widgets;
|
||||
}
|
||||
|
||||
bool DockLayout::hasDebuggerView(const QString& unique_name)
|
||||
{
|
||||
return m_widgets.find(unique_name) != m_widgets.end();
|
||||
}
|
||||
|
||||
size_t DockLayout::countDebuggerViewsOfType(const char* type)
|
||||
{
|
||||
size_t count = 0;
|
||||
for (const auto& [unique_name, widget] : m_widgets)
|
||||
{
|
||||
if (strcmp(widget->metaObject()->className(), type) == 0)
|
||||
count++;
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
void DockLayout::createDebuggerView(const std::string& type)
|
||||
{
|
||||
pxAssert(m_is_active);
|
||||
|
||||
if (!g_debugger_window)
|
||||
return;
|
||||
|
||||
auto description_iterator = DockTables::DEBUGGER_VIEWS.find(type);
|
||||
pxAssert(description_iterator != DockTables::DEBUGGER_VIEWS.end());
|
||||
|
||||
const DockTables::DebuggerViewDescription& description = description_iterator->second;
|
||||
|
||||
DebuggerViewParameters parameters;
|
||||
std::tie(parameters.unique_name, parameters.id) = generateNewUniqueName(type.c_str());
|
||||
parameters.cpu = &DebugInterface::get(m_cpu);
|
||||
|
||||
if (parameters.unique_name.isEmpty())
|
||||
return;
|
||||
|
||||
DebuggerView* widget = description.create_widget(parameters);
|
||||
m_widgets.emplace(parameters.unique_name, widget);
|
||||
|
||||
setPrimaryDebuggerView(widget, countDebuggerViewsOfType(type.c_str()) == 0);
|
||||
|
||||
auto view = static_cast<KDDockWidgets::QtWidgets::DockWidget*>(
|
||||
KDDockWidgets::Config::self().viewFactory()->createDockWidget(widget->uniqueName()));
|
||||
view->setWidget(widget);
|
||||
|
||||
KDDockWidgets::Core::DockWidget* controller = view->asController<KDDockWidgets::Core::DockWidget>();
|
||||
pxAssert(controller);
|
||||
|
||||
DockUtils::insertDockWidgetAtPreferredLocation(controller, description.preferred_location, g_debugger_window);
|
||||
updateDockWidgetTitles();
|
||||
}
|
||||
|
||||
void DockLayout::recreateDebuggerView(const QString& unique_name)
|
||||
{
|
||||
if (!g_debugger_window)
|
||||
return;
|
||||
|
||||
auto debugger_view_iterator = m_widgets.find(unique_name);
|
||||
pxAssert(debugger_view_iterator != m_widgets.end());
|
||||
|
||||
DebuggerView* old_debugger_view = debugger_view_iterator->second;
|
||||
|
||||
auto description_iterator = DockTables::DEBUGGER_VIEWS.find(old_debugger_view->metaObject()->className());
|
||||
pxAssert(description_iterator != DockTables::DEBUGGER_VIEWS.end());
|
||||
|
||||
const DockTables::DebuggerViewDescription& description = description_iterator->second;
|
||||
|
||||
DebuggerViewParameters parameters;
|
||||
parameters.unique_name = old_debugger_view->uniqueName();
|
||||
parameters.id = old_debugger_view->id();
|
||||
parameters.cpu = &DebugInterface::get(m_cpu);
|
||||
parameters.cpu_override = old_debugger_view->cpuOverride();
|
||||
|
||||
DebuggerView* new_debugger_view = description.create_widget(parameters);
|
||||
new_debugger_view->setCustomDisplayName(old_debugger_view->customDisplayName());
|
||||
new_debugger_view->setPrimary(old_debugger_view->isPrimary());
|
||||
debugger_view_iterator->second = new_debugger_view;
|
||||
|
||||
if (m_is_active)
|
||||
{
|
||||
auto [controller, view] = DockUtils::dockWidgetFromName(unique_name);
|
||||
if (view)
|
||||
view->setWidget(new_debugger_view);
|
||||
}
|
||||
|
||||
delete old_debugger_view;
|
||||
}
|
||||
|
||||
void DockLayout::destroyDebuggerView(const QString& unique_name)
|
||||
{
|
||||
pxAssert(m_is_active);
|
||||
|
||||
if (!g_debugger_window)
|
||||
return;
|
||||
|
||||
auto debugger_view_iterator = m_widgets.find(unique_name);
|
||||
if (debugger_view_iterator == m_widgets.end())
|
||||
return;
|
||||
|
||||
setPrimaryDebuggerView(debugger_view_iterator->second.get(), false);
|
||||
delete debugger_view_iterator->second.get();
|
||||
m_widgets.erase(debugger_view_iterator);
|
||||
|
||||
auto [controller, view] = DockUtils::dockWidgetFromName(unique_name);
|
||||
if (!controller)
|
||||
return;
|
||||
|
||||
controller->deleteLater();
|
||||
|
||||
updateDockWidgetTitles();
|
||||
}
|
||||
|
||||
void DockLayout::setPrimaryDebuggerView(DebuggerView* widget, bool is_primary)
|
||||
{
|
||||
bool present = false;
|
||||
for (auto& [unique_name, test_widget] : m_widgets)
|
||||
{
|
||||
if (test_widget.get() == widget)
|
||||
{
|
||||
present = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!present)
|
||||
return;
|
||||
|
||||
if (is_primary)
|
||||
{
|
||||
// Set the passed widget as the primary widget.
|
||||
for (auto& [unique_name, test_widget] : m_widgets)
|
||||
{
|
||||
if (strcmp(test_widget->metaObject()->className(), widget->metaObject()->className()) == 0)
|
||||
{
|
||||
test_widget->setPrimary(test_widget.get() == widget);
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (widget->isPrimary())
|
||||
{
|
||||
// Set an arbitrary widget as the primary widget.
|
||||
bool next = true;
|
||||
for (auto& [unique_name, test_widget] : m_widgets)
|
||||
{
|
||||
if (test_widget != widget &&
|
||||
strcmp(test_widget->metaObject()->className(), widget->metaObject()->className()) == 0)
|
||||
{
|
||||
test_widget->setPrimary(next);
|
||||
next = false;
|
||||
}
|
||||
}
|
||||
|
||||
// If we haven't set another widget as the primary one we can't make
|
||||
// this one not the primary one.
|
||||
if (!next)
|
||||
widget->setPrimary(false);
|
||||
}
|
||||
}
|
||||
|
||||
void DockLayout::deleteFile()
|
||||
{
|
||||
if (m_layout_file_path.empty())
|
||||
return;
|
||||
|
||||
if (!FileSystem::DeleteFilePath(m_layout_file_path.c_str()))
|
||||
Console.Error("Debugger: Failed to delete layout file '%s'.", m_layout_file_path.c_str());
|
||||
}
|
||||
|
||||
bool DockLayout::save(DockLayout::Index layout_index)
|
||||
{
|
||||
if (!g_debugger_window)
|
||||
return false;
|
||||
|
||||
if (m_is_active)
|
||||
{
|
||||
m_toolbars = g_debugger_window->saveState();
|
||||
|
||||
// Store the geometry of all the dock widgets as JSON.
|
||||
KDDockWidgets::LayoutSaver saver(KDDockWidgets::RestoreOption_RelativeToMainWindow);
|
||||
m_geometry = saver.serializeLayout();
|
||||
}
|
||||
|
||||
// Serialize the layout as JSON.
|
||||
rapidjson::Document json(rapidjson::kObjectType);
|
||||
rapidjson::Document geometry;
|
||||
|
||||
const char* cpu_name = DebugInterface::cpuName(m_cpu);
|
||||
u32 default_layout_hash = DockTables::hashDefaultLayouts();
|
||||
|
||||
rapidjson::Value format;
|
||||
format.SetString(DEBUGGER_LAYOUT_FILE_FORMAT, strlen(DEBUGGER_LAYOUT_FILE_FORMAT));
|
||||
json.AddMember("format", format, json.GetAllocator());
|
||||
|
||||
json.AddMember("versionMajor", DEBUGGER_LAYOUT_FILE_VERSION_MAJOR, json.GetAllocator());
|
||||
json.AddMember("versionMinor", DEBUGGER_LAYOUT_FILE_VERSION_MINOR, json.GetAllocator());
|
||||
json.AddMember("defaultLayoutHash", default_layout_hash, json.GetAllocator());
|
||||
|
||||
std::string name_str = m_name.toStdString();
|
||||
json.AddMember("name", rapidjson::Value().SetString(name_str.c_str(), name_str.size()), json.GetAllocator());
|
||||
json.AddMember("target", rapidjson::Value().SetString(cpu_name, strlen(cpu_name)), json.GetAllocator());
|
||||
json.AddMember("index", static_cast<int>(layout_index), json.GetAllocator());
|
||||
json.AddMember("isDefault", m_is_default, json.GetAllocator());
|
||||
json.AddMember("nextId", m_next_id, json.GetAllocator());
|
||||
|
||||
if (!m_base_layout.empty())
|
||||
{
|
||||
rapidjson::Value base_layout;
|
||||
base_layout.SetString(m_base_layout.c_str(), m_base_layout.size());
|
||||
json.AddMember("baseLayout", base_layout, json.GetAllocator());
|
||||
}
|
||||
|
||||
if (!m_toolbars.isEmpty())
|
||||
{
|
||||
std::string toolbars_str = m_toolbars.toBase64().toStdString();
|
||||
rapidjson::Value toolbars;
|
||||
toolbars.SetString(toolbars_str.data(), toolbars_str.size(), json.GetAllocator());
|
||||
json.AddMember("toolbars", toolbars, json.GetAllocator());
|
||||
}
|
||||
|
||||
rapidjson::Value dock_widgets(rapidjson::kArrayType);
|
||||
for (auto& [unique_name, widget] : m_widgets)
|
||||
{
|
||||
pxAssert(widget.get());
|
||||
|
||||
rapidjson::Value object(rapidjson::kObjectType);
|
||||
|
||||
std::string name_str = unique_name.toStdString();
|
||||
rapidjson::Value name;
|
||||
name.SetString(name_str.c_str(), name_str.size(), json.GetAllocator());
|
||||
object.AddMember("uniqueName", name, json.GetAllocator());
|
||||
object.AddMember("id", widget->id(), json.GetAllocator());
|
||||
|
||||
const char* type_str = widget->metaObject()->className();
|
||||
rapidjson::Value type;
|
||||
type.SetString(type_str, strlen(type_str), json.GetAllocator());
|
||||
object.AddMember("type", type, json.GetAllocator());
|
||||
|
||||
if (widget->cpuOverride().has_value())
|
||||
{
|
||||
const char* cpu_name = DebugInterface::cpuName(*widget->cpuOverride());
|
||||
|
||||
rapidjson::Value target;
|
||||
target.SetString(cpu_name, strlen(cpu_name));
|
||||
object.AddMember("target", target, json.GetAllocator());
|
||||
}
|
||||
|
||||
JsonValueWrapper wrapper(object, json.GetAllocator());
|
||||
widget->toJson(wrapper);
|
||||
|
||||
dock_widgets.PushBack(object, json.GetAllocator());
|
||||
}
|
||||
json.AddMember("dockWidgets", dock_widgets, json.GetAllocator());
|
||||
|
||||
if (!m_geometry.isEmpty() && !geometry.Parse(m_geometry).HasParseError())
|
||||
json.AddMember("geometry", geometry, json.GetAllocator());
|
||||
|
||||
rapidjson::StringBuffer string_buffer;
|
||||
rapidjson::PrettyWriter<rapidjson::StringBuffer> writer(string_buffer);
|
||||
json.Accept(writer);
|
||||
|
||||
std::string safe_name = Path::SanitizeFileName(m_name.toStdString());
|
||||
|
||||
// Create a temporary file first so that we don't corrupt an existing file
|
||||
// in the case that we succeed in opening the file but fail to write our
|
||||
// data to it.
|
||||
std::string temp_file_path = Path::Combine(EmuFolders::DebuggerLayouts, safe_name + ".tmp");
|
||||
|
||||
if (!FileSystem::WriteStringToFile(temp_file_path.c_str(), string_buffer.GetString()))
|
||||
{
|
||||
Console.Error("Debugger: Failed to save temporary layout file '%s'.", temp_file_path.c_str());
|
||||
FileSystem::DeleteFilePath(temp_file_path.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
// Now move the layout to its final location.
|
||||
std::string file_path = Path::Combine(EmuFolders::DebuggerLayouts, safe_name + ".json");
|
||||
|
||||
if (!FileSystem::RenamePath(temp_file_path.c_str(), file_path.c_str()))
|
||||
{
|
||||
Console.Error("Debugger: Failed to move layout file to '%s'.", file_path.c_str());
|
||||
FileSystem::DeleteFilePath(temp_file_path.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
// If the layout has been renamed we need to delete the old file.
|
||||
if (file_path != m_layout_file_path)
|
||||
deleteFile();
|
||||
|
||||
m_layout_file_path = std::move(file_path);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void DockLayout::load(
|
||||
const std::string& path,
|
||||
LoadResult& result,
|
||||
DockLayout::Index& index_last_session)
|
||||
{
|
||||
pxAssert(!m_is_active);
|
||||
|
||||
result = SUCCESS;
|
||||
|
||||
std::optional<std::string> text = FileSystem::ReadFileToString(path.c_str());
|
||||
if (!text.has_value())
|
||||
{
|
||||
Console.Error("Debugger: Failed to open layout file '%s'.", path.c_str());
|
||||
result = FILE_NOT_FOUND;
|
||||
return;
|
||||
}
|
||||
|
||||
rapidjson::Document json;
|
||||
if (json.Parse(text->c_str()).HasParseError() || !json.IsObject())
|
||||
{
|
||||
Console.Error("Debugger: Failed to parse layout file '%s' as JSON.", path.c_str());
|
||||
result = INVALID_FORMAT;
|
||||
return;
|
||||
}
|
||||
|
||||
auto format = json.FindMember("format");
|
||||
if (format == json.MemberEnd() ||
|
||||
!format->value.IsString() ||
|
||||
strcmp(format->value.GetString(), DEBUGGER_LAYOUT_FILE_FORMAT) != 0)
|
||||
{
|
||||
Console.Error("Debugger: Layout file '%s' has missing or invalid 'format' property.", path.c_str());
|
||||
result = INVALID_FORMAT;
|
||||
return;
|
||||
}
|
||||
|
||||
auto version_major = json.FindMember("versionMajor");
|
||||
if (version_major == json.MemberEnd() || !version_major->value.IsInt())
|
||||
{
|
||||
Console.Error("Debugger: Layout file '%s' has missing or invalid 'versionMajor' property.", path.c_str());
|
||||
result = MAJOR_VERSION_MISMATCH;
|
||||
return;
|
||||
}
|
||||
|
||||
if (version_major->value.GetInt() != DEBUGGER_LAYOUT_FILE_VERSION_MAJOR)
|
||||
{
|
||||
result = MAJOR_VERSION_MISMATCH;
|
||||
return;
|
||||
}
|
||||
|
||||
auto version_minor = json.FindMember("versionMinor");
|
||||
if (version_minor == json.MemberEnd() || !version_minor->value.IsInt())
|
||||
{
|
||||
Console.Error("Debugger: Layout file '%s' has missing or invalid 'versionMinor' property.", path.c_str());
|
||||
result = MAJOR_VERSION_MISMATCH;
|
||||
return;
|
||||
}
|
||||
|
||||
auto default_layout_hash = json.FindMember("defaultLayoutHash");
|
||||
if (default_layout_hash == json.MemberEnd() || !default_layout_hash->value.IsUint())
|
||||
{
|
||||
Console.Error("Debugger: Layout file '%s' has missing or invalid 'defaultLayoutHash' property.", path.c_str());
|
||||
result = MAJOR_VERSION_MISMATCH;
|
||||
return;
|
||||
}
|
||||
|
||||
if (default_layout_hash->value.GetUint() != DockTables::hashDefaultLayouts())
|
||||
result = DEFAULT_LAYOUT_HASH_MISMATCH;
|
||||
|
||||
auto name = json.FindMember("name");
|
||||
if (name != json.MemberEnd() && name->value.IsString())
|
||||
m_name = name->value.GetString();
|
||||
else
|
||||
m_name = QCoreApplication::translate("DockLayout", "Unnamed");
|
||||
|
||||
m_name.truncate(DockUtils::MAX_LAYOUT_NAME_SIZE);
|
||||
|
||||
auto target = json.FindMember("target");
|
||||
m_cpu = BREAKPOINT_EE;
|
||||
if (target != json.MemberEnd() && target->value.IsString())
|
||||
{
|
||||
for (BreakPointCpu cpu : DEBUG_CPUS)
|
||||
if (strcmp(DebugInterface::cpuName(cpu), target->value.GetString()) == 0)
|
||||
m_cpu = cpu;
|
||||
}
|
||||
|
||||
auto index = json.FindMember("index");
|
||||
if (index != json.MemberEnd() && index->value.IsInt())
|
||||
index_last_session = index->value.GetInt();
|
||||
|
||||
auto is_default = json.FindMember("isDefault");
|
||||
if (is_default != json.MemberEnd() && is_default->value.IsBool())
|
||||
m_is_default = is_default->value.GetBool();
|
||||
|
||||
auto next_id = json.FindMember("nextId");
|
||||
if (next_id != json.MemberBegin() && next_id->value.IsUint64())
|
||||
m_next_id = next_id->value.GetUint64();
|
||||
|
||||
auto base_layout = json.FindMember("baseLayout");
|
||||
if (base_layout != json.MemberEnd() && base_layout->value.IsString())
|
||||
m_base_layout = base_layout->value.GetString();
|
||||
|
||||
auto toolbars = json.FindMember("toolbars");
|
||||
if (toolbars != json.MemberEnd() && toolbars->value.IsString())
|
||||
m_toolbars = QByteArray::fromBase64(toolbars->value.GetString());
|
||||
|
||||
auto dock_widgets = json.FindMember("dockWidgets");
|
||||
if (dock_widgets != json.MemberEnd() && dock_widgets->value.IsArray())
|
||||
{
|
||||
for (rapidjson::Value& object : dock_widgets->value.GetArray())
|
||||
{
|
||||
auto unique_name = object.FindMember("uniqueName");
|
||||
if (unique_name == object.MemberEnd() || !unique_name->value.IsString())
|
||||
continue;
|
||||
|
||||
auto id = object.FindMember("id");
|
||||
if (id == object.MemberEnd() || !id->value.IsUint64())
|
||||
continue;
|
||||
|
||||
auto widgets_iterator = m_widgets.find(unique_name->value.GetString());
|
||||
if (widgets_iterator != m_widgets.end())
|
||||
continue;
|
||||
|
||||
auto type = object.FindMember("type");
|
||||
if (type == object.MemberEnd() || !type->value.IsString())
|
||||
continue;
|
||||
|
||||
auto description = DockTables::DEBUGGER_VIEWS.find(type->value.GetString());
|
||||
if (description == DockTables::DEBUGGER_VIEWS.end())
|
||||
continue;
|
||||
|
||||
std::optional<BreakPointCpu> cpu_override;
|
||||
|
||||
auto target = object.FindMember("target");
|
||||
if (target != object.MemberEnd() && target->value.IsString())
|
||||
{
|
||||
for (BreakPointCpu cpu : DEBUG_CPUS)
|
||||
if (strcmp(DebugInterface::cpuName(cpu), target->value.GetString()) == 0)
|
||||
cpu_override = cpu;
|
||||
}
|
||||
|
||||
DebuggerViewParameters parameters;
|
||||
parameters.unique_name = unique_name->value.GetString();
|
||||
parameters.id = id->value.GetUint64();
|
||||
parameters.cpu = &DebugInterface::get(m_cpu);
|
||||
parameters.cpu_override = cpu_override;
|
||||
|
||||
DebuggerView* widget = description->second.create_widget(parameters);
|
||||
|
||||
JsonValueWrapper wrapper(object, json.GetAllocator());
|
||||
if (!widget->fromJson(wrapper))
|
||||
{
|
||||
delete widget;
|
||||
continue;
|
||||
}
|
||||
|
||||
m_widgets.emplace(unique_name->value.GetString(), widget);
|
||||
}
|
||||
}
|
||||
|
||||
auto geometry = json.FindMember("geometry");
|
||||
if (geometry != json.MemberEnd() && geometry->value.IsObject())
|
||||
{
|
||||
rapidjson::StringBuffer string_buffer;
|
||||
rapidjson::Writer<rapidjson::StringBuffer> writer(string_buffer);
|
||||
geometry->value.Accept(writer);
|
||||
|
||||
m_geometry = QByteArray(string_buffer.GetString(), string_buffer.GetSize());
|
||||
}
|
||||
|
||||
m_layout_file_path = path;
|
||||
|
||||
validatePrimaryDebuggerViews();
|
||||
}
|
||||
|
||||
void DockLayout::validatePrimaryDebuggerViews()
|
||||
{
|
||||
std::map<std::string, std::vector<DebuggerView*>> type_to_widgets;
|
||||
for (const auto& [unique_name, widget] : m_widgets)
|
||||
type_to_widgets[widget->metaObject()->className()].emplace_back(widget.get());
|
||||
|
||||
for (auto& [type, widgets] : type_to_widgets)
|
||||
{
|
||||
u32 primary_widgets = 0;
|
||||
|
||||
// Make sure at most one widget is marked as primary.
|
||||
for (DebuggerView* widget : widgets)
|
||||
{
|
||||
if (widget->isPrimary())
|
||||
{
|
||||
if (primary_widgets != 0)
|
||||
widget->setPrimary(false);
|
||||
|
||||
primary_widgets++;
|
||||
}
|
||||
}
|
||||
|
||||
// If none of the widgets were marked as primary, just set the first one
|
||||
// as the primary one.
|
||||
if (primary_widgets == 0)
|
||||
widgets[0]->setPrimary(true);
|
||||
}
|
||||
}
|
||||
|
||||
void DockLayout::setupDefaultLayout()
|
||||
{
|
||||
pxAssert(m_is_active);
|
||||
|
||||
if (!g_debugger_window)
|
||||
return;
|
||||
|
||||
const DockTables::DefaultDockLayout* base_layout = DockTables::defaultLayout(m_base_layout);
|
||||
if (!base_layout)
|
||||
return;
|
||||
|
||||
std::vector<KDDockWidgets::QtWidgets::DockWidget*> groups(base_layout->groups.size(), nullptr);
|
||||
|
||||
for (const DockTables::DefaultDockWidgetDescription& dock_description : base_layout->widgets)
|
||||
{
|
||||
const DockTables::DefaultDockGroupDescription& group =
|
||||
base_layout->groups[static_cast<u32>(dock_description.group)];
|
||||
|
||||
DebuggerView* widget = nullptr;
|
||||
for (auto& [unique_name, test_widget] : m_widgets)
|
||||
if (test_widget->metaObject()->className() == dock_description.type)
|
||||
widget = test_widget;
|
||||
|
||||
if (!widget)
|
||||
continue;
|
||||
|
||||
auto view = static_cast<KDDockWidgets::QtWidgets::DockWidget*>(
|
||||
KDDockWidgets::Config::self().viewFactory()->createDockWidget(widget->uniqueName()));
|
||||
view->setWidget(widget);
|
||||
|
||||
if (!groups[static_cast<u32>(dock_description.group)])
|
||||
{
|
||||
KDDockWidgets::QtWidgets::DockWidget* parent = nullptr;
|
||||
if (group.parent != DockTables::DefaultDockGroup::ROOT)
|
||||
parent = groups[static_cast<u32>(group.parent)];
|
||||
|
||||
g_debugger_window->addDockWidget(view, group.location, parent);
|
||||
|
||||
groups[static_cast<u32>(dock_description.group)] = view;
|
||||
}
|
||||
else
|
||||
{
|
||||
groups[static_cast<u32>(dock_description.group)]->addDockWidgetAsTab(view);
|
||||
}
|
||||
}
|
||||
|
||||
for (KDDockWidgets::Core::Group* group : KDDockWidgets::DockRegistry::self()->groups())
|
||||
group->setCurrentTabIndex(0);
|
||||
}
|
||||
|
||||
std::pair<QString, u64> DockLayout::generateNewUniqueName(const char* type)
|
||||
{
|
||||
QString name;
|
||||
u64 id;
|
||||
|
||||
do
|
||||
{
|
||||
if (m_next_id == INT_MAX)
|
||||
return {QString(), 0};
|
||||
|
||||
id = m_next_id;
|
||||
name = QStringLiteral("%1-%2").arg(type).arg(static_cast<qulonglong>(m_next_id));
|
||||
m_next_id++;
|
||||
} while (hasDebuggerView(name));
|
||||
|
||||
return {name, id};
|
||||
}
|
||||
163
pcsx2-qt/Debugger/Docking/DockLayout.h
Normal file
163
pcsx2-qt/Debugger/Docking/DockLayout.h
Normal file
@@ -0,0 +1,163 @@
|
||||
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
|
||||
// SPDX-License-Identifier: GPL-3.0+
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "Debugger/Docking/DockTables.h"
|
||||
|
||||
#include "DebugTools/DebugInterface.h"
|
||||
|
||||
#include <kddockwidgets/MainWindow.h>
|
||||
#include <kddockwidgets/DockWidget.h>
|
||||
|
||||
#include <QtCore/QPointer>
|
||||
|
||||
class DebuggerView;
|
||||
class DebuggerWindow;
|
||||
|
||||
extern const char* DEBUGGER_LAYOUT_FILE_FORMAT;
|
||||
|
||||
// Increment this whenever there is a breaking change to the JSON format.
|
||||
extern const u32 DEBUGGER_LAYOUT_FILE_VERSION_MAJOR;
|
||||
|
||||
// Increment this whenever there is a non-breaking change to the JSON format.
|
||||
extern const u32 DEBUGGER_LAYOUT_FILE_VERSION_MINOR;
|
||||
|
||||
class DockLayout
|
||||
{
|
||||
public:
|
||||
using Index = size_t;
|
||||
static const constexpr Index INVALID_INDEX = SIZE_MAX;
|
||||
|
||||
enum LoadResult
|
||||
{
|
||||
SUCCESS,
|
||||
FILE_NOT_FOUND,
|
||||
INVALID_FORMAT,
|
||||
MAJOR_VERSION_MISMATCH,
|
||||
DEFAULT_LAYOUT_HASH_MISMATCH,
|
||||
CONFLICTING_NAME
|
||||
};
|
||||
|
||||
// Create a layout based on a default layout.
|
||||
DockLayout(
|
||||
QString name,
|
||||
BreakPointCpu cpu,
|
||||
bool is_default,
|
||||
const std::string& base_name,
|
||||
DockLayout::Index index);
|
||||
|
||||
// Create a new blank layout.
|
||||
DockLayout(
|
||||
QString name,
|
||||
BreakPointCpu cpu,
|
||||
bool is_default,
|
||||
DockLayout::Index index);
|
||||
|
||||
// Clone an existing layout.
|
||||
DockLayout(
|
||||
QString name,
|
||||
BreakPointCpu cpu,
|
||||
bool is_default,
|
||||
const DockLayout& layout_to_clone,
|
||||
DockLayout::Index index);
|
||||
|
||||
// Load a layout from a file.
|
||||
DockLayout(
|
||||
const std::string& path,
|
||||
LoadResult& result,
|
||||
DockLayout::Index& index_last_session,
|
||||
DockLayout::Index index);
|
||||
|
||||
~DockLayout();
|
||||
|
||||
DockLayout(const DockLayout& rhs) = delete;
|
||||
DockLayout& operator=(const DockLayout& rhs) = delete;
|
||||
|
||||
DockLayout(DockLayout&& rhs) = default;
|
||||
DockLayout& operator=(DockLayout&&) = default;
|
||||
|
||||
const QString& name() const;
|
||||
void setName(QString name);
|
||||
|
||||
BreakPointCpu cpu() const;
|
||||
void setCpu(BreakPointCpu cpu);
|
||||
|
||||
bool isDefault() const;
|
||||
|
||||
// Tear down and save the state of all the dock widgets from this layout.
|
||||
void freeze();
|
||||
|
||||
// Restore the state of all the dock widgets from this layout.
|
||||
void thaw();
|
||||
|
||||
bool canReset();
|
||||
void reset();
|
||||
|
||||
KDDockWidgets::Core::DockWidget* createDockWidget(const QString& name);
|
||||
void updateDockWidgetTitles();
|
||||
|
||||
const std::map<QString, QPointer<DebuggerView>>& debuggerViews();
|
||||
bool hasDebuggerView(const QString& unique_name);
|
||||
size_t countDebuggerViewsOfType(const char* type);
|
||||
void createDebuggerView(const std::string& type);
|
||||
void recreateDebuggerView(const QString& unique_name);
|
||||
void destroyDebuggerView(const QString& unique_name);
|
||||
void setPrimaryDebuggerView(DebuggerView* widget, bool is_primary);
|
||||
|
||||
void deleteFile();
|
||||
|
||||
bool save(DockLayout::Index layout_index);
|
||||
|
||||
private:
|
||||
void load(
|
||||
const std::string& path,
|
||||
DockLayout::LoadResult& result,
|
||||
DockLayout::Index& index_last_session);
|
||||
|
||||
// Make sure there is only a single primary debugger view of each type.
|
||||
void validatePrimaryDebuggerViews();
|
||||
|
||||
void setupDefaultLayout();
|
||||
|
||||
std::pair<QString, u64> generateNewUniqueName(const char* type);
|
||||
|
||||
// The name displayed in the user interface. Also used to determine the
|
||||
// file name for the layout file.
|
||||
QString m_name;
|
||||
|
||||
// The default target for dock widgets in this layout. This can be
|
||||
// overriden on a per-widget basis.
|
||||
BreakPointCpu m_cpu;
|
||||
|
||||
// Is this one of the default layouts?
|
||||
bool m_is_default = false;
|
||||
|
||||
// A counter used to generate new unique names for dock widgets.
|
||||
u64 m_next_id = 0;
|
||||
|
||||
// The name of the default layout which this layout was based on. This will
|
||||
// be used if the m_geometry variable above is empty.
|
||||
std::string m_base_layout;
|
||||
|
||||
// The state of all the toolbars, saved and restored using
|
||||
// QMainWindow::saveState and QMainWindow::restoreState respectively.
|
||||
QByteArray m_toolbars;
|
||||
|
||||
// All the dock widgets currently open in this layout. If this is the active
|
||||
// layout then these will be owned by the docking system, otherwise they
|
||||
// won't be and will need to be cleaned up separately.
|
||||
std::map<QString, QPointer<DebuggerView>> m_widgets;
|
||||
|
||||
// The geometry of all the dock widgets, converted to JSON by the
|
||||
// LayoutSaver class from KDDockWidgets.
|
||||
QByteArray m_geometry;
|
||||
|
||||
// The absolute file path of the corresponding layout file as it currently
|
||||
// exists exists on disk, or empty if no such file exists.
|
||||
std::string m_layout_file_path;
|
||||
|
||||
// If this layout is the currently selected layout this will be true,
|
||||
// otherwise it will be false.
|
||||
bool m_is_active = false;
|
||||
};
|
||||
874
pcsx2-qt/Debugger/Docking/DockManager.cpp
Normal file
874
pcsx2-qt/Debugger/Docking/DockManager.cpp
Normal file
@@ -0,0 +1,874 @@
|
||||
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
|
||||
// SPDX-License-Identifier: GPL-3.0+
|
||||
|
||||
#include "DockManager.h"
|
||||
|
||||
#include "Debugger/DebuggerView.h"
|
||||
#include "Debugger/DebuggerWindow.h"
|
||||
#include "Debugger/Docking/DockTables.h"
|
||||
#include "Debugger/Docking/DockViews.h"
|
||||
#include "Debugger/Docking/DropIndicators.h"
|
||||
#include "Debugger/Docking/LayoutEditorDialog.h"
|
||||
#include "Debugger/Docking/NoLayoutsWidget.h"
|
||||
|
||||
#include "common/Assertions.h"
|
||||
#include "common/FileSystem.h"
|
||||
#include "common/StringUtil.h"
|
||||
#include "common/Path.h"
|
||||
|
||||
#include <kddockwidgets/Config.h>
|
||||
#include <kddockwidgets/core/Group.h>
|
||||
#include <kddockwidgets/core/Stack.h>
|
||||
#include <kddockwidgets/core/indicators/SegmentedDropIndicatorOverlay.h>
|
||||
#include <kddockwidgets/qtwidgets/Stack.h>
|
||||
|
||||
#include <QtCore/QTimer>
|
||||
#include <QtWidgets/QMessageBox>
|
||||
#include <QtWidgets/QProxyStyle>
|
||||
#include <QtWidgets/QStyleFactory>
|
||||
|
||||
DockManager::DockManager(QObject* parent)
|
||||
: QObject(parent)
|
||||
{
|
||||
QTimer* autosave_timer = new QTimer(this);
|
||||
connect(autosave_timer, &QTimer::timeout, this, &DockManager::saveCurrentLayout);
|
||||
autosave_timer->start(60 * 1000);
|
||||
}
|
||||
|
||||
void DockManager::configureDockingSystem()
|
||||
{
|
||||
std::string indicator_style = Host::GetBaseStringSettingValue(
|
||||
"Debugger/UserInterface", "DropIndicatorStyle", "Classic");
|
||||
|
||||
if (indicator_style == "Segmented" || indicator_style == "Minimalistic")
|
||||
{
|
||||
KDDockWidgets::Core::ViewFactory::s_dropIndicatorType = KDDockWidgets::DropIndicatorType::Segmented;
|
||||
DockSegmentedDropIndicatorOverlay::s_indicator_style = indicator_style;
|
||||
}
|
||||
else
|
||||
{
|
||||
KDDockWidgets::Core::ViewFactory::s_dropIndicatorType = KDDockWidgets::DropIndicatorType::Classic;
|
||||
}
|
||||
|
||||
static bool done = false;
|
||||
if (done)
|
||||
return;
|
||||
|
||||
KDDockWidgets::initFrontend(KDDockWidgets::FrontendType::QtWidgets);
|
||||
|
||||
KDDockWidgets::Config& config = KDDockWidgets::Config::self();
|
||||
|
||||
config.setFlags(
|
||||
KDDockWidgets::Config::Flag_HideTitleBarWhenTabsVisible |
|
||||
KDDockWidgets::Config::Flag_AlwaysShowTabs |
|
||||
KDDockWidgets::Config::Flag_AllowReorderTabs |
|
||||
KDDockWidgets::Config::Flag_TitleBarIsFocusable);
|
||||
|
||||
// We set this flag regardless of whether or not the windowing system
|
||||
// supports compositing since it's only used by the built-in docking
|
||||
// indicator, and we only fall back to that if compositing is disabled.
|
||||
config.setInternalFlags(KDDockWidgets::Config::InternalFlag_DisableTranslucency);
|
||||
|
||||
config.setDockWidgetFactoryFunc(&DockManager::dockWidgetFactory);
|
||||
config.setViewFactory(new DockViewFactory());
|
||||
config.setDragAboutToStartFunc(&DockManager::dragAboutToStart);
|
||||
config.setStartDragDistance(std::max(QApplication::startDragDistance(), 32));
|
||||
|
||||
done = true;
|
||||
}
|
||||
|
||||
bool DockManager::deleteLayout(DockLayout::Index layout_index)
|
||||
{
|
||||
pxAssertRel(layout_index != DockLayout::INVALID_INDEX,
|
||||
"DockManager::deleteLayout called with INVALID_INDEX.");
|
||||
|
||||
if (layout_index == m_current_layout)
|
||||
{
|
||||
DockLayout::Index other_layout = DockLayout::INVALID_INDEX;
|
||||
if (layout_index + 1 < m_layouts.size())
|
||||
other_layout = layout_index + 1;
|
||||
else if (layout_index > 0)
|
||||
other_layout = layout_index - 1;
|
||||
|
||||
switchToLayout(other_layout);
|
||||
}
|
||||
|
||||
m_layouts.at(layout_index).deleteFile();
|
||||
m_layouts.erase(m_layouts.begin() + layout_index);
|
||||
|
||||
// All the layouts after the one being deleted have been shifted over by
|
||||
// one, so adjust the current layout index accordingly.
|
||||
if (m_current_layout > layout_index && m_current_layout != DockLayout::INVALID_INDEX)
|
||||
m_current_layout--;
|
||||
|
||||
if (m_layouts.empty() && g_debugger_window)
|
||||
{
|
||||
NoLayoutsWidget* widget = new NoLayoutsWidget;
|
||||
connect(widget->createDefaultLayoutsButton(), &QPushButton::clicked, this, &DockManager::resetAllLayouts);
|
||||
|
||||
KDDockWidgets::QtWidgets::DockWidget* dock = new KDDockWidgets::QtWidgets::DockWidget("placeholder");
|
||||
dock->setTitle(tr("No Layouts"));
|
||||
dock->setWidget(widget);
|
||||
g_debugger_window->addDockWidget(dock, KDDockWidgets::Location_OnTop);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void DockManager::switchToLayout(DockLayout::Index layout_index, bool blink_tab)
|
||||
{
|
||||
if (layout_index != m_current_layout)
|
||||
{
|
||||
if (m_current_layout != DockLayout::INVALID_INDEX)
|
||||
{
|
||||
DockLayout& layout = m_layouts.at(m_current_layout);
|
||||
layout.freeze();
|
||||
layout.save(m_current_layout);
|
||||
}
|
||||
|
||||
// Clear out the existing positions of toolbars so they don't affect
|
||||
// where new toolbars appear for other layouts.
|
||||
if (g_debugger_window)
|
||||
g_debugger_window->clearToolBarState();
|
||||
|
||||
updateToolBarLockState();
|
||||
|
||||
m_current_layout = layout_index;
|
||||
|
||||
if (m_current_layout != DockLayout::INVALID_INDEX)
|
||||
{
|
||||
DockLayout& layout = m_layouts.at(m_current_layout);
|
||||
layout.thaw();
|
||||
|
||||
int tab_index = static_cast<int>(layout_index);
|
||||
if (tab_index >= 0 && m_menu_bar)
|
||||
m_menu_bar->onCurrentLayoutChanged(layout_index);
|
||||
}
|
||||
}
|
||||
|
||||
if (blink_tab && m_menu_bar)
|
||||
m_menu_bar->startBlink(m_current_layout);
|
||||
}
|
||||
|
||||
bool DockManager::switchToLayoutWithCPU(BreakPointCpu cpu, bool blink_tab)
|
||||
{
|
||||
// Don't interrupt the user if the current layout already has the right CPU.
|
||||
if (m_current_layout != DockLayout::INVALID_INDEX && m_layouts.at(m_current_layout).cpu() == cpu)
|
||||
{
|
||||
switchToLayout(m_current_layout, blink_tab);
|
||||
return true;
|
||||
}
|
||||
|
||||
for (DockLayout::Index i = 0; i < m_layouts.size(); i++)
|
||||
{
|
||||
if (m_layouts[i].cpu() == cpu)
|
||||
{
|
||||
switchToLayout(i, blink_tab);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void DockManager::loadLayouts()
|
||||
{
|
||||
m_layouts.clear();
|
||||
|
||||
// Load the layouts.
|
||||
FileSystem::FindResultsArray files;
|
||||
FileSystem::FindFiles(
|
||||
EmuFolders::DebuggerLayouts.c_str(),
|
||||
"*.json",
|
||||
FILESYSTEM_FIND_FILES | FILESYSTEM_FIND_HIDDEN_FILES,
|
||||
&files);
|
||||
|
||||
bool needs_reset = false;
|
||||
std::vector<DockLayout::Index> indices_last_session;
|
||||
|
||||
for (const FILESYSTEM_FIND_DATA& ffd : files)
|
||||
{
|
||||
DockLayout::LoadResult result;
|
||||
DockLayout::Index index_last_session = DockLayout::INVALID_INDEX;
|
||||
DockLayout::Index index =
|
||||
createLayout(ffd.FileName, result, index_last_session);
|
||||
|
||||
DockLayout& layout = m_layouts.at(index);
|
||||
|
||||
// Try to make sure the layout has a unique name.
|
||||
const QString& name = layout.name();
|
||||
QString new_name = name;
|
||||
if (result == DockLayout::SUCCESS || result == DockLayout::DEFAULT_LAYOUT_HASH_MISMATCH)
|
||||
{
|
||||
for (int i = 2; hasNameConflict(new_name, index) && i < 100; i++)
|
||||
{
|
||||
if (i == 99)
|
||||
{
|
||||
result = DockLayout::CONFLICTING_NAME;
|
||||
break;
|
||||
}
|
||||
|
||||
new_name = QString("%1 #%2").arg(name).arg(i);
|
||||
}
|
||||
}
|
||||
|
||||
needs_reset |= result != DockLayout::SUCCESS;
|
||||
|
||||
if (result != DockLayout::SUCCESS && result != DockLayout::DEFAULT_LAYOUT_HASH_MISMATCH)
|
||||
{
|
||||
deleteLayout(index);
|
||||
|
||||
// Only delete the file if we've identified that it's actually a
|
||||
// layout file.
|
||||
if (result == DockLayout::MAJOR_VERSION_MISMATCH || result == DockLayout::CONFLICTING_NAME)
|
||||
FileSystem::DeleteFilePath(ffd.FileName.c_str());
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (new_name != name)
|
||||
{
|
||||
layout.setName(new_name);
|
||||
layout.save(index);
|
||||
}
|
||||
|
||||
indices_last_session.emplace_back(index_last_session);
|
||||
}
|
||||
|
||||
// Make sure the layouts remain in the same order they were in previously.
|
||||
std::vector<size_t> layout_indices;
|
||||
for (size_t i = 0; i < m_layouts.size(); i++)
|
||||
layout_indices.emplace_back(i);
|
||||
|
||||
std::sort(layout_indices.begin(), layout_indices.end(),
|
||||
[&indices_last_session](size_t lhs, size_t rhs) {
|
||||
DockLayout::Index lhs_index_last_session = indices_last_session.at(lhs);
|
||||
DockLayout::Index rhs_index_last_session = indices_last_session.at(rhs);
|
||||
return lhs_index_last_session < rhs_index_last_session;
|
||||
});
|
||||
|
||||
bool order_changed = false;
|
||||
std::vector<DockLayout> sorted_layouts;
|
||||
for (size_t i = 0; i < layout_indices.size(); i++)
|
||||
{
|
||||
if (i != indices_last_session[layout_indices[i]])
|
||||
order_changed = true;
|
||||
|
||||
sorted_layouts.emplace_back(std::move(m_layouts[layout_indices[i]]));
|
||||
}
|
||||
|
||||
m_layouts = std::move(sorted_layouts);
|
||||
|
||||
if (m_layouts.empty() || needs_reset)
|
||||
resetDefaultLayouts();
|
||||
else
|
||||
updateLayoutSwitcher();
|
||||
|
||||
// Make sure the indices in the existing layout files match up with the
|
||||
// indices of any new layouts.
|
||||
if (order_changed)
|
||||
saveLayouts();
|
||||
}
|
||||
|
||||
bool DockManager::saveLayouts()
|
||||
{
|
||||
for (DockLayout::Index i = 0; i < m_layouts.size(); i++)
|
||||
if (!m_layouts[i].save(i))
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool DockManager::saveCurrentLayout()
|
||||
{
|
||||
if (m_current_layout == DockLayout::INVALID_INDEX)
|
||||
return true;
|
||||
|
||||
return m_layouts.at(m_current_layout).save(m_current_layout);
|
||||
}
|
||||
|
||||
void DockManager::resetAllLayouts()
|
||||
{
|
||||
switchToLayout(DockLayout::INVALID_INDEX);
|
||||
|
||||
for (DockLayout& layout : m_layouts)
|
||||
layout.deleteFile();
|
||||
|
||||
m_layouts.clear();
|
||||
|
||||
for (const DockTables::DefaultDockLayout& layout : DockTables::DEFAULT_DOCK_LAYOUTS)
|
||||
{
|
||||
QString name = QCoreApplication::translate("DebuggerLayout", layout.name.c_str());
|
||||
createLayout(name, layout.cpu, true, layout.name);
|
||||
}
|
||||
|
||||
switchToLayout(0);
|
||||
updateLayoutSwitcher();
|
||||
saveLayouts();
|
||||
}
|
||||
|
||||
void DockManager::resetDefaultLayouts()
|
||||
{
|
||||
switchToLayout(DockLayout::INVALID_INDEX);
|
||||
|
||||
std::vector<DockLayout> old_layouts = std::move(m_layouts);
|
||||
m_layouts = std::vector<DockLayout>();
|
||||
|
||||
for (const DockTables::DefaultDockLayout& layout : DockTables::DEFAULT_DOCK_LAYOUTS)
|
||||
{
|
||||
QString name = QCoreApplication::translate("DebuggerLayout", layout.name.c_str());
|
||||
createLayout(name, layout.cpu, true, layout.name);
|
||||
}
|
||||
|
||||
for (DockLayout& layout : old_layouts)
|
||||
if (!layout.isDefault())
|
||||
m_layouts.emplace_back(std::move(layout));
|
||||
else
|
||||
layout.deleteFile();
|
||||
|
||||
switchToLayout(0);
|
||||
updateLayoutSwitcher();
|
||||
saveLayouts();
|
||||
}
|
||||
|
||||
void DockManager::createToolsMenu(QMenu* menu)
|
||||
{
|
||||
menu->clear();
|
||||
|
||||
if (m_current_layout == DockLayout::INVALID_INDEX || !g_debugger_window)
|
||||
return;
|
||||
|
||||
for (QToolBar* widget : g_debugger_window->findChildren<QToolBar*>())
|
||||
{
|
||||
QAction* action = menu->addAction(widget->windowTitle());
|
||||
action->setText(widget->windowTitle());
|
||||
action->setCheckable(true);
|
||||
action->setChecked(widget->isVisible());
|
||||
connect(action, &QAction::triggered, this, [widget]() {
|
||||
widget->setVisible(!widget->isVisible());
|
||||
});
|
||||
menu->addAction(action);
|
||||
}
|
||||
}
|
||||
|
||||
void DockManager::createWindowsMenu(QMenu* menu)
|
||||
{
|
||||
menu->clear();
|
||||
|
||||
if (m_current_layout == DockLayout::INVALID_INDEX)
|
||||
return;
|
||||
|
||||
DockLayout& layout = m_layouts.at(m_current_layout);
|
||||
|
||||
// Create a menu that allows for multiple dock widgets of the same type to
|
||||
// be opened.
|
||||
QMenu* add_another_menu = menu->addMenu(tr("Add Another..."));
|
||||
|
||||
std::vector<DebuggerView*> add_another_widgets;
|
||||
std::set<std::string> add_another_types;
|
||||
for (const auto& [unique_name, widget] : layout.debuggerViews())
|
||||
{
|
||||
std::string type = widget->metaObject()->className();
|
||||
|
||||
if (widget->supportsMultipleInstances() && !add_another_types.contains(type))
|
||||
{
|
||||
add_another_widgets.emplace_back(widget);
|
||||
add_another_types.emplace(type);
|
||||
}
|
||||
}
|
||||
|
||||
std::sort(add_another_widgets.begin(), add_another_widgets.end(),
|
||||
[](const DebuggerView* lhs, const DebuggerView* rhs) {
|
||||
if (lhs->displayNameWithoutSuffix() == rhs->displayNameWithoutSuffix())
|
||||
return lhs->displayNameSuffixNumber() < rhs->displayNameSuffixNumber();
|
||||
|
||||
return lhs->displayNameWithoutSuffix() < rhs->displayNameWithoutSuffix();
|
||||
});
|
||||
|
||||
for (DebuggerView* widget : add_another_widgets)
|
||||
{
|
||||
const char* type = widget->metaObject()->className();
|
||||
|
||||
const auto description_iterator = DockTables::DEBUGGER_VIEWS.find(type);
|
||||
pxAssert(description_iterator != DockTables::DEBUGGER_VIEWS.end());
|
||||
|
||||
QAction* action = add_another_menu->addAction(description_iterator->second.display_name);
|
||||
connect(action, &QAction::triggered, this, [this, type]() {
|
||||
if (m_current_layout == DockLayout::INVALID_INDEX)
|
||||
return;
|
||||
|
||||
m_layouts.at(m_current_layout).createDebuggerView(type);
|
||||
});
|
||||
}
|
||||
|
||||
if (add_another_widgets.empty())
|
||||
add_another_menu->setDisabled(true);
|
||||
|
||||
menu->addSeparator();
|
||||
|
||||
struct DebuggerViewToggle
|
||||
{
|
||||
QString display_name;
|
||||
std::optional<int> suffix_number;
|
||||
QAction* action;
|
||||
};
|
||||
|
||||
std::vector<DebuggerViewToggle> toggles;
|
||||
std::set<std::string> toggle_types;
|
||||
|
||||
// Create a menu item for each open debugger view.
|
||||
for (const auto& pair : layout.debuggerViews())
|
||||
{
|
||||
DebuggerView* widget = pair.second.get();
|
||||
QAction* action = new QAction(menu);
|
||||
action->setText(widget->displayName());
|
||||
action->setCheckable(true);
|
||||
action->setChecked(true);
|
||||
connect(action, &QAction::triggered, this, [this, unique_name = pair.first]() {
|
||||
if (m_current_layout == DockLayout::INVALID_INDEX)
|
||||
return;
|
||||
|
||||
m_layouts.at(m_current_layout).destroyDebuggerView(unique_name);
|
||||
});
|
||||
|
||||
DebuggerViewToggle& toggle = toggles.emplace_back();
|
||||
toggle.display_name = widget->displayNameWithoutSuffix();
|
||||
toggle.suffix_number = widget->displayNameSuffixNumber();
|
||||
toggle.action = action;
|
||||
|
||||
toggle_types.emplace(widget->metaObject()->className());
|
||||
}
|
||||
|
||||
// Create menu items to open debugger views without any open instances.
|
||||
for (const auto& pair : DockTables::DEBUGGER_VIEWS)
|
||||
{
|
||||
const std::string& type = pair.first;
|
||||
const DockTables::DebuggerViewDescription& desc = pair.second;
|
||||
if (!toggle_types.contains(type))
|
||||
{
|
||||
QString display_name = QCoreApplication::translate("DebuggerView", desc.display_name);
|
||||
|
||||
QAction* action = new QAction(menu);
|
||||
action->setText(display_name);
|
||||
action->setCheckable(true);
|
||||
action->setChecked(false);
|
||||
connect(action, &QAction::triggered, this, [this, type]() {
|
||||
if (m_current_layout == DockLayout::INVALID_INDEX)
|
||||
return;
|
||||
|
||||
m_layouts.at(m_current_layout).createDebuggerView(type);
|
||||
});
|
||||
|
||||
DebuggerViewToggle& toggle = toggles.emplace_back();
|
||||
toggle.display_name = display_name;
|
||||
toggle.suffix_number = std::nullopt;
|
||||
toggle.action = action;
|
||||
}
|
||||
}
|
||||
|
||||
std::sort(toggles.begin(), toggles.end(),
|
||||
[](const DebuggerViewToggle& lhs, const DebuggerViewToggle& rhs) {
|
||||
if (lhs.display_name == rhs.display_name)
|
||||
return lhs.suffix_number < rhs.suffix_number;
|
||||
|
||||
return lhs.display_name < rhs.display_name;
|
||||
});
|
||||
|
||||
for (const DebuggerViewToggle& toggle : toggles)
|
||||
menu->addAction(toggle.action);
|
||||
}
|
||||
|
||||
QWidget* DockManager::createMenuBar(QWidget* original_menu_bar)
|
||||
{
|
||||
pxAssert(!m_menu_bar);
|
||||
|
||||
m_menu_bar = new DockMenuBar(original_menu_bar);
|
||||
|
||||
connect(m_menu_bar, &DockMenuBar::currentLayoutChanged, this, [this](DockLayout::Index layout_index) {
|
||||
if (layout_index >= m_layouts.size())
|
||||
return;
|
||||
|
||||
switchToLayout(layout_index);
|
||||
});
|
||||
connect(m_menu_bar, &DockMenuBar::newButtonClicked, this, &DockManager::newLayoutClicked);
|
||||
connect(m_menu_bar, &DockMenuBar::layoutMoved, this, &DockManager::layoutSwitcherTabMoved);
|
||||
connect(m_menu_bar, &DockMenuBar::lockButtonToggled, this, &DockManager::setLayoutLockedAndSaveSetting);
|
||||
connect(m_menu_bar, &DockMenuBar::layoutSwitcherContextMenuRequested,
|
||||
this, &DockManager::openLayoutSwitcherContextMenu);
|
||||
|
||||
updateLayoutSwitcher();
|
||||
|
||||
bool layout_locked = Host::GetBaseBoolSettingValue("Debugger/UserInterface", "LayoutLocked", true);
|
||||
setLayoutLocked(layout_locked, false);
|
||||
|
||||
return m_menu_bar;
|
||||
}
|
||||
|
||||
void DockManager::updateLayoutSwitcher()
|
||||
{
|
||||
if (m_menu_bar)
|
||||
m_menu_bar->updateLayoutSwitcher(m_current_layout, m_layouts);
|
||||
}
|
||||
|
||||
void DockManager::newLayoutClicked()
|
||||
{
|
||||
// The plus button has just been made the current tab, so set it back to the
|
||||
// one corresponding to the current layout again.
|
||||
if (m_menu_bar)
|
||||
m_menu_bar->onCurrentLayoutChanged(m_current_layout);
|
||||
|
||||
auto name_validator = [this](const QString& name) {
|
||||
return !hasNameConflict(name, DockLayout::INVALID_INDEX);
|
||||
};
|
||||
|
||||
bool can_clone_current_layout = m_current_layout != DockLayout::INVALID_INDEX;
|
||||
|
||||
QPointer<LayoutEditorDialog> dialog = new LayoutEditorDialog(
|
||||
name_validator, can_clone_current_layout, g_debugger_window);
|
||||
|
||||
if (dialog->exec() == QDialog::Accepted && name_validator(dialog->name()))
|
||||
{
|
||||
DockLayout::Index new_layout = DockLayout::INVALID_INDEX;
|
||||
|
||||
const auto [mode, index] = dialog->initialState();
|
||||
switch (mode)
|
||||
{
|
||||
case LayoutEditorDialog::DEFAULT_LAYOUT:
|
||||
{
|
||||
const DockTables::DefaultDockLayout& default_layout = DockTables::DEFAULT_DOCK_LAYOUTS.at(index);
|
||||
new_layout = createLayout(dialog->name(), dialog->cpu(), false, default_layout.name);
|
||||
break;
|
||||
}
|
||||
case LayoutEditorDialog::BLANK_LAYOUT:
|
||||
{
|
||||
new_layout = createLayout(dialog->name(), dialog->cpu(), false);
|
||||
break;
|
||||
}
|
||||
case LayoutEditorDialog::CLONE_LAYOUT:
|
||||
{
|
||||
if (m_current_layout == DockLayout::INVALID_INDEX)
|
||||
break;
|
||||
|
||||
DockLayout::Index old_layout = m_current_layout;
|
||||
|
||||
// Freeze the current layout so we can copy the geometry.
|
||||
switchToLayout(DockLayout::INVALID_INDEX);
|
||||
|
||||
new_layout = createLayout(dialog->name(), dialog->cpu(), false, m_layouts.at(old_layout));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (new_layout != DockLayout::INVALID_INDEX)
|
||||
{
|
||||
updateLayoutSwitcher();
|
||||
switchToLayout(new_layout);
|
||||
}
|
||||
}
|
||||
|
||||
delete dialog.get();
|
||||
}
|
||||
|
||||
void DockManager::openLayoutSwitcherContextMenu(const QPoint& pos, QTabBar* layout_switcher)
|
||||
{
|
||||
DockLayout::Index layout_index = static_cast<DockLayout::Index>(layout_switcher->tabAt(pos));
|
||||
if (layout_index >= m_layouts.size())
|
||||
return;
|
||||
|
||||
DockLayout& layout = m_layouts[layout_index];
|
||||
|
||||
QMenu* menu = new QMenu(layout_switcher);
|
||||
menu->setAttribute(Qt::WA_DeleteOnClose);
|
||||
|
||||
QAction* edit_action = menu->addAction(tr("Edit Layout"));
|
||||
connect(edit_action, &QAction::triggered, [this, layout_index]() {
|
||||
editLayoutClicked(layout_index);
|
||||
});
|
||||
|
||||
QAction* reset_action = menu->addAction(tr("Reset Layout"));
|
||||
reset_action->setEnabled(layout.canReset());
|
||||
reset_action->connect(reset_action, &QAction::triggered, [this, layout_index]() {
|
||||
resetLayoutClicked(layout_index);
|
||||
});
|
||||
|
||||
QAction* delete_action = menu->addAction(tr("Delete Layout"));
|
||||
connect(delete_action, &QAction::triggered, [this, layout_index]() {
|
||||
deleteLayoutClicked(layout_index);
|
||||
});
|
||||
|
||||
menu->popup(layout_switcher->mapToGlobal(pos));
|
||||
}
|
||||
|
||||
void DockManager::editLayoutClicked(DockLayout::Index layout_index)
|
||||
{
|
||||
if (layout_index >= m_layouts.size())
|
||||
return;
|
||||
|
||||
DockLayout& layout = m_layouts[layout_index];
|
||||
|
||||
auto name_validator = [this, layout_index](const QString& name) {
|
||||
return !hasNameConflict(name, layout_index);
|
||||
};
|
||||
|
||||
QPointer<LayoutEditorDialog> dialog = new LayoutEditorDialog(
|
||||
layout.name(), layout.cpu(), name_validator, g_debugger_window);
|
||||
|
||||
if (dialog->exec() != QDialog::Accepted || !name_validator(dialog->name()))
|
||||
return;
|
||||
|
||||
layout.setName(dialog->name());
|
||||
layout.setCpu(dialog->cpu());
|
||||
|
||||
layout.save(layout_index);
|
||||
|
||||
delete dialog.get();
|
||||
|
||||
updateLayoutSwitcher();
|
||||
}
|
||||
|
||||
void DockManager::resetLayoutClicked(DockLayout::Index layout_index)
|
||||
{
|
||||
if (layout_index >= m_layouts.size())
|
||||
return;
|
||||
|
||||
DockLayout& layout = m_layouts[layout_index];
|
||||
if (!layout.canReset())
|
||||
return;
|
||||
|
||||
QString text = tr("Are you sure you want to reset layout '%1'?").arg(layout.name());
|
||||
if (QMessageBox::question(g_debugger_window, tr("Confirmation"), text) != QMessageBox::Yes)
|
||||
return;
|
||||
|
||||
bool current_layout = layout_index == m_current_layout;
|
||||
|
||||
if (current_layout)
|
||||
switchToLayout(DockLayout::INVALID_INDEX);
|
||||
|
||||
layout.reset();
|
||||
layout.save(layout_index);
|
||||
|
||||
if (current_layout)
|
||||
switchToLayout(layout_index);
|
||||
}
|
||||
|
||||
void DockManager::deleteLayoutClicked(DockLayout::Index layout_index)
|
||||
{
|
||||
if (layout_index >= m_layouts.size())
|
||||
return;
|
||||
|
||||
DockLayout& layout = m_layouts[layout_index];
|
||||
|
||||
QString text = tr("Are you sure you want to delete layout '%1'?").arg(layout.name());
|
||||
if (QMessageBox::question(g_debugger_window, tr("Confirmation"), text) != QMessageBox::Yes)
|
||||
return;
|
||||
|
||||
deleteLayout(layout_index);
|
||||
updateLayoutSwitcher();
|
||||
}
|
||||
|
||||
void DockManager::layoutSwitcherTabMoved(DockLayout::Index from_index, DockLayout::Index to_index)
|
||||
{
|
||||
if (from_index >= m_layouts.size() || to_index >= m_layouts.size())
|
||||
{
|
||||
// This happens when the user tries to move a layout to the right of the
|
||||
// plus button.
|
||||
updateLayoutSwitcher();
|
||||
return;
|
||||
}
|
||||
|
||||
DockLayout& from_layout = m_layouts[from_index];
|
||||
DockLayout& to_layout = m_layouts[to_index];
|
||||
|
||||
std::swap(from_layout, to_layout);
|
||||
|
||||
from_layout.save(from_index);
|
||||
to_layout.save(to_index);
|
||||
|
||||
if (from_index == m_current_layout)
|
||||
m_current_layout = to_index;
|
||||
else if (to_index == m_current_layout)
|
||||
m_current_layout = from_index;
|
||||
}
|
||||
|
||||
bool DockManager::hasNameConflict(const QString& name, DockLayout::Index layout_index)
|
||||
{
|
||||
std::string safe_name = Path::SanitizeFileName(name.toStdString());
|
||||
for (DockLayout::Index i = 0; i < m_layouts.size(); i++)
|
||||
{
|
||||
std::string other_safe_name = Path::SanitizeFileName(m_layouts[i].name().toStdString());
|
||||
if (i != layout_index && StringUtil::compareNoCase(other_safe_name, safe_name))
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void DockManager::updateDockWidgetTitles()
|
||||
{
|
||||
if (m_current_layout == DockLayout::INVALID_INDEX)
|
||||
return;
|
||||
|
||||
m_layouts.at(m_current_layout).updateDockWidgetTitles();
|
||||
}
|
||||
|
||||
const std::map<QString, QPointer<DebuggerView>>& DockManager::debuggerViews()
|
||||
{
|
||||
static std::map<QString, QPointer<DebuggerView>> dummy;
|
||||
if (m_current_layout == DockLayout::INVALID_INDEX)
|
||||
return dummy;
|
||||
|
||||
return m_layouts.at(m_current_layout).debuggerViews();
|
||||
}
|
||||
|
||||
size_t DockManager::countDebuggerViewsOfType(const char* type)
|
||||
{
|
||||
if (m_current_layout == DockLayout::INVALID_INDEX)
|
||||
return 0;
|
||||
|
||||
return m_layouts.at(m_current_layout).countDebuggerViewsOfType(type);
|
||||
}
|
||||
|
||||
void DockManager::recreateDebuggerView(const QString& unique_name)
|
||||
{
|
||||
if (m_current_layout == DockLayout::INVALID_INDEX)
|
||||
return;
|
||||
|
||||
m_layouts.at(m_current_layout).recreateDebuggerView(unique_name);
|
||||
}
|
||||
|
||||
void DockManager::destroyDebuggerView(const QString& unique_name)
|
||||
{
|
||||
if (m_current_layout == DockLayout::INVALID_INDEX)
|
||||
return;
|
||||
|
||||
m_layouts.at(m_current_layout).destroyDebuggerView(unique_name);
|
||||
}
|
||||
|
||||
void DockManager::setPrimaryDebuggerView(DebuggerView* widget, bool is_primary)
|
||||
{
|
||||
if (m_current_layout == DockLayout::INVALID_INDEX)
|
||||
return;
|
||||
|
||||
m_layouts.at(m_current_layout).setPrimaryDebuggerView(widget, is_primary);
|
||||
}
|
||||
|
||||
void DockManager::switchToDebuggerView(DebuggerView* widget)
|
||||
{
|
||||
if (m_current_layout == DockLayout::INVALID_INDEX)
|
||||
return;
|
||||
|
||||
for (const auto& [unique_name, test_widget] : m_layouts.at(m_current_layout).debuggerViews())
|
||||
{
|
||||
if (widget == test_widget)
|
||||
{
|
||||
auto [controller, view] = DockUtils::dockWidgetFromName(unique_name);
|
||||
controller->setAsCurrentTab();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void DockManager::updateTheme()
|
||||
{
|
||||
if (m_menu_bar)
|
||||
m_menu_bar->updateTheme();
|
||||
|
||||
for (DockLayout& layout : m_layouts)
|
||||
for (const auto& [unique_name, widget] : layout.debuggerViews())
|
||||
widget->updateStyleSheet();
|
||||
|
||||
// KDDockWidgets::QtWidgets::TabBar sets its own style to a subclass of
|
||||
// QProxyStyle in its constructor, so we need to update that here.
|
||||
for (KDDockWidgets::Core::Group* group : KDDockWidgets::DockRegistry::self()->groups())
|
||||
{
|
||||
auto tab_bar = static_cast<KDDockWidgets::QtWidgets::TabBar*>(group->tabBar()->view());
|
||||
if (QProxyStyle* style = qobject_cast<QProxyStyle*>(tab_bar->style()))
|
||||
style->setBaseStyle(QStyleFactory::create(qApp->style()->name()));
|
||||
}
|
||||
}
|
||||
|
||||
bool DockManager::isLayoutLocked()
|
||||
{
|
||||
return m_layout_locked;
|
||||
}
|
||||
|
||||
void DockManager::setLayoutLockedAndSaveSetting(bool locked)
|
||||
{
|
||||
setLayoutLocked(locked, true);
|
||||
}
|
||||
|
||||
void DockManager::setLayoutLocked(bool locked, bool save_setting)
|
||||
{
|
||||
m_layout_locked = locked;
|
||||
|
||||
if (m_menu_bar)
|
||||
m_menu_bar->onLockStateChanged(locked);
|
||||
|
||||
updateToolBarLockState();
|
||||
|
||||
for (KDDockWidgets::Core::Group* group : KDDockWidgets::DockRegistry::self()->groups())
|
||||
{
|
||||
auto stack = static_cast<KDDockWidgets::QtWidgets::Stack*>(group->stack()->view());
|
||||
stack->setTabsClosable(!m_layout_locked);
|
||||
|
||||
// HACK: Make sure the sizes of the tabs get updated.
|
||||
if (stack->tabBar()->count() > 0)
|
||||
stack->tabBar()->setTabText(0, stack->tabBar()->tabText(0));
|
||||
}
|
||||
|
||||
if (save_setting)
|
||||
{
|
||||
Host::SetBaseBoolSettingValue("Debugger/UserInterface", "LayoutLocked", m_layout_locked);
|
||||
Host::CommitBaseSettingChanges();
|
||||
}
|
||||
}
|
||||
|
||||
void DockManager::updateToolBarLockState()
|
||||
{
|
||||
if (!g_debugger_window)
|
||||
return;
|
||||
|
||||
for (QToolBar* toolbar : g_debugger_window->findChildren<QToolBar*>())
|
||||
toolbar->setMovable(!m_layout_locked || toolbar->isFloating());
|
||||
}
|
||||
|
||||
std::optional<BreakPointCpu> DockManager::cpu()
|
||||
{
|
||||
if (m_current_layout == DockLayout::INVALID_INDEX)
|
||||
return std::nullopt;
|
||||
|
||||
return m_layouts.at(m_current_layout).cpu();
|
||||
}
|
||||
|
||||
KDDockWidgets::Core::DockWidget* DockManager::dockWidgetFactory(const QString& name)
|
||||
{
|
||||
if (!g_debugger_window)
|
||||
return nullptr;
|
||||
|
||||
DockManager& manager = g_debugger_window->dockManager();
|
||||
if (manager.m_current_layout == DockLayout::INVALID_INDEX)
|
||||
return nullptr;
|
||||
|
||||
return manager.m_layouts.at(manager.m_current_layout).createDockWidget(name);
|
||||
}
|
||||
|
||||
bool DockManager::dragAboutToStart(KDDockWidgets::Core::Draggable* draggable)
|
||||
{
|
||||
bool locked = true;
|
||||
if (g_debugger_window)
|
||||
locked = g_debugger_window->dockManager().isLayoutLocked();
|
||||
|
||||
KDDockWidgets::Config::self().setDropIndicatorsInhibited(locked);
|
||||
|
||||
if (draggable->isInProgrammaticDrag())
|
||||
return true;
|
||||
|
||||
// Allow floating windows to be dragged around even if the layout is locked.
|
||||
if (draggable->isWindow())
|
||||
return true;
|
||||
|
||||
if (!g_debugger_window)
|
||||
return false;
|
||||
|
||||
return !locked;
|
||||
}
|
||||
111
pcsx2-qt/Debugger/Docking/DockManager.h
Normal file
111
pcsx2-qt/Debugger/Docking/DockManager.h
Normal file
@@ -0,0 +1,111 @@
|
||||
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
|
||||
// SPDX-License-Identifier: GPL-3.0+
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "Debugger/Docking/DockLayout.h"
|
||||
#include "Debugger/Docking/DockMenuBar.h"
|
||||
|
||||
#include <kddockwidgets/MainWindow.h>
|
||||
#include <kddockwidgets/DockWidget.h>
|
||||
#include <kddockwidgets/core/DockRegistry.h>
|
||||
#include <kddockwidgets/core/DockWidget.h>
|
||||
#include <kddockwidgets/core/Draggable_p.h>
|
||||
|
||||
#include <QtCore/QPointer>
|
||||
#include <QtWidgets/QPushButton>
|
||||
#include <QtWidgets/QTabBar>
|
||||
|
||||
class DockManager : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
DockManager(QObject* parent = nullptr);
|
||||
|
||||
DockManager(const DockManager& rhs) = delete;
|
||||
DockManager& operator=(const DockManager& rhs) = delete;
|
||||
|
||||
DockManager(DockManager&& rhs) = delete;
|
||||
DockManager& operator=(DockManager&&) = delete;
|
||||
|
||||
// This needs to be called before any KDDockWidgets objects are created
|
||||
// including the debugger window itself.
|
||||
static void configureDockingSystem();
|
||||
|
||||
template <typename... Args>
|
||||
DockLayout::Index createLayout(Args&&... args)
|
||||
{
|
||||
DockLayout::Index layout_index = m_layouts.size();
|
||||
|
||||
if (m_layouts.empty())
|
||||
{
|
||||
// Delete the placeholder created in DockManager::deleteLayout.
|
||||
for (KDDockWidgets::Core::DockWidget* dock : KDDockWidgets::DockRegistry::self()->dockwidgets())
|
||||
delete dock;
|
||||
}
|
||||
|
||||
m_layouts.emplace_back(std::forward<Args>(args)..., layout_index);
|
||||
|
||||
return layout_index;
|
||||
}
|
||||
|
||||
bool deleteLayout(DockLayout::Index layout_index);
|
||||
|
||||
void switchToLayout(DockLayout::Index layout_index, bool blink_tab = false);
|
||||
bool switchToLayoutWithCPU(BreakPointCpu cpu, bool blink_tab = false);
|
||||
|
||||
void loadLayouts();
|
||||
bool saveLayouts();
|
||||
bool saveCurrentLayout();
|
||||
|
||||
QString currentLayoutName();
|
||||
bool canResetCurrentLayout();
|
||||
|
||||
void resetCurrentLayout();
|
||||
void resetDefaultLayouts();
|
||||
void resetAllLayouts();
|
||||
|
||||
void createToolsMenu(QMenu* menu);
|
||||
void createWindowsMenu(QMenu* menu);
|
||||
|
||||
QWidget* createMenuBar(QWidget* original_menu_bar);
|
||||
void updateLayoutSwitcher();
|
||||
void newLayoutClicked();
|
||||
void openLayoutSwitcherContextMenu(const QPoint& pos, QTabBar* layout_switcher);
|
||||
void editLayoutClicked(DockLayout::Index layout_index);
|
||||
void resetLayoutClicked(DockLayout::Index layout_index);
|
||||
void deleteLayoutClicked(DockLayout::Index layout_index);
|
||||
void layoutSwitcherTabMoved(DockLayout::Index from_index, DockLayout::Index to_index);
|
||||
|
||||
bool hasNameConflict(const QString& name, DockLayout::Index layout_index);
|
||||
|
||||
void updateDockWidgetTitles();
|
||||
|
||||
const std::map<QString, QPointer<DebuggerView>>& debuggerViews();
|
||||
size_t countDebuggerViewsOfType(const char* type);
|
||||
void recreateDebuggerView(const QString& unique_name);
|
||||
void destroyDebuggerView(const QString& unique_name);
|
||||
void setPrimaryDebuggerView(DebuggerView* widget, bool is_primary);
|
||||
void switchToDebuggerView(DebuggerView* widget);
|
||||
|
||||
void updateTheme();
|
||||
|
||||
bool isLayoutLocked();
|
||||
void setLayoutLockedAndSaveSetting(bool locked);
|
||||
void setLayoutLocked(bool locked, bool save_setting);
|
||||
void updateToolBarLockState();
|
||||
|
||||
std::optional<BreakPointCpu> cpu();
|
||||
|
||||
private:
|
||||
static KDDockWidgets::Core::DockWidget* dockWidgetFactory(const QString& name);
|
||||
static bool dragAboutToStart(KDDockWidgets::Core::Draggable* draggable);
|
||||
|
||||
std::vector<DockLayout> m_layouts;
|
||||
DockLayout::Index m_current_layout = DockLayout::INVALID_INDEX;
|
||||
|
||||
DockMenuBar* m_menu_bar = nullptr;
|
||||
|
||||
bool m_layout_locked = true;
|
||||
};
|
||||
342
pcsx2-qt/Debugger/Docking/DockMenuBar.cpp
Normal file
342
pcsx2-qt/Debugger/Docking/DockMenuBar.cpp
Normal file
@@ -0,0 +1,342 @@
|
||||
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
|
||||
// SPDX-License-Identifier: GPL-3.0+
|
||||
|
||||
#include "DockMenuBar.h"
|
||||
|
||||
#include <QtCore/QTimer>
|
||||
#include <QtGui/QPainter>
|
||||
#include <QtGui/QPaintEvent>
|
||||
#include <QtWidgets/QBoxLayout>
|
||||
#include <QtWidgets/QStyleFactory>
|
||||
#include <QtWidgets/QStyleOption>
|
||||
|
||||
static const int OUTER_MENU_MARGIN = 2;
|
||||
static const int INNER_MENU_MARGIN = 4;
|
||||
|
||||
DockMenuBar::DockMenuBar(QWidget* original_menu_bar, QWidget* parent)
|
||||
: QWidget(parent)
|
||||
, m_original_menu_bar(original_menu_bar)
|
||||
{
|
||||
QHBoxLayout* layout = new QHBoxLayout;
|
||||
layout->setContentsMargins(0, OUTER_MENU_MARGIN, OUTER_MENU_MARGIN, 0);
|
||||
setLayout(layout);
|
||||
|
||||
QWidget* menu_wrapper = new QWidget;
|
||||
menu_wrapper->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Preferred);
|
||||
layout->addWidget(menu_wrapper);
|
||||
|
||||
QHBoxLayout* menu_layout = new QHBoxLayout;
|
||||
menu_layout->setContentsMargins(0, INNER_MENU_MARGIN, 0, INNER_MENU_MARGIN);
|
||||
menu_wrapper->setLayout(menu_layout);
|
||||
|
||||
menu_layout->addWidget(original_menu_bar);
|
||||
|
||||
m_layout_switcher = new QTabBar;
|
||||
m_layout_switcher->setContentsMargins(0, 0, 0, 0);
|
||||
m_layout_switcher->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred);
|
||||
m_layout_switcher->setContextMenuPolicy(Qt::CustomContextMenu);
|
||||
m_layout_switcher->setDrawBase(false);
|
||||
m_layout_switcher->setExpanding(false);
|
||||
m_layout_switcher->setMovable(true);
|
||||
layout->addWidget(m_layout_switcher);
|
||||
|
||||
connect(m_layout_switcher, &QTabBar::tabMoved, this, [this](int from, int to) {
|
||||
DockLayout::Index from_index = static_cast<DockLayout::Index>(from);
|
||||
DockLayout::Index to_index = static_cast<DockLayout::Index>(to);
|
||||
emit layoutMoved(from_index, to_index);
|
||||
});
|
||||
|
||||
connect(m_layout_switcher, &QTabBar::customContextMenuRequested, this, [this](const QPoint& pos) {
|
||||
emit layoutSwitcherContextMenuRequested(pos, m_layout_switcher);
|
||||
});
|
||||
|
||||
m_blink_timer = new QTimer(this);
|
||||
connect(m_blink_timer, &QTimer::timeout, this, &DockMenuBar::updateBlink);
|
||||
|
||||
m_layout_locked_toggle = new QPushButton;
|
||||
m_layout_locked_toggle->setCheckable(true);
|
||||
connect(m_layout_locked_toggle, &QPushButton::clicked, this, [this](bool checked) {
|
||||
if (m_ignore_lock_state_changed)
|
||||
return;
|
||||
|
||||
emit lockButtonToggled(checked);
|
||||
});
|
||||
layout->addWidget(m_layout_locked_toggle);
|
||||
|
||||
updateTheme();
|
||||
}
|
||||
|
||||
void DockMenuBar::updateTheme()
|
||||
{
|
||||
DockMenuBarStyle* style = new DockMenuBarStyle(m_layout_switcher);
|
||||
m_original_menu_bar->setStyle(style);
|
||||
m_layout_switcher->setStyle(style);
|
||||
m_layout_locked_toggle->setStyle(style);
|
||||
|
||||
delete m_style;
|
||||
m_style = style;
|
||||
}
|
||||
|
||||
void DockMenuBar::updateLayoutSwitcher(DockLayout::Index current_index, const std::vector<DockLayout>& layouts)
|
||||
{
|
||||
disconnect(m_tab_connection);
|
||||
|
||||
for (int i = m_layout_switcher->count(); i > 0; i--)
|
||||
m_layout_switcher->removeTab(i - 1);
|
||||
|
||||
for (const DockLayout& layout : layouts)
|
||||
{
|
||||
const char* cpu_name = DebugInterface::cpuName(layout.cpu());
|
||||
QString tab_name = QString("%1 (%2)").arg(layout.name()).arg(cpu_name);
|
||||
m_layout_switcher->addTab(tab_name);
|
||||
}
|
||||
|
||||
m_plus_tab_index = m_layout_switcher->addTab("+");
|
||||
m_current_tab_index = current_index;
|
||||
|
||||
if (current_index != DockLayout::INVALID_INDEX)
|
||||
m_layout_switcher->setCurrentIndex(current_index);
|
||||
else
|
||||
m_layout_switcher->setCurrentIndex(m_plus_tab_index);
|
||||
|
||||
// If we don't have any layouts, the currently selected tab will never be
|
||||
// changed, so we respond to all clicks instead.
|
||||
if (m_plus_tab_index > 0)
|
||||
m_tab_connection = connect(m_layout_switcher, &QTabBar::currentChanged, this, &DockMenuBar::tabChanged);
|
||||
else
|
||||
m_tab_connection = connect(m_layout_switcher, &QTabBar::tabBarClicked, this, &DockMenuBar::tabChanged);
|
||||
|
||||
stopBlink();
|
||||
}
|
||||
|
||||
void DockMenuBar::onCurrentLayoutChanged(DockLayout::Index current_index)
|
||||
{
|
||||
m_ignore_current_tab_changed = true;
|
||||
|
||||
if (current_index != DockLayout::INVALID_INDEX)
|
||||
m_layout_switcher->setCurrentIndex(current_index);
|
||||
else
|
||||
m_layout_switcher->setCurrentIndex(m_plus_tab_index);
|
||||
|
||||
m_ignore_current_tab_changed = false;
|
||||
}
|
||||
|
||||
void DockMenuBar::onLockStateChanged(bool layout_locked)
|
||||
{
|
||||
m_ignore_lock_state_changed = true;
|
||||
|
||||
m_layout_locked_toggle->setChecked(layout_locked);
|
||||
|
||||
if (layout_locked)
|
||||
{
|
||||
m_layout_locked_toggle->setText(tr("Layout Locked"));
|
||||
m_layout_locked_toggle->setIcon(QIcon::fromTheme(QString::fromUtf8("padlock-lock")));
|
||||
}
|
||||
else
|
||||
{
|
||||
m_layout_locked_toggle->setText(tr("Layout Unlocked"));
|
||||
m_layout_locked_toggle->setIcon(QIcon::fromTheme(QString::fromUtf8("padlock-unlock")));
|
||||
}
|
||||
|
||||
m_ignore_lock_state_changed = false;
|
||||
}
|
||||
|
||||
void DockMenuBar::startBlink(DockLayout::Index layout_index)
|
||||
{
|
||||
stopBlink();
|
||||
|
||||
if (layout_index == DockLayout::INVALID_INDEX)
|
||||
return;
|
||||
|
||||
m_blink_tab = static_cast<int>(layout_index);
|
||||
m_blink_stage = 0;
|
||||
m_blink_timer->start(500);
|
||||
|
||||
updateBlink();
|
||||
}
|
||||
|
||||
void DockMenuBar::updateBlink()
|
||||
{
|
||||
if (m_blink_tab < m_layout_switcher->count())
|
||||
{
|
||||
if (m_blink_stage % 2 == 0)
|
||||
m_layout_switcher->setTabTextColor(m_blink_tab, Qt::red);
|
||||
else
|
||||
m_layout_switcher->setTabTextColor(m_blink_tab, m_layout_switcher->palette().text().color());
|
||||
}
|
||||
|
||||
m_blink_stage++;
|
||||
|
||||
if (m_blink_stage > 7)
|
||||
m_blink_timer->stop();
|
||||
}
|
||||
|
||||
void DockMenuBar::stopBlink()
|
||||
{
|
||||
if (m_blink_timer->isActive())
|
||||
{
|
||||
if (m_blink_tab < m_layout_switcher->count())
|
||||
m_layout_switcher->setTabTextColor(m_blink_tab, m_layout_switcher->palette().text().color());
|
||||
|
||||
m_blink_timer->stop();
|
||||
}
|
||||
}
|
||||
|
||||
int DockMenuBar::innerHeight() const
|
||||
{
|
||||
return m_original_menu_bar->sizeHint().height() + INNER_MENU_MARGIN * 2;
|
||||
}
|
||||
|
||||
void DockMenuBar::paintEvent(QPaintEvent* event)
|
||||
{
|
||||
QPainter painter(this);
|
||||
|
||||
// This fixes the background colour of the menu bar when using the Windows
|
||||
// Vista style.
|
||||
QStyleOptionMenuItem menu_option;
|
||||
menu_option.palette = palette();
|
||||
menu_option.state = QStyle::State_None;
|
||||
menu_option.menuItemType = QStyleOptionMenuItem::EmptyArea;
|
||||
menu_option.checkType = QStyleOptionMenuItem::NotCheckable;
|
||||
menu_option.rect = rect();
|
||||
menu_option.menuRect = rect();
|
||||
style()->drawControl(QStyle::CE_MenuBarEmptyArea, &menu_option, &painter, this);
|
||||
}
|
||||
|
||||
void DockMenuBar::tabChanged(int index)
|
||||
{
|
||||
// Prevent recursion.
|
||||
if (m_ignore_current_tab_changed)
|
||||
return;
|
||||
|
||||
if (index < m_plus_tab_index)
|
||||
{
|
||||
DockLayout::Index layout_index = static_cast<DockLayout::Index>(index);
|
||||
emit currentLayoutChanged(layout_index);
|
||||
}
|
||||
else if (index == m_plus_tab_index)
|
||||
{
|
||||
emit newButtonClicked();
|
||||
}
|
||||
}
|
||||
|
||||
// *****************************************************************************
|
||||
|
||||
DockMenuBarStyle::DockMenuBarStyle(QObject* parent)
|
||||
: QProxyStyle(QStyleFactory::create(qApp->style()->name()))
|
||||
{
|
||||
setParent(parent);
|
||||
}
|
||||
|
||||
void DockMenuBarStyle::drawControl(
|
||||
ControlElement element,
|
||||
const QStyleOption* option,
|
||||
QPainter* painter,
|
||||
const QWidget* widget) const
|
||||
{
|
||||
switch (element)
|
||||
{
|
||||
case CE_MenuBarItem:
|
||||
{
|
||||
const QStyleOptionMenuItem* opt = qstyleoption_cast<const QStyleOptionMenuItem*>(option);
|
||||
if (!opt)
|
||||
break;
|
||||
|
||||
QWidget* menu_wrapper = widget->parentWidget();
|
||||
if (!menu_wrapper)
|
||||
break;
|
||||
|
||||
const DockMenuBar* menu_bar = qobject_cast<const DockMenuBar*>(menu_wrapper->parentWidget());
|
||||
if (!menu_bar)
|
||||
break;
|
||||
|
||||
if (baseStyle()->name() != "fusion")
|
||||
break;
|
||||
|
||||
// This mirrors a check in QFusionStyle::drawControl. If act is
|
||||
// false, QFusionStyle will try to draw a border along the bottom.
|
||||
bool act = opt->state & State_Selected && opt->state & State_Sunken;
|
||||
if (act)
|
||||
break;
|
||||
|
||||
// Extend the menu item to the bottom of the menu bar to fix the
|
||||
// position in which it draws its bottom border. We also need to
|
||||
// extend it up by the same amount so that the text isn't moved.
|
||||
QStyleOptionMenuItem menu_opt = *opt;
|
||||
int difference = (menu_bar->innerHeight() - option->rect.top()) - menu_opt.rect.height();
|
||||
menu_opt.rect.adjust(0, -difference, 0, difference);
|
||||
QProxyStyle::drawControl(element, &menu_opt, painter, widget);
|
||||
|
||||
return;
|
||||
}
|
||||
case CE_TabBarTab:
|
||||
{
|
||||
QProxyStyle::drawControl(element, option, painter, widget);
|
||||
|
||||
// Draw a slick-looking highlight under the currently selected tab.
|
||||
if (baseStyle()->name() == "fusion")
|
||||
{
|
||||
const QStyleOptionTab* tab = qstyleoption_cast<const QStyleOptionTab*>(option);
|
||||
if (tab && (tab->state & State_Selected))
|
||||
{
|
||||
painter->setPen(tab->palette.highlight().color());
|
||||
painter->drawLine(tab->rect.bottomLeft(), tab->rect.bottomRight());
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
case CE_MenuBarEmptyArea:
|
||||
{
|
||||
// Prevent it from drawing a border in the wrong position.
|
||||
return;
|
||||
}
|
||||
default:
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
QProxyStyle::drawControl(element, option, painter, widget);
|
||||
}
|
||||
|
||||
QSize DockMenuBarStyle::sizeFromContents(
|
||||
QStyle::ContentsType type, const QStyleOption* option, const QSize& contents_size, const QWidget* widget) const
|
||||
{
|
||||
QSize size = QProxyStyle::sizeFromContents(type, option, contents_size, widget);
|
||||
|
||||
#ifdef Q_OS_WIN32
|
||||
// Adjust the sizes of the layout switcher tabs depending on the theme.
|
||||
if (type == CT_TabBarTab)
|
||||
{
|
||||
const QStyleOptionTab* opt = qstyleoption_cast<const QStyleOptionTab*>(option);
|
||||
if (!opt)
|
||||
return size;
|
||||
|
||||
const QTabBar* tab_bar = qobject_cast<const QTabBar*>(widget);
|
||||
if (!tab_bar)
|
||||
return size;
|
||||
|
||||
const DockMenuBar* menu_bar = qobject_cast<const DockMenuBar*>(tab_bar->parentWidget());
|
||||
if (!menu_bar)
|
||||
return size;
|
||||
|
||||
if (baseStyle()->name() == "fusion" || baseStyle()->name() == "windowsvista")
|
||||
{
|
||||
// Make sure the tab extends to the bottom of the widget.
|
||||
size.setHeight(menu_bar->innerHeight() - opt->rect.top());
|
||||
}
|
||||
else if (baseStyle()->name() == "windows11")
|
||||
{
|
||||
// Adjust the size of the tab such that it is vertically centred.
|
||||
size.setHeight(menu_bar->innerHeight() - opt->rect.top() * 2 - OUTER_MENU_MARGIN);
|
||||
|
||||
// Make the plus button square.
|
||||
if (opt->tabIndex + 1 == tab_bar->count())
|
||||
size.setWidth(size.height());
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
return size;
|
||||
}
|
||||
93
pcsx2-qt/Debugger/Docking/DockMenuBar.h
Normal file
93
pcsx2-qt/Debugger/Docking/DockMenuBar.h
Normal file
@@ -0,0 +1,93 @@
|
||||
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
|
||||
// SPDX-License-Identifier: GPL-3.0+
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "Debugger/Docking/DockLayout.h"
|
||||
|
||||
#include <QtWidgets/QMenuBar>
|
||||
#include <QtWidgets/QProxyStyle>
|
||||
#include <QtWidgets/QPushButton>
|
||||
#include <QtWidgets/QTabBar>
|
||||
#include <QtWidgets/QWidget>
|
||||
|
||||
class DockMenuBarStyle;
|
||||
|
||||
// The widget that replaces the normal menu bar. This contains the original menu
|
||||
// bar, the layout switcher and the layout locked/unlocked toggle button.
|
||||
class DockMenuBar : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
DockMenuBar(QWidget* original_menu_bar, QWidget* parent = nullptr);
|
||||
|
||||
void updateLayoutSwitcher(DockLayout::Index current_index, const std::vector<DockLayout>& layouts);
|
||||
|
||||
void updateTheme();
|
||||
|
||||
// Notify the menu bar that a new layout has been selected.
|
||||
void onCurrentLayoutChanged(DockLayout::Index current_index);
|
||||
|
||||
// Notify the menu bar that the layout has been locked/unlocked.
|
||||
void onLockStateChanged(bool layout_locked);
|
||||
|
||||
void startBlink(DockLayout::Index layout_index);
|
||||
void updateBlink();
|
||||
void stopBlink();
|
||||
|
||||
int innerHeight() const;
|
||||
|
||||
Q_SIGNALS:
|
||||
void currentLayoutChanged(DockLayout::Index layout_index);
|
||||
void newButtonClicked();
|
||||
void layoutMoved(DockLayout::Index from_index, DockLayout::Index to_index);
|
||||
void lockButtonToggled(bool locked);
|
||||
|
||||
void layoutSwitcherContextMenuRequested(const QPoint& pos, QTabBar* layout_switcher);
|
||||
|
||||
protected:
|
||||
void paintEvent(QPaintEvent* event) override;
|
||||
|
||||
private:
|
||||
void tabChanged(int index);
|
||||
|
||||
QWidget* m_original_menu_bar;
|
||||
|
||||
QTabBar* m_layout_switcher;
|
||||
QMetaObject::Connection m_tab_connection;
|
||||
int m_plus_tab_index = -1;
|
||||
int m_current_tab_index = -1;
|
||||
bool m_ignore_current_tab_changed = false;
|
||||
|
||||
QTimer* m_blink_timer = nullptr;
|
||||
int m_blink_tab = 0;
|
||||
int m_blink_stage = 0;
|
||||
|
||||
QPushButton* m_layout_locked_toggle;
|
||||
bool m_ignore_lock_state_changed = false;
|
||||
|
||||
DockMenuBarStyle* m_style = nullptr;
|
||||
};
|
||||
|
||||
// Fixes some theming issues relating to the menu bar, the layout switcher and
|
||||
// the layout locked/unlocked toggle button.
|
||||
class DockMenuBarStyle : public QProxyStyle
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
DockMenuBarStyle(QObject* parent = nullptr);
|
||||
|
||||
void drawControl(
|
||||
ControlElement element,
|
||||
const QStyleOption* option,
|
||||
QPainter* painter,
|
||||
const QWidget* widget = nullptr) const override;
|
||||
|
||||
QSize sizeFromContents(
|
||||
QStyle::ContentsType type,
|
||||
const QStyleOption* option,
|
||||
const QSize& contents_size,
|
||||
const QWidget* widget = nullptr) const override;
|
||||
};
|
||||
209
pcsx2-qt/Debugger/Docking/DockTables.cpp
Normal file
209
pcsx2-qt/Debugger/Docking/DockTables.cpp
Normal file
@@ -0,0 +1,209 @@
|
||||
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
|
||||
// SPDX-License-Identifier: GPL-3.0+
|
||||
|
||||
#include "DockTables.h"
|
||||
|
||||
#include "Debugger/DebuggerEvents.h"
|
||||
#include "Debugger/DisassemblyView.h"
|
||||
#include "Debugger/RegisterView.h"
|
||||
#include "Debugger/StackView.h"
|
||||
#include "Debugger/ThreadView.h"
|
||||
#include "Debugger/Breakpoints/BreakpointView.h"
|
||||
#include "Debugger/Memory/MemorySearchView.h"
|
||||
#include "Debugger/Memory/MemoryView.h"
|
||||
#include "Debugger/Memory/SavedAddressesView.h"
|
||||
#include "Debugger/SymbolTree/SymbolTreeViews.h"
|
||||
|
||||
using namespace DockUtils;
|
||||
|
||||
static void hashDefaultLayout(const DockTables::DefaultDockLayout& layout, u32& hash);
|
||||
static void hashDefaultGroup(const DockTables::DefaultDockGroupDescription& group, u32& hash);
|
||||
static void hashDefaultDockWidget(const DockTables::DefaultDockWidgetDescription& widget, u32& hash);
|
||||
static void hashNumber(u32 number, u32& hash);
|
||||
static void hashString(const char* string, u32& hash);
|
||||
|
||||
#define DEBUGGER_VIEW(type, display_name, preferred_location) \
|
||||
{ \
|
||||
#type, \
|
||||
{ \
|
||||
[](const DebuggerViewParameters& parameters) -> DebuggerView* { \
|
||||
DebuggerView* widget = new type(parameters); \
|
||||
widget->handleEvent(DebuggerEvents::Refresh()); \
|
||||
return widget; \
|
||||
}, \
|
||||
display_name, \
|
||||
preferred_location \
|
||||
} \
|
||||
}
|
||||
|
||||
const std::map<std::string, DockTables::DebuggerViewDescription> DockTables::DEBUGGER_VIEWS = {
|
||||
DEBUGGER_VIEW(BreakpointView, QT_TRANSLATE_NOOP("DebuggerView", "Breakpoints"), BOTTOM_MIDDLE),
|
||||
DEBUGGER_VIEW(DisassemblyView, QT_TRANSLATE_NOOP("DebuggerView", "Disassembly"), TOP_RIGHT),
|
||||
DEBUGGER_VIEW(FunctionTreeView, QT_TRANSLATE_NOOP("DebuggerView", "Functions"), TOP_LEFT),
|
||||
DEBUGGER_VIEW(GlobalVariableTreeView, QT_TRANSLATE_NOOP("DebuggerView", "Globals"), BOTTOM_MIDDLE),
|
||||
DEBUGGER_VIEW(LocalVariableTreeView, QT_TRANSLATE_NOOP("DebuggerView", "Locals"), BOTTOM_MIDDLE),
|
||||
DEBUGGER_VIEW(MemorySearchView, QT_TRANSLATE_NOOP("DebuggerView", "Memory Search"), TOP_LEFT),
|
||||
DEBUGGER_VIEW(MemoryView, QT_TRANSLATE_NOOP("DebuggerView", "Memory"), BOTTOM_MIDDLE),
|
||||
DEBUGGER_VIEW(ParameterVariableTreeView, QT_TRANSLATE_NOOP("DebuggerView", "Parameters"), BOTTOM_MIDDLE),
|
||||
DEBUGGER_VIEW(RegisterView, QT_TRANSLATE_NOOP("DebuggerView", "Registers"), TOP_LEFT),
|
||||
DEBUGGER_VIEW(SavedAddressesView, QT_TRANSLATE_NOOP("DebuggerView", "Saved Addresses"), BOTTOM_MIDDLE),
|
||||
DEBUGGER_VIEW(StackView, QT_TRANSLATE_NOOP("DebuggerView", "Stack"), BOTTOM_MIDDLE),
|
||||
DEBUGGER_VIEW(ThreadView, QT_TRANSLATE_NOOP("DebuggerView", "Threads"), BOTTOM_MIDDLE),
|
||||
};
|
||||
|
||||
#undef DEBUGGER_VIEW
|
||||
|
||||
const std::vector<DockTables::DefaultDockLayout> DockTables::DEFAULT_DOCK_LAYOUTS = {
|
||||
{
|
||||
.name = QT_TRANSLATE_NOOP("DebuggerLayout", "R5900"),
|
||||
.cpu = BREAKPOINT_EE,
|
||||
.groups = {
|
||||
/* [DefaultDockGroup::TOP_RIGHT] = */ {KDDockWidgets::Location_OnRight, DefaultDockGroup::ROOT},
|
||||
/* [DefaultDockGroup::BOTTOM] = */ {KDDockWidgets::Location_OnBottom, DefaultDockGroup::TOP_RIGHT},
|
||||
/* [DefaultDockGroup::TOP_LEFT] = */ {KDDockWidgets::Location_OnLeft, DefaultDockGroup::TOP_RIGHT},
|
||||
},
|
||||
.widgets = {
|
||||
/* DefaultDockGroup::TOP_RIGHT */
|
||||
{"DisassemblyView", DefaultDockGroup::TOP_RIGHT},
|
||||
/* DefaultDockGroup::BOTTOM */
|
||||
{"MemoryView", DefaultDockGroup::BOTTOM},
|
||||
{"BreakpointView", DefaultDockGroup::BOTTOM},
|
||||
{"ThreadView", DefaultDockGroup::BOTTOM},
|
||||
{"StackView", DefaultDockGroup::BOTTOM},
|
||||
{"SavedAddressesView", DefaultDockGroup::BOTTOM},
|
||||
{"GlobalVariableTreeView", DefaultDockGroup::BOTTOM},
|
||||
{"LocalVariableTreeView", DefaultDockGroup::BOTTOM},
|
||||
{"ParameterVariableTreeView", DefaultDockGroup::BOTTOM},
|
||||
/* DefaultDockGroup::TOP_LEFT */
|
||||
{"RegisterView", DefaultDockGroup::TOP_LEFT},
|
||||
{"FunctionTreeView", DefaultDockGroup::TOP_LEFT},
|
||||
{"MemorySearchView", DefaultDockGroup::TOP_LEFT},
|
||||
},
|
||||
.toolbars = {
|
||||
"toolBarDebug",
|
||||
"toolBarFile",
|
||||
},
|
||||
},
|
||||
{
|
||||
.name = QT_TRANSLATE_NOOP("DebuggerLayout", "R3000"),
|
||||
.cpu = BREAKPOINT_IOP,
|
||||
.groups = {
|
||||
/* [DefaultDockGroup::TOP_RIGHT] = */ {KDDockWidgets::Location_OnRight, DefaultDockGroup::ROOT},
|
||||
/* [DefaultDockGroup::BOTTOM] = */ {KDDockWidgets::Location_OnBottom, DefaultDockGroup::TOP_RIGHT},
|
||||
/* [DefaultDockGroup::TOP_LEFT] = */ {KDDockWidgets::Location_OnLeft, DefaultDockGroup::TOP_RIGHT},
|
||||
},
|
||||
.widgets = {
|
||||
/* DefaultDockGroup::TOP_RIGHT */
|
||||
{"DisassemblyView", DefaultDockGroup::TOP_RIGHT},
|
||||
/* DefaultDockGroup::BOTTOM */
|
||||
{"MemoryView", DefaultDockGroup::BOTTOM},
|
||||
{"BreakpointView", DefaultDockGroup::BOTTOM},
|
||||
{"ThreadView", DefaultDockGroup::BOTTOM},
|
||||
{"StackView", DefaultDockGroup::BOTTOM},
|
||||
{"SavedAddressesView", DefaultDockGroup::BOTTOM},
|
||||
{"GlobalVariableTreeView", DefaultDockGroup::BOTTOM},
|
||||
{"LocalVariableTreeView", DefaultDockGroup::BOTTOM},
|
||||
{"ParameterVariableTreeView", DefaultDockGroup::BOTTOM},
|
||||
/* DefaultDockGroup::TOP_LEFT */
|
||||
{"RegisterView", DefaultDockGroup::TOP_LEFT},
|
||||
{"FunctionTreeView", DefaultDockGroup::TOP_LEFT},
|
||||
{"MemorySearchView", DefaultDockGroup::TOP_LEFT},
|
||||
},
|
||||
.toolbars = {
|
||||
"toolBarDebug",
|
||||
"toolBarFile",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const DockTables::DefaultDockLayout* DockTables::defaultLayout(const std::string& name)
|
||||
{
|
||||
for (const DockTables::DefaultDockLayout& default_layout : DockTables::DEFAULT_DOCK_LAYOUTS)
|
||||
if (default_layout.name == name)
|
||||
return &default_layout;
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
u32 DockTables::hashDefaultLayouts()
|
||||
{
|
||||
static std::optional<u32> hash;
|
||||
if (hash.has_value())
|
||||
return *hash;
|
||||
|
||||
hash.emplace(0);
|
||||
|
||||
u32 hash_version = 2;
|
||||
hashNumber(hash_version, *hash);
|
||||
|
||||
hashNumber(static_cast<u32>(DEFAULT_DOCK_LAYOUTS.size()), *hash);
|
||||
for (const DefaultDockLayout& layout : DEFAULT_DOCK_LAYOUTS)
|
||||
hashDefaultLayout(layout, *hash);
|
||||
|
||||
return *hash;
|
||||
}
|
||||
|
||||
static void hashDefaultLayout(const DockTables::DefaultDockLayout& layout, u32& hash)
|
||||
{
|
||||
hashString(layout.name.c_str(), hash);
|
||||
hashString(DebugInterface::cpuName(layout.cpu), hash);
|
||||
|
||||
hashNumber(static_cast<u32>(layout.groups.size()), hash);
|
||||
for (const DockTables::DefaultDockGroupDescription& group : layout.groups)
|
||||
hashDefaultGroup(group, hash);
|
||||
|
||||
hashNumber(static_cast<u32>(layout.widgets.size()), hash);
|
||||
for (const DockTables::DefaultDockWidgetDescription& widget : layout.widgets)
|
||||
hashDefaultDockWidget(widget, hash);
|
||||
|
||||
hashNumber(static_cast<u32>(layout.toolbars.size()), hash);
|
||||
for (const std::string& toolbar : layout.toolbars)
|
||||
hashString(toolbar.c_str(), hash);
|
||||
}
|
||||
|
||||
static void hashDefaultGroup(const DockTables::DefaultDockGroupDescription& group, u32& hash)
|
||||
{
|
||||
// This is inline here so that it's obvious that changing it will affect the
|
||||
// result of the hash.
|
||||
const char* location = "";
|
||||
switch (group.location)
|
||||
{
|
||||
case KDDockWidgets::Location_None:
|
||||
location = "none";
|
||||
break;
|
||||
case KDDockWidgets::Location_OnLeft:
|
||||
location = "left";
|
||||
break;
|
||||
case KDDockWidgets::Location_OnTop:
|
||||
location = "top";
|
||||
break;
|
||||
case KDDockWidgets::Location_OnRight:
|
||||
location = "right";
|
||||
break;
|
||||
case KDDockWidgets::Location_OnBottom:
|
||||
location = "bottom";
|
||||
break;
|
||||
}
|
||||
|
||||
hashString(location, hash);
|
||||
hashNumber(static_cast<u32>(group.parent), hash);
|
||||
}
|
||||
|
||||
static void hashDefaultDockWidget(const DockTables::DefaultDockWidgetDescription& widget, u32& hash)
|
||||
{
|
||||
hashString(widget.type.c_str(), hash);
|
||||
hashNumber(static_cast<u32>(widget.group), hash);
|
||||
}
|
||||
|
||||
static void hashNumber(u32 number, u32& hash)
|
||||
{
|
||||
hash = hash * 31 + number;
|
||||
}
|
||||
|
||||
static void hashString(const char* string, u32& hash)
|
||||
{
|
||||
u32 size = static_cast<u32>(strlen(string));
|
||||
hash = hash * 31 + size;
|
||||
for (u32 i = 0; i < size; i++)
|
||||
hash = hash * 31 + string[i];
|
||||
}
|
||||
71
pcsx2-qt/Debugger/Docking/DockTables.h
Normal file
71
pcsx2-qt/Debugger/Docking/DockTables.h
Normal file
@@ -0,0 +1,71 @@
|
||||
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
|
||||
// SPDX-License-Identifier: GPL-3.0+
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "DockUtils.h"
|
||||
|
||||
#include "DebugTools/DebugInterface.h"
|
||||
|
||||
#include <kddockwidgets/KDDockWidgets.h>
|
||||
|
||||
class MD5Digest;
|
||||
|
||||
class DebuggerView;
|
||||
struct DebuggerViewParameters;
|
||||
|
||||
namespace DockTables
|
||||
{
|
||||
struct DebuggerViewDescription
|
||||
{
|
||||
DebuggerView* (*create_widget)(const DebuggerViewParameters& parameters);
|
||||
|
||||
// The untranslated string displayed as the dock widget tab text.
|
||||
const char* display_name;
|
||||
|
||||
// This is used to determine which group dock widgets of this type are
|
||||
// added to when they're opened from the Windows menu.
|
||||
DockUtils::PreferredLocation preferred_location;
|
||||
};
|
||||
|
||||
extern const std::map<std::string, DebuggerViewDescription> DEBUGGER_VIEWS;
|
||||
|
||||
enum class DefaultDockGroup
|
||||
{
|
||||
ROOT = -1,
|
||||
TOP_RIGHT = 0,
|
||||
BOTTOM = 1,
|
||||
TOP_LEFT = 2
|
||||
};
|
||||
|
||||
struct DefaultDockGroupDescription
|
||||
{
|
||||
KDDockWidgets::Location location;
|
||||
DefaultDockGroup parent;
|
||||
};
|
||||
|
||||
extern const std::vector<DefaultDockGroupDescription> DEFAULT_DOCK_GROUPS;
|
||||
|
||||
struct DefaultDockWidgetDescription
|
||||
{
|
||||
std::string type;
|
||||
DefaultDockGroup group;
|
||||
};
|
||||
|
||||
struct DefaultDockLayout
|
||||
{
|
||||
std::string name;
|
||||
BreakPointCpu cpu;
|
||||
std::vector<DefaultDockGroupDescription> groups;
|
||||
std::vector<DefaultDockWidgetDescription> widgets;
|
||||
std::set<std::string> toolbars;
|
||||
};
|
||||
|
||||
extern const std::vector<DefaultDockLayout> DEFAULT_DOCK_LAYOUTS;
|
||||
|
||||
const DefaultDockLayout* defaultLayout(const std::string& name);
|
||||
|
||||
// This is used to determine if the user has updated and we need to recreate
|
||||
// the default layouts.
|
||||
u32 hashDefaultLayouts();
|
||||
} // namespace DockTables
|
||||
98
pcsx2-qt/Debugger/Docking/DockUtils.cpp
Normal file
98
pcsx2-qt/Debugger/Docking/DockUtils.cpp
Normal file
@@ -0,0 +1,98 @@
|
||||
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
|
||||
// SPDX-License-Identifier: GPL-3.0+
|
||||
|
||||
#include "DockUtils.h"
|
||||
|
||||
#include <kddockwidgets/Config.h>
|
||||
#include <kddockwidgets/core/DockRegistry.h>
|
||||
#include <kddockwidgets/core/Group.h>
|
||||
#include <kddockwidgets/qtwidgets/DockWidget.h>
|
||||
#include <kddockwidgets/qtwidgets/Group.h>
|
||||
|
||||
DockUtils::DockWidgetPair DockUtils::dockWidgetFromName(const QString& unique_name)
|
||||
{
|
||||
KDDockWidgets::Vector<QString> names{unique_name};
|
||||
KDDockWidgets::Vector<KDDockWidgets::Core::DockWidget*> dock_widgets =
|
||||
KDDockWidgets::DockRegistry::self()->dockWidgets(names);
|
||||
if (dock_widgets.size() != 1 || !dock_widgets[0])
|
||||
return {};
|
||||
|
||||
return {dock_widgets[0], static_cast<KDDockWidgets::QtWidgets::DockWidget*>(dock_widgets[0]->view())};
|
||||
}
|
||||
|
||||
void DockUtils::insertDockWidgetAtPreferredLocation(
|
||||
KDDockWidgets::Core::DockWidget* dock_widget,
|
||||
PreferredLocation location,
|
||||
KDDockWidgets::QtWidgets::MainWindow* window)
|
||||
{
|
||||
int width = window->width();
|
||||
int height = window->height();
|
||||
int half_width = width / 2;
|
||||
int half_height = height / 2;
|
||||
|
||||
QPoint preferred_location;
|
||||
switch (location)
|
||||
{
|
||||
case DockUtils::TOP_LEFT:
|
||||
preferred_location = {0, 0};
|
||||
break;
|
||||
case DockUtils::TOP_MIDDLE:
|
||||
preferred_location = {half_width, 0};
|
||||
break;
|
||||
case DockUtils::TOP_RIGHT:
|
||||
preferred_location = {width, 0};
|
||||
break;
|
||||
case DockUtils::MIDDLE_LEFT:
|
||||
preferred_location = {0, half_height};
|
||||
break;
|
||||
case DockUtils::MIDDLE_MIDDLE:
|
||||
preferred_location = {half_width, half_height};
|
||||
break;
|
||||
case DockUtils::MIDDLE_RIGHT:
|
||||
preferred_location = {width, half_height};
|
||||
break;
|
||||
case DockUtils::BOTTOM_LEFT:
|
||||
preferred_location = {0, height};
|
||||
break;
|
||||
case DockUtils::BOTTOM_MIDDLE:
|
||||
preferred_location = {half_width, height};
|
||||
break;
|
||||
case DockUtils::BOTTOM_RIGHT:
|
||||
preferred_location = {width, height};
|
||||
break;
|
||||
}
|
||||
|
||||
// Find the dock group which is closest to the preferred location.
|
||||
KDDockWidgets::Core::Group* best_group = nullptr;
|
||||
int best_distance_squared = 0;
|
||||
|
||||
for (KDDockWidgets::Core::Group* group_controller : KDDockWidgets::DockRegistry::self()->groups())
|
||||
{
|
||||
if (group_controller->isFloating())
|
||||
continue;
|
||||
|
||||
auto group = static_cast<KDDockWidgets::QtWidgets::Group*>(group_controller->view());
|
||||
|
||||
QPoint local_midpoint = group->pos() + QPoint(group->width() / 2, group->height() / 2);
|
||||
QPoint midpoint = group->mapTo(window, local_midpoint);
|
||||
QPoint delta = midpoint - preferred_location;
|
||||
int distance_squared = delta.x() * delta.x() + delta.y() * delta.y();
|
||||
|
||||
if (!best_group || distance_squared < best_distance_squared)
|
||||
{
|
||||
best_group = group_controller;
|
||||
best_distance_squared = distance_squared;
|
||||
}
|
||||
}
|
||||
|
||||
if (best_group && best_group->dockWidgetCount() > 0)
|
||||
{
|
||||
KDDockWidgets::Core::DockWidget* other_dock_widget = best_group->dockWidgetAt(0);
|
||||
other_dock_widget->addDockWidgetAsTab(dock_widget);
|
||||
}
|
||||
else
|
||||
{
|
||||
auto dock_view = static_cast<KDDockWidgets::QtWidgets::DockWidget*>(dock_widget->view());
|
||||
window->addDockWidget(dock_view, KDDockWidgets::Location_OnTop);
|
||||
}
|
||||
}
|
||||
40
pcsx2-qt/Debugger/Docking/DockUtils.h
Normal file
40
pcsx2-qt/Debugger/Docking/DockUtils.h
Normal file
@@ -0,0 +1,40 @@
|
||||
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
|
||||
// SPDX-License-Identifier: GPL-3.0+
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <kddockwidgets/KDDockWidgets.h>
|
||||
#include <kddockwidgets/core/DockWidget.h>
|
||||
#include <kddockwidgets/qtwidgets/MainWindow.h>
|
||||
|
||||
namespace DockUtils
|
||||
{
|
||||
inline const constexpr int MAX_LAYOUT_NAME_SIZE = 40;
|
||||
inline const constexpr int MAX_DOCK_WIDGET_NAME_SIZE = 40;
|
||||
|
||||
struct DockWidgetPair
|
||||
{
|
||||
KDDockWidgets::Core::DockWidget* controller = nullptr;
|
||||
KDDockWidgets::QtWidgets::DockWidget* view = nullptr;
|
||||
};
|
||||
|
||||
DockWidgetPair dockWidgetFromName(const QString& unique_name);
|
||||
|
||||
enum PreferredLocation
|
||||
{
|
||||
TOP_LEFT,
|
||||
TOP_MIDDLE,
|
||||
TOP_RIGHT,
|
||||
MIDDLE_LEFT,
|
||||
MIDDLE_MIDDLE,
|
||||
MIDDLE_RIGHT,
|
||||
BOTTOM_LEFT,
|
||||
BOTTOM_MIDDLE,
|
||||
BOTTOM_RIGHT
|
||||
};
|
||||
|
||||
void insertDockWidgetAtPreferredLocation(
|
||||
KDDockWidgets::Core::DockWidget* dock_widget,
|
||||
PreferredLocation location,
|
||||
KDDockWidgets::QtWidgets::MainWindow* window);
|
||||
} // namespace DockUtils
|
||||
314
pcsx2-qt/Debugger/Docking/DockViews.cpp
Normal file
314
pcsx2-qt/Debugger/Docking/DockViews.cpp
Normal file
@@ -0,0 +1,314 @@
|
||||
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
|
||||
// SPDX-License-Identifier: GPL-3.0+
|
||||
|
||||
#include "DockViews.h"
|
||||
|
||||
#include "QtUtils.h"
|
||||
#include "Debugger/DebuggerView.h"
|
||||
#include "Debugger/DebuggerWindow.h"
|
||||
#include "Debugger/Docking/DockManager.h"
|
||||
#include "Debugger/Docking/DropIndicators.h"
|
||||
|
||||
#include "DebugTools/DebugInterface.h"
|
||||
|
||||
#include <kddockwidgets/Config.h>
|
||||
#include <kddockwidgets/core/TabBar.h>
|
||||
#include <kddockwidgets/qtwidgets/views/DockWidget.h>
|
||||
|
||||
#include <QtGui/QActionGroup>
|
||||
#include <QtGui/QPalette>
|
||||
#include <QtWidgets/QInputDialog>
|
||||
#include <QtWidgets/QMessageBox>
|
||||
#include <QtWidgets/QMenu>
|
||||
#include <QtWidgets/QStyleFactory>
|
||||
|
||||
KDDockWidgets::Core::View* DockViewFactory::createDockWidget(
|
||||
const QString& unique_name,
|
||||
KDDockWidgets::DockWidgetOptions options,
|
||||
KDDockWidgets::LayoutSaverOptions layout_saver_options,
|
||||
Qt::WindowFlags window_flags) const
|
||||
{
|
||||
return new DockWidget(unique_name, options, layout_saver_options, window_flags);
|
||||
}
|
||||
|
||||
KDDockWidgets::Core::View* DockViewFactory::createTitleBar(
|
||||
KDDockWidgets::Core::TitleBar* controller,
|
||||
KDDockWidgets::Core::View* parent) const
|
||||
{
|
||||
return new DockTitleBar(controller, parent);
|
||||
}
|
||||
|
||||
KDDockWidgets::Core::View* DockViewFactory::createStack(
|
||||
KDDockWidgets::Core::Stack* controller,
|
||||
KDDockWidgets::Core::View* parent) const
|
||||
{
|
||||
return new DockStack(controller, KDDockWidgets::QtCommon::View_qt::asQWidget(parent));
|
||||
}
|
||||
|
||||
KDDockWidgets::Core::View* DockViewFactory::createTabBar(
|
||||
KDDockWidgets::Core::TabBar* tabBar,
|
||||
KDDockWidgets::Core::View* parent) const
|
||||
{
|
||||
return new DockTabBar(tabBar, KDDockWidgets::QtCommon::View_qt::asQWidget(parent));
|
||||
}
|
||||
|
||||
KDDockWidgets::Core::ClassicIndicatorWindowViewInterface* DockViewFactory::createClassicIndicatorWindow(
|
||||
KDDockWidgets::Core::ClassicDropIndicatorOverlay* classic_indicators,
|
||||
KDDockWidgets::Core::View* parent) const
|
||||
{
|
||||
return new DockDropIndicatorProxy(classic_indicators);
|
||||
}
|
||||
|
||||
KDDockWidgets::Core::ClassicIndicatorWindowViewInterface* DockViewFactory::createFallbackClassicIndicatorWindow(
|
||||
KDDockWidgets::Core::ClassicDropIndicatorOverlay* classic_indicators,
|
||||
KDDockWidgets::Core::View* parent) const
|
||||
{
|
||||
return KDDockWidgets::QtWidgets::ViewFactory::createClassicIndicatorWindow(classic_indicators, parent);
|
||||
}
|
||||
|
||||
KDDockWidgets::Core::View* DockViewFactory::createSegmentedDropIndicatorOverlayView(
|
||||
KDDockWidgets::Core::SegmentedDropIndicatorOverlay* controller,
|
||||
KDDockWidgets::Core::View* parent) const
|
||||
{
|
||||
return new DockSegmentedDropIndicatorOverlay(controller, KDDockWidgets::QtCommon::View_qt::asQWidget(parent));
|
||||
}
|
||||
|
||||
// *****************************************************************************
|
||||
|
||||
DockWidget::DockWidget(
|
||||
const QString& unique_name,
|
||||
KDDockWidgets::DockWidgetOptions options,
|
||||
KDDockWidgets::LayoutSaverOptions layout_saver_options,
|
||||
Qt::WindowFlags window_flags)
|
||||
: KDDockWidgets::QtWidgets::DockWidget(unique_name, options, layout_saver_options, window_flags)
|
||||
{
|
||||
connect(this, &DockWidget::isOpenChanged, this, &DockWidget::openStateChanged);
|
||||
}
|
||||
|
||||
void DockWidget::openStateChanged(bool open)
|
||||
{
|
||||
// The LayoutSaver class will close a bunch of dock widgets. We only want to
|
||||
// delete the dock widgets when they're being closed by the user.
|
||||
if (KDDockWidgets::LayoutSaver::restoreInProgress())
|
||||
return;
|
||||
|
||||
if (!open && g_debugger_window)
|
||||
g_debugger_window->dockManager().destroyDebuggerView(uniqueName());
|
||||
}
|
||||
|
||||
// *****************************************************************************
|
||||
|
||||
DockTitleBar::DockTitleBar(KDDockWidgets::Core::TitleBar* controller, KDDockWidgets::Core::View* parent)
|
||||
: KDDockWidgets::QtWidgets::TitleBar(controller, parent)
|
||||
{
|
||||
}
|
||||
|
||||
void DockTitleBar::mouseDoubleClickEvent(QMouseEvent* event)
|
||||
{
|
||||
if (g_debugger_window && !g_debugger_window->dockManager().isLayoutLocked())
|
||||
KDDockWidgets::QtWidgets::TitleBar::mouseDoubleClickEvent(event);
|
||||
else
|
||||
event->ignore();
|
||||
}
|
||||
|
||||
// *****************************************************************************
|
||||
|
||||
DockStack::DockStack(KDDockWidgets::Core::Stack* controller, QWidget* parent)
|
||||
: KDDockWidgets::QtWidgets::Stack(controller, parent)
|
||||
{
|
||||
}
|
||||
|
||||
void DockStack::init()
|
||||
{
|
||||
KDDockWidgets::QtWidgets::Stack::init();
|
||||
|
||||
if (g_debugger_window)
|
||||
{
|
||||
bool locked = g_debugger_window->dockManager().isLayoutLocked();
|
||||
setTabsClosable(!locked);
|
||||
}
|
||||
}
|
||||
|
||||
void DockStack::mouseDoubleClickEvent(QMouseEvent* ev)
|
||||
{
|
||||
if (g_debugger_window && !g_debugger_window->dockManager().isLayoutLocked())
|
||||
KDDockWidgets::QtWidgets::Stack::mouseDoubleClickEvent(ev);
|
||||
else
|
||||
ev->ignore();
|
||||
}
|
||||
|
||||
// *****************************************************************************
|
||||
|
||||
DockTabBar::DockTabBar(KDDockWidgets::Core::TabBar* controller, QWidget* parent)
|
||||
: KDDockWidgets::QtWidgets::TabBar(controller, parent)
|
||||
{
|
||||
setContextMenuPolicy(Qt::CustomContextMenu);
|
||||
connect(this, &DockTabBar::customContextMenuRequested, this, &DockTabBar::openContextMenu);
|
||||
|
||||
// The constructor of KDDockWidgets::QtWidgets::TabBar makes a QProxyStyle
|
||||
// that ends up taking ownerhsip of the style for the entire application!
|
||||
if (QProxyStyle* proxy_style = qobject_cast<QProxyStyle*>(style()))
|
||||
{
|
||||
if (proxy_style->baseStyle() == qApp->style())
|
||||
proxy_style->baseStyle()->setParent(qApp);
|
||||
|
||||
proxy_style->setBaseStyle(QStyleFactory::create(qApp->style()->name()));
|
||||
}
|
||||
}
|
||||
|
||||
void DockTabBar::openContextMenu(QPoint pos)
|
||||
{
|
||||
if (!g_debugger_window)
|
||||
return;
|
||||
|
||||
int tab_index = tabAt(pos);
|
||||
|
||||
// Filter out the placeholder widget displayed when there are no layouts.
|
||||
auto [widget, controller, view] = widgetsFromTabIndex(tab_index);
|
||||
if (!widget)
|
||||
return;
|
||||
|
||||
size_t dock_widgets_of_type = g_debugger_window->dockManager().countDebuggerViewsOfType(
|
||||
widget->metaObject()->className());
|
||||
|
||||
QMenu* menu = new QMenu(this);
|
||||
menu->setAttribute(Qt::WA_DeleteOnClose);
|
||||
|
||||
QAction* rename_action = menu->addAction(tr("Rename"));
|
||||
connect(rename_action, &QAction::triggered, this, [this, tab_index]() {
|
||||
if (!g_debugger_window)
|
||||
return;
|
||||
|
||||
auto [widget, controller, view] = widgetsFromTabIndex(tab_index);
|
||||
if (!widget)
|
||||
return;
|
||||
|
||||
bool ok;
|
||||
QString new_name = QInputDialog::getText(
|
||||
this, tr("Rename Window"), tr("New name:"), QLineEdit::Normal, widget->displayNameWithoutSuffix(), &ok);
|
||||
if (!ok)
|
||||
return;
|
||||
|
||||
if (!widget->setCustomDisplayName(new_name))
|
||||
{
|
||||
QMessageBox::warning(this, tr("Invalid Name"), tr("The specified name is too long."));
|
||||
return;
|
||||
}
|
||||
|
||||
g_debugger_window->dockManager().updateDockWidgetTitles();
|
||||
});
|
||||
|
||||
QAction* reset_name_action = menu->addAction(tr("Reset Name"));
|
||||
reset_name_action->setEnabled(!widget->customDisplayName().isEmpty());
|
||||
connect(reset_name_action, &QAction::triggered, this, [this, tab_index] {
|
||||
if (!g_debugger_window)
|
||||
return;
|
||||
|
||||
auto [widget, controller, view] = widgetsFromTabIndex(tab_index);
|
||||
if (!widget)
|
||||
return;
|
||||
|
||||
widget->setCustomDisplayName(QString());
|
||||
g_debugger_window->dockManager().updateDockWidgetTitles();
|
||||
});
|
||||
|
||||
QAction* primary_action = menu->addAction(tr("Primary"));
|
||||
primary_action->setCheckable(true);
|
||||
primary_action->setChecked(widget->isPrimary());
|
||||
primary_action->setEnabled(dock_widgets_of_type > 1);
|
||||
connect(primary_action, &QAction::triggered, this, [this, tab_index](bool checked) {
|
||||
if (!g_debugger_window)
|
||||
return;
|
||||
|
||||
auto [widget, controller, view] = widgetsFromTabIndex(tab_index);
|
||||
if (!widget)
|
||||
return;
|
||||
|
||||
g_debugger_window->dockManager().setPrimaryDebuggerView(widget, checked);
|
||||
});
|
||||
|
||||
QMenu* set_target_menu = menu->addMenu(tr("Set Target"));
|
||||
QActionGroup* set_target_group = new QActionGroup(menu);
|
||||
set_target_group->setExclusive(true);
|
||||
|
||||
for (BreakPointCpu cpu : DEBUG_CPUS)
|
||||
{
|
||||
const char* long_cpu_name = DebugInterface::longCpuName(cpu);
|
||||
const char* cpu_name = DebugInterface::cpuName(cpu);
|
||||
QString text = QString("%1 (%2)").arg(long_cpu_name).arg(cpu_name);
|
||||
|
||||
QAction* cpu_action = set_target_menu->addAction(text);
|
||||
cpu_action->setCheckable(true);
|
||||
cpu_action->setChecked(widget->cpuOverride().has_value() && *widget->cpuOverride() == cpu);
|
||||
connect(cpu_action, &QAction::triggered, this, [this, tab_index, cpu]() {
|
||||
setCpuOverrideForTab(tab_index, cpu);
|
||||
});
|
||||
set_target_group->addAction(cpu_action);
|
||||
}
|
||||
|
||||
set_target_menu->addSeparator();
|
||||
|
||||
QAction* inherit_action = set_target_menu->addAction(tr("Inherit From Layout"));
|
||||
inherit_action->setCheckable(true);
|
||||
inherit_action->setChecked(!widget->cpuOverride().has_value());
|
||||
connect(inherit_action, &QAction::triggered, this, [this, tab_index]() {
|
||||
setCpuOverrideForTab(tab_index, std::nullopt);
|
||||
});
|
||||
set_target_group->addAction(inherit_action);
|
||||
|
||||
QAction* close_action = menu->addAction(tr("Close"));
|
||||
connect(close_action, &QAction::triggered, this, [this, tab_index]() {
|
||||
if (!g_debugger_window)
|
||||
return;
|
||||
|
||||
auto [widget, controller, view] = widgetsFromTabIndex(tab_index);
|
||||
if (!widget)
|
||||
return;
|
||||
|
||||
g_debugger_window->dockManager().destroyDebuggerView(widget->uniqueName());
|
||||
});
|
||||
|
||||
menu->popup(mapToGlobal(pos));
|
||||
}
|
||||
|
||||
void DockTabBar::setCpuOverrideForTab(int tab_index, std::optional<BreakPointCpu> cpu_override)
|
||||
{
|
||||
if (!g_debugger_window)
|
||||
return;
|
||||
|
||||
auto [widget, controller, view] = widgetsFromTabIndex(tab_index);
|
||||
if (!widget)
|
||||
return;
|
||||
|
||||
if (!widget->setCpuOverride(cpu_override))
|
||||
g_debugger_window->dockManager().recreateDebuggerView(view->uniqueName());
|
||||
|
||||
g_debugger_window->dockManager().updateDockWidgetTitles();
|
||||
}
|
||||
|
||||
DockTabBar::WidgetsFromTabIndexResult DockTabBar::widgetsFromTabIndex(int tab_index)
|
||||
{
|
||||
KDDockWidgets::Core::TabBar* tab_bar_controller = asController<KDDockWidgets::Core::TabBar>();
|
||||
if (!tab_bar_controller)
|
||||
return {};
|
||||
|
||||
KDDockWidgets::Core::DockWidget* dock_controller = tab_bar_controller->dockWidgetAt(tab_index);
|
||||
if (!dock_controller)
|
||||
return {};
|
||||
|
||||
auto dock_view = static_cast<KDDockWidgets::QtWidgets::DockWidget*>(dock_controller->view());
|
||||
|
||||
DebuggerView* widget = qobject_cast<DebuggerView*>(dock_view->widget());
|
||||
if (!widget)
|
||||
return {};
|
||||
|
||||
return {widget, dock_controller, dock_view};
|
||||
}
|
||||
|
||||
void DockTabBar::mouseDoubleClickEvent(QMouseEvent* event)
|
||||
{
|
||||
if (g_debugger_window && !g_debugger_window->dockManager().isLayoutLocked())
|
||||
KDDockWidgets::QtWidgets::TabBar::mouseDoubleClickEvent(event);
|
||||
else
|
||||
event->ignore();
|
||||
}
|
||||
113
pcsx2-qt/Debugger/Docking/DockViews.h
Normal file
113
pcsx2-qt/Debugger/Docking/DockViews.h
Normal file
@@ -0,0 +1,113 @@
|
||||
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
|
||||
// SPDX-License-Identifier: GPL-3.0+
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "DebugTools/DebugInterface.h"
|
||||
|
||||
#include <kddockwidgets/qtwidgets/ViewFactory.h>
|
||||
#include <kddockwidgets/qtwidgets/views/DockWidget.h>
|
||||
#include <kddockwidgets/qtwidgets/views/Stack.h>
|
||||
#include <kddockwidgets/qtwidgets/views/TitleBar.h>
|
||||
#include <kddockwidgets/qtwidgets/views/TabBar.h>
|
||||
|
||||
class DebuggerView;
|
||||
class DockManager;
|
||||
|
||||
class DockViewFactory : public KDDockWidgets::QtWidgets::ViewFactory
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
KDDockWidgets::Core::View* createDockWidget(
|
||||
const QString& unique_name,
|
||||
KDDockWidgets::DockWidgetOptions options = {},
|
||||
KDDockWidgets::LayoutSaverOptions layout_saver_options = {},
|
||||
Qt::WindowFlags window_flags = {}) const override;
|
||||
|
||||
KDDockWidgets::Core::View* createTitleBar(
|
||||
KDDockWidgets::Core::TitleBar* controller,
|
||||
KDDockWidgets::Core::View* parent) const override;
|
||||
|
||||
KDDockWidgets::Core::View* createStack(
|
||||
KDDockWidgets::Core::Stack* controller,
|
||||
KDDockWidgets::Core::View* parent) const override;
|
||||
|
||||
KDDockWidgets::Core::View* createTabBar(
|
||||
KDDockWidgets::Core::TabBar* tabBar,
|
||||
KDDockWidgets::Core::View* parent) const override;
|
||||
|
||||
KDDockWidgets::Core::ClassicIndicatorWindowViewInterface* createClassicIndicatorWindow(
|
||||
KDDockWidgets::Core::ClassicDropIndicatorOverlay* classic_indicators,
|
||||
KDDockWidgets::Core::View* parent) const override;
|
||||
|
||||
KDDockWidgets::Core::ClassicIndicatorWindowViewInterface* createFallbackClassicIndicatorWindow(
|
||||
KDDockWidgets::Core::ClassicDropIndicatorOverlay* classic_indicators,
|
||||
KDDockWidgets::Core::View* parent) const;
|
||||
|
||||
KDDockWidgets::Core::View* createSegmentedDropIndicatorOverlayView(
|
||||
KDDockWidgets::Core::SegmentedDropIndicatorOverlay* controller,
|
||||
KDDockWidgets::Core::View* parent) const override;
|
||||
};
|
||||
|
||||
class DockWidget : public KDDockWidgets::QtWidgets::DockWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
DockWidget(
|
||||
const QString& unique_name,
|
||||
KDDockWidgets::DockWidgetOptions options,
|
||||
KDDockWidgets::LayoutSaverOptions layout_saver_options,
|
||||
Qt::WindowFlags window_flags);
|
||||
|
||||
protected:
|
||||
void openStateChanged(bool open);
|
||||
};
|
||||
|
||||
class DockTitleBar : public KDDockWidgets::QtWidgets::TitleBar
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
DockTitleBar(KDDockWidgets::Core::TitleBar* controller, KDDockWidgets::Core::View* parent = nullptr);
|
||||
|
||||
protected:
|
||||
void mouseDoubleClickEvent(QMouseEvent* event) override;
|
||||
};
|
||||
|
||||
class DockStack : public KDDockWidgets::QtWidgets::Stack
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
DockStack(KDDockWidgets::Core::Stack* controller, QWidget* parent = nullptr);
|
||||
|
||||
void init() override;
|
||||
|
||||
protected:
|
||||
void mouseDoubleClickEvent(QMouseEvent* event) override;
|
||||
};
|
||||
|
||||
class DockTabBar : public KDDockWidgets::QtWidgets::TabBar
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
DockTabBar(KDDockWidgets::Core::TabBar* controller, QWidget* parent = nullptr);
|
||||
|
||||
protected:
|
||||
void openContextMenu(QPoint pos);
|
||||
|
||||
struct WidgetsFromTabIndexResult
|
||||
{
|
||||
DebuggerView* widget = nullptr;
|
||||
KDDockWidgets::Core::DockWidget* controller = nullptr;
|
||||
KDDockWidgets::QtWidgets::DockWidget* view = nullptr;
|
||||
};
|
||||
|
||||
void setCpuOverrideForTab(int tab_index, std::optional<BreakPointCpu> cpu_override);
|
||||
WidgetsFromTabIndexResult widgetsFromTabIndex(int tab_index);
|
||||
|
||||
void mouseDoubleClickEvent(QMouseEvent* event) override;
|
||||
};
|
||||
572
pcsx2-qt/Debugger/Docking/DropIndicators.cpp
Normal file
572
pcsx2-qt/Debugger/Docking/DropIndicators.cpp
Normal file
@@ -0,0 +1,572 @@
|
||||
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
|
||||
// SPDX-License-Identifier: GPL-3.0+
|
||||
|
||||
#include "DropIndicators.h"
|
||||
|
||||
#include "QtUtils.h"
|
||||
#include "Debugger/Docking/DockViews.h"
|
||||
|
||||
#include "common/Assertions.h"
|
||||
|
||||
#include <kddockwidgets/Config.h>
|
||||
#include <kddockwidgets/core/Group.h>
|
||||
#include <kddockwidgets/core/Platform.h>
|
||||
#include <kddockwidgets/core/indicators/SegmentedDropIndicatorOverlay.h>
|
||||
#include <kddockwidgets/qtwidgets/ViewFactory.h>
|
||||
|
||||
#include <QtGui/QPainter>
|
||||
|
||||
static std::pair<QColor, QColor> pickNiceColours(const QPalette& palette, bool hovered)
|
||||
{
|
||||
QColor fill = palette.highlight().color();
|
||||
QColor outline = palette.highlight().color();
|
||||
|
||||
if (QtUtils::IsLightTheme(palette))
|
||||
{
|
||||
fill = fill.darker(200);
|
||||
outline = outline.darker(200);
|
||||
}
|
||||
else
|
||||
{
|
||||
fill = fill.lighter(200);
|
||||
outline = outline.lighter(200);
|
||||
}
|
||||
|
||||
fill.setAlpha(200);
|
||||
outline.setAlpha(255);
|
||||
|
||||
if (!hovered)
|
||||
{
|
||||
fill.setAlpha(fill.alpha() / 2);
|
||||
outline.setAlpha(outline.alpha() / 2);
|
||||
}
|
||||
|
||||
return {fill, outline};
|
||||
}
|
||||
|
||||
// *****************************************************************************
|
||||
|
||||
DockDropIndicatorProxy::DockDropIndicatorProxy(KDDockWidgets::Core::ClassicDropIndicatorOverlay* classic_indicators)
|
||||
: m_classic_indicators(classic_indicators)
|
||||
{
|
||||
recreateWindowIfNecessary();
|
||||
}
|
||||
|
||||
DockDropIndicatorProxy::~DockDropIndicatorProxy()
|
||||
{
|
||||
delete m_window;
|
||||
delete m_fallback_window;
|
||||
}
|
||||
|
||||
void DockDropIndicatorProxy::setObjectName(const QString& name)
|
||||
{
|
||||
window()->setObjectName(name);
|
||||
}
|
||||
|
||||
KDDockWidgets::DropLocation DockDropIndicatorProxy::hover(QPoint globalPos)
|
||||
{
|
||||
return window()->hover(globalPos);
|
||||
}
|
||||
|
||||
QPoint DockDropIndicatorProxy::posForIndicator(KDDockWidgets::DropLocation loc) const
|
||||
{
|
||||
return window()->posForIndicator(loc);
|
||||
}
|
||||
|
||||
void DockDropIndicatorProxy::updatePositions()
|
||||
{
|
||||
// Check if a compositor is running whenever a drag starts.
|
||||
recreateWindowIfNecessary();
|
||||
|
||||
window()->updatePositions();
|
||||
}
|
||||
|
||||
void DockDropIndicatorProxy::raise()
|
||||
{
|
||||
window()->raise();
|
||||
}
|
||||
|
||||
void DockDropIndicatorProxy::setVisible(bool visible)
|
||||
{
|
||||
window()->setVisible(visible);
|
||||
}
|
||||
|
||||
void DockDropIndicatorProxy::resize(QSize size)
|
||||
{
|
||||
window()->resize(size);
|
||||
}
|
||||
|
||||
void DockDropIndicatorProxy::setGeometry(QRect rect)
|
||||
{
|
||||
window()->setGeometry(rect);
|
||||
}
|
||||
|
||||
bool DockDropIndicatorProxy::isWindow() const
|
||||
{
|
||||
return window()->isWindow();
|
||||
}
|
||||
|
||||
void DockDropIndicatorProxy::updateIndicatorVisibility()
|
||||
{
|
||||
window()->updateIndicatorVisibility();
|
||||
}
|
||||
|
||||
KDDockWidgets::Core::ClassicIndicatorWindowViewInterface* DockDropIndicatorProxy::window()
|
||||
{
|
||||
if (!m_supports_compositing)
|
||||
{
|
||||
pxAssert(m_fallback_window);
|
||||
return m_fallback_window;
|
||||
}
|
||||
|
||||
pxAssert(m_window);
|
||||
return m_window;
|
||||
}
|
||||
|
||||
const KDDockWidgets::Core::ClassicIndicatorWindowViewInterface* DockDropIndicatorProxy::window() const
|
||||
{
|
||||
if (!m_supports_compositing)
|
||||
{
|
||||
pxAssert(m_fallback_window);
|
||||
return m_fallback_window;
|
||||
}
|
||||
|
||||
pxAssert(m_window);
|
||||
return m_window;
|
||||
}
|
||||
|
||||
void DockDropIndicatorProxy::recreateWindowIfNecessary()
|
||||
{
|
||||
bool supports_compositing = QtUtils::IsCompositorManagerRunning();
|
||||
if (supports_compositing == m_supports_compositing && (m_window || m_fallback_window))
|
||||
return;
|
||||
|
||||
m_supports_compositing = supports_compositing;
|
||||
|
||||
DockViewFactory* factory = static_cast<DockViewFactory*>(KDDockWidgets::Config::self().viewFactory());
|
||||
|
||||
if (supports_compositing)
|
||||
{
|
||||
if (!m_window)
|
||||
m_window = new DockDropIndicatorWindow(m_classic_indicators);
|
||||
|
||||
QWidget* old_window = dynamic_cast<QWidget*>(m_fallback_window);
|
||||
if (old_window)
|
||||
{
|
||||
m_window->setObjectName(old_window->objectName());
|
||||
m_window->setVisible(old_window->isVisible());
|
||||
m_window->setGeometry(old_window->geometry());
|
||||
}
|
||||
|
||||
delete m_fallback_window;
|
||||
m_fallback_window = nullptr;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!m_fallback_window)
|
||||
m_fallback_window = factory->createFallbackClassicIndicatorWindow(m_classic_indicators, nullptr);
|
||||
|
||||
QWidget* old_window = dynamic_cast<QWidget*>(m_window);
|
||||
if (old_window)
|
||||
{
|
||||
m_window->setObjectName(old_window->objectName());
|
||||
m_window->setVisible(old_window->isVisible());
|
||||
m_window->setGeometry(old_window->geometry());
|
||||
}
|
||||
|
||||
delete m_window;
|
||||
m_window = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
// *****************************************************************************
|
||||
|
||||
static const constexpr int IND_LEFT = 0;
|
||||
static const constexpr int IND_TOP = 1;
|
||||
static const constexpr int IND_RIGHT = 2;
|
||||
static const constexpr int IND_BOTTOM = 3;
|
||||
static const constexpr int IND_CENTER = 4;
|
||||
static const constexpr int IND_OUTER_LEFT = 5;
|
||||
static const constexpr int IND_OUTER_TOP = 6;
|
||||
static const constexpr int IND_OUTER_RIGHT = 7;
|
||||
static const constexpr int IND_OUTER_BOTTOM = 8;
|
||||
|
||||
static const constexpr int INDICATOR_SIZE = 40;
|
||||
static const constexpr int INDICATOR_MARGIN = 10;
|
||||
|
||||
static bool isWayland()
|
||||
{
|
||||
return KDDockWidgets::Core::Platform::instance()->displayType() ==
|
||||
KDDockWidgets::Core::Platform::DisplayType::Wayland;
|
||||
}
|
||||
|
||||
static QWidget* parentForIndicatorWindow(KDDockWidgets::Core::ClassicDropIndicatorOverlay* classic_indicators)
|
||||
{
|
||||
if (isWayland())
|
||||
return KDDockWidgets::QtCommon::View_qt::asQWidget(classic_indicators->view());
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
static Qt::WindowFlags flagsForIndicatorWindow()
|
||||
{
|
||||
if (isWayland())
|
||||
return Qt::Widget;
|
||||
|
||||
return Qt::Tool | Qt::BypassWindowManagerHint;
|
||||
}
|
||||
|
||||
DockDropIndicatorWindow::DockDropIndicatorWindow(
|
||||
KDDockWidgets::Core::ClassicDropIndicatorOverlay* classic_indicators)
|
||||
: QWidget(parentForIndicatorWindow(classic_indicators), flagsForIndicatorWindow())
|
||||
, m_classic_indicators(classic_indicators)
|
||||
, m_indicators({
|
||||
/* [IND_LEFT] = */ new DockDropIndicator(KDDockWidgets::DropLocation_Left, this),
|
||||
/* [IND_TOP] = */ new DockDropIndicator(KDDockWidgets::DropLocation_Top, this),
|
||||
/* [IND_RIGHT] = */ new DockDropIndicator(KDDockWidgets::DropLocation_Right, this),
|
||||
/* [IND_BOTTOM] = */ new DockDropIndicator(KDDockWidgets::DropLocation_Bottom, this),
|
||||
/* [IND_CENTER] = */ new DockDropIndicator(KDDockWidgets::DropLocation_Center, this),
|
||||
/* [IND_OUTER_LEFT] = */ new DockDropIndicator(KDDockWidgets::DropLocation_OutterLeft, this),
|
||||
/* [IND_OUTER_TOP] = */ new DockDropIndicator(KDDockWidgets::DropLocation_OutterTop, this),
|
||||
/* [IND_OUTER_RIGHT] = */ new DockDropIndicator(KDDockWidgets::DropLocation_OutterRight, this),
|
||||
/* [IND_OUTER_BOTTOM] = */ new DockDropIndicator(KDDockWidgets::DropLocation_OutterBottom, this),
|
||||
})
|
||||
{
|
||||
setWindowFlag(Qt::FramelessWindowHint, true);
|
||||
|
||||
if (KDDockWidgets::Config::self().flags() & KDDockWidgets::Config::Flag_KeepAboveIfNotUtilityWindow)
|
||||
setWindowFlag(Qt::WindowStaysOnTopHint, true);
|
||||
|
||||
setAttribute(Qt::WA_TranslucentBackground);
|
||||
}
|
||||
|
||||
void DockDropIndicatorWindow::setObjectName(const QString& name)
|
||||
{
|
||||
QWidget::setObjectName(name);
|
||||
}
|
||||
|
||||
KDDockWidgets::DropLocation DockDropIndicatorWindow::hover(QPoint globalPos)
|
||||
{
|
||||
KDDockWidgets::DropLocation result = KDDockWidgets::DropLocation_None;
|
||||
|
||||
for (DockDropIndicator* indicator : m_indicators)
|
||||
{
|
||||
if (indicator->isVisible())
|
||||
{
|
||||
bool hovered = indicator->rect().contains(indicator->mapFromGlobal(globalPos));
|
||||
if (hovered != indicator->hovered)
|
||||
{
|
||||
indicator->hovered = hovered;
|
||||
indicator->update();
|
||||
}
|
||||
|
||||
if (hovered)
|
||||
result = indicator->location;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
QPoint DockDropIndicatorWindow::posForIndicator(KDDockWidgets::DropLocation loc) const
|
||||
{
|
||||
for (DockDropIndicator* indicator : m_indicators)
|
||||
if (indicator->location == loc)
|
||||
return indicator->mapToGlobal(indicator->rect().center());
|
||||
|
||||
return QPoint();
|
||||
}
|
||||
|
||||
void DockDropIndicatorWindow::updatePositions()
|
||||
{
|
||||
DockDropIndicator* left = m_indicators[IND_LEFT];
|
||||
DockDropIndicator* top = m_indicators[IND_TOP];
|
||||
DockDropIndicator* right = m_indicators[IND_RIGHT];
|
||||
DockDropIndicator* bottom = m_indicators[IND_BOTTOM];
|
||||
DockDropIndicator* center = m_indicators[IND_CENTER];
|
||||
DockDropIndicator* outer_left = m_indicators[IND_OUTER_LEFT];
|
||||
DockDropIndicator* outer_top = m_indicators[IND_OUTER_TOP];
|
||||
DockDropIndicator* outer_right = m_indicators[IND_OUTER_RIGHT];
|
||||
DockDropIndicator* outer_bottom = m_indicators[IND_OUTER_BOTTOM];
|
||||
|
||||
QRect r = rect();
|
||||
int half_indicator_width = INDICATOR_SIZE / 2;
|
||||
|
||||
outer_left->move(r.x() + INDICATOR_MARGIN, r.center().y() - half_indicator_width);
|
||||
outer_bottom->move(r.center().x() - half_indicator_width, r.y() + height() - INDICATOR_SIZE - INDICATOR_MARGIN);
|
||||
outer_top->move(r.center().x() - half_indicator_width, r.y() + INDICATOR_MARGIN);
|
||||
outer_right->move(r.x() + width() - INDICATOR_SIZE - INDICATOR_MARGIN, r.center().y() - half_indicator_width);
|
||||
|
||||
KDDockWidgets::Core::Group* hovered_group = m_classic_indicators->hoveredGroup();
|
||||
if (hovered_group)
|
||||
{
|
||||
QRect hoveredRect = hovered_group->view()->geometry();
|
||||
center->move(r.topLeft() + hoveredRect.center() - QPoint(half_indicator_width, half_indicator_width));
|
||||
top->move(center->pos() - QPoint(0, INDICATOR_SIZE + INDICATOR_MARGIN));
|
||||
right->move(center->pos() + QPoint(INDICATOR_SIZE + INDICATOR_MARGIN, 0));
|
||||
bottom->move(center->pos() + QPoint(0, INDICATOR_SIZE + INDICATOR_MARGIN));
|
||||
left->move(center->pos() - QPoint(INDICATOR_SIZE + INDICATOR_MARGIN, 0));
|
||||
}
|
||||
}
|
||||
|
||||
void DockDropIndicatorWindow::raise()
|
||||
{
|
||||
QWidget::raise();
|
||||
}
|
||||
|
||||
void DockDropIndicatorWindow::setVisible(bool is)
|
||||
{
|
||||
QWidget::setVisible(is);
|
||||
}
|
||||
|
||||
void DockDropIndicatorWindow::resize(QSize size)
|
||||
{
|
||||
QWidget::resize(size);
|
||||
}
|
||||
|
||||
void DockDropIndicatorWindow::setGeometry(QRect rect)
|
||||
{
|
||||
QWidget::setGeometry(rect);
|
||||
}
|
||||
|
||||
bool DockDropIndicatorWindow::isWindow() const
|
||||
{
|
||||
return QWidget::isWindow();
|
||||
}
|
||||
|
||||
void DockDropIndicatorWindow::updateIndicatorVisibility()
|
||||
{
|
||||
for (DockDropIndicator* indicator : m_indicators)
|
||||
indicator->setVisible(m_classic_indicators->dropIndicatorVisible(indicator->location));
|
||||
}
|
||||
|
||||
void DockDropIndicatorWindow::resizeEvent(QResizeEvent* ev)
|
||||
{
|
||||
QWidget::resizeEvent(ev);
|
||||
updatePositions();
|
||||
}
|
||||
|
||||
// *****************************************************************************
|
||||
|
||||
DockDropIndicator::DockDropIndicator(KDDockWidgets::DropLocation loc, QWidget* parent)
|
||||
: QWidget(parent)
|
||||
, location(loc)
|
||||
{
|
||||
setFixedSize(INDICATOR_SIZE, INDICATOR_SIZE);
|
||||
setVisible(true);
|
||||
}
|
||||
|
||||
void DockDropIndicator::paintEvent(QPaintEvent* event)
|
||||
{
|
||||
QPainter painter(this);
|
||||
painter.setRenderHint(QPainter::Antialiasing, true);
|
||||
|
||||
auto [fill, outline] = pickNiceColours(palette(), hovered);
|
||||
|
||||
painter.setBrush(fill);
|
||||
|
||||
QPen pen;
|
||||
pen.setColor(outline);
|
||||
pen.setWidth(2);
|
||||
painter.setPen(pen);
|
||||
|
||||
painter.drawRect(rect());
|
||||
|
||||
QRectF rf = rect();
|
||||
|
||||
QRectF outer = rf.marginsRemoved(QMarginsF(4.f, 4.f, 4.f, 4.f));
|
||||
QPointF icon_position;
|
||||
switch (location)
|
||||
{
|
||||
case KDDockWidgets::DropLocation_Left:
|
||||
case KDDockWidgets::DropLocation_OutterLeft:
|
||||
outer = outer.marginsRemoved(QMarginsF(0.f, 0.f, outer.width() / 2.f, 0.f));
|
||||
icon_position = rf.marginsRemoved(QMarginsF(rf.width() / 2.f, 0.f, 0.f, 0.f)).center();
|
||||
break;
|
||||
case KDDockWidgets::DropLocation_Top:
|
||||
case KDDockWidgets::DropLocation_OutterTop:
|
||||
outer = outer.marginsRemoved(QMarginsF(0.f, 0.f, 0.f, outer.width() / 2.f));
|
||||
icon_position = rf.marginsRemoved(QMarginsF(0.f, rf.width() / 2.f, 0.f, 0.f)).center();
|
||||
break;
|
||||
case KDDockWidgets::DropLocation_Right:
|
||||
case KDDockWidgets::DropLocation_OutterRight:
|
||||
outer = outer.marginsRemoved(QMarginsF(outer.width() / 2.f, 0.f, 0.f, 0.f));
|
||||
icon_position = rf.marginsRemoved(QMarginsF(0.f, 0.f, rf.width() / 2.f, 0.f)).center();
|
||||
break;
|
||||
case KDDockWidgets::DropLocation_Bottom:
|
||||
case KDDockWidgets::DropLocation_OutterBottom:
|
||||
outer = outer.marginsRemoved(QMarginsF(0.f, outer.width() / 2.f, 0.f, 0.f));
|
||||
icon_position = rf.marginsRemoved(QMarginsF(0.f, 0.f, 0.f, rf.width() / 2.f)).center();
|
||||
break;
|
||||
default:
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
painter.drawRect(outer);
|
||||
|
||||
float arrow_size = INDICATOR_SIZE / 10.f;
|
||||
|
||||
QPolygonF arrow;
|
||||
switch (location)
|
||||
{
|
||||
case KDDockWidgets::DropLocation_Left:
|
||||
arrow = {
|
||||
icon_position + QPointF(-arrow_size, 0.f),
|
||||
icon_position + QPointF(arrow_size, arrow_size * 2.f),
|
||||
icon_position + QPointF(arrow_size, -arrow_size * 2.f),
|
||||
};
|
||||
break;
|
||||
case KDDockWidgets::DropLocation_Top:
|
||||
arrow = {
|
||||
icon_position + QPointF(0.f, -arrow_size),
|
||||
icon_position + QPointF(arrow_size * 2.f, arrow_size),
|
||||
icon_position + QPointF(-arrow_size * 2.f, arrow_size),
|
||||
};
|
||||
break;
|
||||
case KDDockWidgets::DropLocation_Right:
|
||||
arrow = {
|
||||
icon_position + QPointF(arrow_size, 0.f),
|
||||
icon_position + QPointF(-arrow_size, arrow_size * 2.f),
|
||||
icon_position + QPointF(-arrow_size, -arrow_size * 2.f),
|
||||
};
|
||||
break;
|
||||
case KDDockWidgets::DropLocation_Bottom:
|
||||
arrow = {
|
||||
icon_position + QPointF(0.f, arrow_size),
|
||||
icon_position + QPointF(arrow_size * 2.f, -arrow_size),
|
||||
icon_position + QPointF(-arrow_size * 2.f, -arrow_size),
|
||||
};
|
||||
break;
|
||||
default:
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
painter.drawPolygon(arrow);
|
||||
}
|
||||
|
||||
// *****************************************************************************
|
||||
|
||||
std::string DockSegmentedDropIndicatorOverlay::s_indicator_style;
|
||||
|
||||
DockSegmentedDropIndicatorOverlay::DockSegmentedDropIndicatorOverlay(
|
||||
KDDockWidgets::Core::SegmentedDropIndicatorOverlay* controller, QWidget* parent)
|
||||
: KDDockWidgets::QtWidgets::SegmentedDropIndicatorOverlay(controller, parent)
|
||||
{
|
||||
}
|
||||
|
||||
void DockSegmentedDropIndicatorOverlay::paintEvent(QPaintEvent* event)
|
||||
{
|
||||
if (s_indicator_style == "Minimalistic")
|
||||
drawMinimalistic();
|
||||
else
|
||||
drawSegmented();
|
||||
}
|
||||
|
||||
void DockSegmentedDropIndicatorOverlay::drawSegmented()
|
||||
{
|
||||
QPainter painter(this);
|
||||
painter.setRenderHint(QPainter::Antialiasing, true);
|
||||
|
||||
KDDockWidgets::Core::SegmentedDropIndicatorOverlay* controller =
|
||||
asController<KDDockWidgets::Core::SegmentedDropIndicatorOverlay>();
|
||||
|
||||
const std::unordered_map<KDDockWidgets::DropLocation, QPolygon>& segments = controller->segments();
|
||||
|
||||
for (KDDockWidgets::DropLocation location :
|
||||
{KDDockWidgets::DropLocation_Left,
|
||||
KDDockWidgets::DropLocation_Top,
|
||||
KDDockWidgets::DropLocation_Right,
|
||||
KDDockWidgets::DropLocation_Bottom,
|
||||
KDDockWidgets::DropLocation_Center,
|
||||
KDDockWidgets::DropLocation_OutterLeft,
|
||||
KDDockWidgets::DropLocation_OutterTop,
|
||||
KDDockWidgets::DropLocation_OutterRight,
|
||||
KDDockWidgets::DropLocation_OutterBottom})
|
||||
{
|
||||
auto segment = segments.find(location);
|
||||
if (segment == segments.end() || segment->second.size() < 2)
|
||||
continue;
|
||||
|
||||
bool hovered = segment->second.containsPoint(controller->hoveredPt(), Qt::OddEvenFill);
|
||||
auto [fill, outline] = pickNiceColours(palette(), hovered);
|
||||
|
||||
painter.setBrush(fill);
|
||||
|
||||
QPen pen(outline);
|
||||
pen.setWidth(1);
|
||||
painter.setPen(pen);
|
||||
|
||||
int margin = KDDockWidgets::Core::SegmentedDropIndicatorOverlay::s_segmentGirth * 2;
|
||||
|
||||
// Make sure the rectangles don't intersect with each other.
|
||||
QRect rect;
|
||||
switch (location)
|
||||
{
|
||||
case KDDockWidgets::DropLocation_Top:
|
||||
case KDDockWidgets::DropLocation_Bottom:
|
||||
case KDDockWidgets::DropLocation_OutterTop:
|
||||
case KDDockWidgets::DropLocation_OutterBottom:
|
||||
{
|
||||
rect = segment->second.boundingRect().marginsRemoved(QMargins(margin, 4, margin, 4));
|
||||
break;
|
||||
}
|
||||
case KDDockWidgets::DropLocation_Left:
|
||||
case KDDockWidgets::DropLocation_Right:
|
||||
case KDDockWidgets::DropLocation_OutterLeft:
|
||||
case KDDockWidgets::DropLocation_OutterRight:
|
||||
{
|
||||
rect = segment->second.boundingRect().marginsRemoved(QMargins(4, margin, 4, margin));
|
||||
break;
|
||||
}
|
||||
default:
|
||||
{
|
||||
rect = segment->second.boundingRect().marginsRemoved(QMargins(4, 4, 4, 4));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
painter.drawRect(rect);
|
||||
}
|
||||
}
|
||||
|
||||
void DockSegmentedDropIndicatorOverlay::drawMinimalistic()
|
||||
{
|
||||
QPainter painter(this);
|
||||
painter.setRenderHint(QPainter::Antialiasing, true);
|
||||
|
||||
KDDockWidgets::Core::SegmentedDropIndicatorOverlay* controller =
|
||||
asController<KDDockWidgets::Core::SegmentedDropIndicatorOverlay>();
|
||||
|
||||
const std::unordered_map<KDDockWidgets::DropLocation, QPolygon>& segments = controller->segments();
|
||||
|
||||
for (KDDockWidgets::DropLocation location :
|
||||
{KDDockWidgets::DropLocation_Left,
|
||||
KDDockWidgets::DropLocation_Top,
|
||||
KDDockWidgets::DropLocation_Right,
|
||||
KDDockWidgets::DropLocation_Bottom,
|
||||
KDDockWidgets::DropLocation_Center,
|
||||
KDDockWidgets::DropLocation_OutterLeft,
|
||||
KDDockWidgets::DropLocation_OutterTop,
|
||||
KDDockWidgets::DropLocation_OutterRight,
|
||||
KDDockWidgets::DropLocation_OutterBottom})
|
||||
{
|
||||
auto segment = segments.find(location);
|
||||
if (segment == segments.end() || segment->second.size() < 2)
|
||||
continue;
|
||||
|
||||
if (!segment->second.containsPoint(controller->hoveredPt(), Qt::OddEvenFill))
|
||||
continue;
|
||||
|
||||
auto [fill, outline] = pickNiceColours(palette(), true);
|
||||
|
||||
painter.setBrush(fill);
|
||||
|
||||
QPen pen(outline);
|
||||
pen.setWidth(1);
|
||||
painter.setPen(pen);
|
||||
|
||||
painter.drawRect(segment->second.boundingRect());
|
||||
}
|
||||
}
|
||||
108
pcsx2-qt/Debugger/Docking/DropIndicators.h
Normal file
108
pcsx2-qt/Debugger/Docking/DropIndicators.h
Normal file
@@ -0,0 +1,108 @@
|
||||
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
|
||||
// SPDX-License-Identifier: GPL-3.0+
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <kddockwidgets/core/indicators/ClassicDropIndicatorOverlay.h>
|
||||
#include <kddockwidgets/core/views/ClassicIndicatorWindowViewInterface.h>
|
||||
#include <kddockwidgets/qtwidgets/views/SegmentedDropIndicatorOverlay.h>
|
||||
|
||||
class DockDropIndicator;
|
||||
|
||||
// This switches between our custom drop indicators and KDDockWidget's built-in
|
||||
// ones on the fly depending on whether or not we have a windowing system that
|
||||
// supports compositing.
|
||||
class DockDropIndicatorProxy : public KDDockWidgets::Core::ClassicIndicatorWindowViewInterface
|
||||
{
|
||||
public:
|
||||
DockDropIndicatorProxy(KDDockWidgets::Core::ClassicDropIndicatorOverlay* classic_indicators);
|
||||
~DockDropIndicatorProxy();
|
||||
|
||||
void setObjectName(const QString&) override;
|
||||
KDDockWidgets::DropLocation hover(QPoint globalPos) override;
|
||||
QPoint posForIndicator(KDDockWidgets::DropLocation) const override;
|
||||
void updatePositions() override;
|
||||
void raise() override;
|
||||
void setVisible(bool visible) override;
|
||||
void resize(QSize size) override;
|
||||
void setGeometry(QRect rect) override;
|
||||
bool isWindow() const override;
|
||||
void updateIndicatorVisibility() override;
|
||||
|
||||
private:
|
||||
KDDockWidgets::Core::ClassicIndicatorWindowViewInterface* window();
|
||||
const KDDockWidgets::Core::ClassicIndicatorWindowViewInterface* window() const;
|
||||
|
||||
void recreateWindowIfNecessary();
|
||||
|
||||
KDDockWidgets::Core::ClassicIndicatorWindowViewInterface* m_window = nullptr;
|
||||
KDDockWidgets::Core::ClassicIndicatorWindowViewInterface* m_fallback_window = nullptr;
|
||||
|
||||
bool m_supports_compositing = true;
|
||||
KDDockWidgets::Core::ClassicDropIndicatorOverlay* m_classic_indicators = nullptr;
|
||||
};
|
||||
|
||||
// Our default custom drop indicator implementation. This fits in with PCSX2's
|
||||
// themes a lot better, but doesn't support windowing systems where compositing
|
||||
// is disabled (it would show a black screen).
|
||||
class DockDropIndicatorWindow : public QWidget, public KDDockWidgets::Core::ClassicIndicatorWindowViewInterface
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
DockDropIndicatorWindow(
|
||||
KDDockWidgets::Core::ClassicDropIndicatorOverlay* classic_indicators);
|
||||
|
||||
void setObjectName(const QString& name) override;
|
||||
KDDockWidgets::DropLocation hover(QPoint globalPos) override;
|
||||
QPoint posForIndicator(KDDockWidgets::DropLocation loc) const override;
|
||||
void updatePositions() override;
|
||||
void raise() override;
|
||||
void setVisible(bool visible) override;
|
||||
void resize(QSize size) override;
|
||||
void setGeometry(QRect rect) override;
|
||||
bool isWindow() const override;
|
||||
void updateIndicatorVisibility() override;
|
||||
|
||||
protected:
|
||||
void resizeEvent(QResizeEvent* ev) override;
|
||||
|
||||
private:
|
||||
KDDockWidgets::Core::ClassicDropIndicatorOverlay* m_classic_indicators;
|
||||
std::vector<DockDropIndicator*> m_indicators;
|
||||
};
|
||||
|
||||
class DockDropIndicator : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
DockDropIndicator(KDDockWidgets::DropLocation loc, QWidget* parent = nullptr);
|
||||
|
||||
KDDockWidgets::DropLocation location;
|
||||
bool hovered = false;
|
||||
|
||||
protected:
|
||||
void paintEvent(QPaintEvent* event) override;
|
||||
};
|
||||
|
||||
// An alternative drop indicator design that can be enabled from the settings
|
||||
// menu. For this one we don't need to worry about whether compositing is
|
||||
// supported since it doesn't create its own window.
|
||||
class DockSegmentedDropIndicatorOverlay : public KDDockWidgets::QtWidgets::SegmentedDropIndicatorOverlay
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
DockSegmentedDropIndicatorOverlay(
|
||||
KDDockWidgets::Core::SegmentedDropIndicatorOverlay* controller, QWidget* parent = nullptr);
|
||||
|
||||
static std::string s_indicator_style;
|
||||
|
||||
protected:
|
||||
void paintEvent(QPaintEvent* event) override;
|
||||
|
||||
private:
|
||||
void drawSegmented();
|
||||
void drawMinimalistic();
|
||||
};
|
||||
107
pcsx2-qt/Debugger/Docking/LayoutEditorDialog.cpp
Normal file
107
pcsx2-qt/Debugger/Docking/LayoutEditorDialog.cpp
Normal file
@@ -0,0 +1,107 @@
|
||||
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
|
||||
// SPDX-License-Identifier: GPL-3.0+
|
||||
|
||||
#include "LayoutEditorDialog.h"
|
||||
|
||||
#include "Debugger/Docking/DockTables.h"
|
||||
|
||||
#include <QtWidgets/QPushButton>
|
||||
|
||||
Q_DECLARE_METATYPE(LayoutEditorDialog::InitialState);
|
||||
|
||||
LayoutEditorDialog::LayoutEditorDialog(NameValidator name_validator, bool can_clone_current_layout, QWidget* parent)
|
||||
: QDialog(parent)
|
||||
, m_name_validator(name_validator)
|
||||
{
|
||||
m_ui.setupUi(this);
|
||||
|
||||
setWindowTitle(tr("New Layout"));
|
||||
|
||||
setupInputWidgets(BREAKPOINT_EE, can_clone_current_layout);
|
||||
|
||||
onNameChanged();
|
||||
}
|
||||
|
||||
LayoutEditorDialog::LayoutEditorDialog(
|
||||
const QString& name, BreakPointCpu cpu, NameValidator name_validator, QWidget* parent)
|
||||
: QDialog(parent)
|
||||
, m_name_validator(name_validator)
|
||||
{
|
||||
m_ui.setupUi(this);
|
||||
|
||||
setWindowTitle(tr("Edit Layout"));
|
||||
|
||||
m_ui.nameEditor->setText(name);
|
||||
|
||||
setupInputWidgets(cpu, {});
|
||||
|
||||
m_ui.initialStateLabel->hide();
|
||||
m_ui.initialStateEditor->hide();
|
||||
|
||||
onNameChanged();
|
||||
}
|
||||
|
||||
QString LayoutEditorDialog::name()
|
||||
{
|
||||
return m_ui.nameEditor->text();
|
||||
}
|
||||
|
||||
BreakPointCpu LayoutEditorDialog::cpu()
|
||||
{
|
||||
return static_cast<BreakPointCpu>(m_ui.cpuEditor->currentData().toInt());
|
||||
}
|
||||
|
||||
LayoutEditorDialog::InitialState LayoutEditorDialog::initialState()
|
||||
{
|
||||
return m_ui.initialStateEditor->currentData().value<InitialState>();
|
||||
}
|
||||
|
||||
void LayoutEditorDialog::setupInputWidgets(BreakPointCpu cpu, bool can_clone_current_layout)
|
||||
{
|
||||
connect(m_ui.nameEditor, &QLineEdit::textChanged, this, &LayoutEditorDialog::onNameChanged);
|
||||
|
||||
for (BreakPointCpu cpu : DEBUG_CPUS)
|
||||
{
|
||||
const char* long_cpu_name = DebugInterface::longCpuName(cpu);
|
||||
const char* cpu_name = DebugInterface::cpuName(cpu);
|
||||
QString text = QString("%1 (%2)").arg(long_cpu_name).arg(cpu_name);
|
||||
m_ui.cpuEditor->addItem(text, cpu);
|
||||
}
|
||||
|
||||
for (int i = 0; i < m_ui.cpuEditor->count(); i++)
|
||||
if (m_ui.cpuEditor->itemData(i).toInt() == cpu)
|
||||
m_ui.cpuEditor->setCurrentIndex(i);
|
||||
|
||||
for (size_t i = 0; i < DockTables::DEFAULT_DOCK_LAYOUTS.size(); i++)
|
||||
m_ui.initialStateEditor->addItem(
|
||||
tr("Create Default \"%1\" Layout").arg(tr(DockTables::DEFAULT_DOCK_LAYOUTS[i].name.c_str())),
|
||||
QVariant::fromValue(InitialState(DEFAULT_LAYOUT, i)));
|
||||
|
||||
m_ui.initialStateEditor->addItem(tr("Create Blank Layout"), QVariant::fromValue(InitialState(BLANK_LAYOUT, 0)));
|
||||
|
||||
if (can_clone_current_layout)
|
||||
m_ui.initialStateEditor->addItem(tr("Clone Current Layout"), QVariant::fromValue(InitialState(CLONE_LAYOUT, 0)));
|
||||
|
||||
m_ui.initialStateEditor->setCurrentIndex(0);
|
||||
}
|
||||
|
||||
void LayoutEditorDialog::onNameChanged()
|
||||
{
|
||||
QString error_message;
|
||||
|
||||
if (m_ui.nameEditor->text().isEmpty())
|
||||
{
|
||||
error_message = tr("Name is empty.");
|
||||
}
|
||||
else if (m_ui.nameEditor->text().size() > DockUtils::MAX_LAYOUT_NAME_SIZE)
|
||||
{
|
||||
error_message = tr("Name too long.");
|
||||
}
|
||||
else if (!m_name_validator(m_ui.nameEditor->text()))
|
||||
{
|
||||
error_message = tr("A layout with that name already exists.");
|
||||
}
|
||||
|
||||
m_ui.buttonBox->button(QDialogButtonBox::Ok)->setEnabled(error_message.isEmpty());
|
||||
m_ui.errorMessage->setText(error_message);
|
||||
}
|
||||
45
pcsx2-qt/Debugger/Docking/LayoutEditorDialog.h
Normal file
45
pcsx2-qt/Debugger/Docking/LayoutEditorDialog.h
Normal file
@@ -0,0 +1,45 @@
|
||||
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
|
||||
// SPDX-License-Identifier: GPL-3.0+
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "ui_LayoutEditorDialog.h"
|
||||
|
||||
#include "DebugTools/DebugInterface.h"
|
||||
|
||||
#include <QtWidgets/QDialog>
|
||||
|
||||
class LayoutEditorDialog : public QDialog
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
using NameValidator = std::function<bool(const QString&)>;
|
||||
|
||||
enum CreationMode
|
||||
{
|
||||
DEFAULT_LAYOUT,
|
||||
BLANK_LAYOUT,
|
||||
CLONE_LAYOUT,
|
||||
};
|
||||
|
||||
// Bundles together a creation mode and inital state.
|
||||
using InitialState = std::pair<CreationMode, size_t>;
|
||||
|
||||
// Create a "New Layout" dialog.
|
||||
LayoutEditorDialog(NameValidator name_validator, bool can_clone_current_layout, QWidget* parent = nullptr);
|
||||
|
||||
// Create a "Edit Layout" dialog.
|
||||
LayoutEditorDialog(const QString& name, BreakPointCpu cpu, NameValidator name_validator, QWidget* parent = nullptr);
|
||||
|
||||
QString name();
|
||||
BreakPointCpu cpu();
|
||||
InitialState initialState();
|
||||
|
||||
private:
|
||||
void setupInputWidgets(BreakPointCpu cpu, bool can_clone_current_layout);
|
||||
void onNameChanged();
|
||||
|
||||
Ui::LayoutEditorDialog m_ui;
|
||||
NameValidator m_name_validator;
|
||||
};
|
||||
115
pcsx2-qt/Debugger/Docking/LayoutEditorDialog.ui
Normal file
115
pcsx2-qt/Debugger/Docking/LayoutEditorDialog.ui
Normal file
@@ -0,0 +1,115 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>LayoutEditorDialog</class>
|
||||
<widget class="QDialog" name="LayoutEditorDialog">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>400</width>
|
||||
<height>150</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string/>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<layout class="QFormLayout" name="formLayout">
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="nameLabel">
|
||||
<property name="text">
|
||||
<string>Name</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QLabel" name="cpuLabel">
|
||||
<property name="text">
|
||||
<string>Target</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="0">
|
||||
<widget class="QLabel" name="initialStateLabel">
|
||||
<property name="text">
|
||||
<string>Initial State</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<widget class="QComboBox" name="cpuEditor"/>
|
||||
</item>
|
||||
<item row="0" column="1">
|
||||
<widget class="QLineEdit" name="nameEditor"/>
|
||||
</item>
|
||||
<item row="2" column="1">
|
||||
<widget class="QComboBox" name="initialStateEditor"/>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<item>
|
||||
<widget class="QLabel" name="errorMessage">
|
||||
<property name="styleSheet">
|
||||
<string notr="true">color: red</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QDialogButtonBox" name="buttonBox">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="standardButtons">
|
||||
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections>
|
||||
<connection>
|
||||
<sender>buttonBox</sender>
|
||||
<signal>accepted()</signal>
|
||||
<receiver>LayoutEditorDialog</receiver>
|
||||
<slot>accept()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>248</x>
|
||||
<y>254</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>157</x>
|
||||
<y>274</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
<connection>
|
||||
<sender>buttonBox</sender>
|
||||
<signal>rejected()</signal>
|
||||
<receiver>LayoutEditorDialog</receiver>
|
||||
<slot>reject()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>316</x>
|
||||
<y>260</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>286</x>
|
||||
<y>274</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
</connections>
|
||||
</ui>
|
||||
15
pcsx2-qt/Debugger/Docking/NoLayoutsWidget.cpp
Normal file
15
pcsx2-qt/Debugger/Docking/NoLayoutsWidget.cpp
Normal file
@@ -0,0 +1,15 @@
|
||||
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
|
||||
// SPDX-License-Identifier: GPL-3.0+
|
||||
|
||||
#include "NoLayoutsWidget.h"
|
||||
|
||||
NoLayoutsWidget::NoLayoutsWidget(QWidget* parent)
|
||||
: QWidget(parent)
|
||||
{
|
||||
m_ui.setupUi(this);
|
||||
}
|
||||
|
||||
QPushButton* NoLayoutsWidget::createDefaultLayoutsButton()
|
||||
{
|
||||
return m_ui.createDefaultLayoutsButton;
|
||||
}
|
||||
21
pcsx2-qt/Debugger/Docking/NoLayoutsWidget.h
Normal file
21
pcsx2-qt/Debugger/Docking/NoLayoutsWidget.h
Normal file
@@ -0,0 +1,21 @@
|
||||
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
|
||||
// SPDX-License-Identifier: GPL-3.0+
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "ui_NoLayoutsWidget.h"
|
||||
|
||||
#include <QtWidgets/QPushButton>
|
||||
|
||||
class NoLayoutsWidget : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
NoLayoutsWidget(QWidget* parent = nullptr);
|
||||
|
||||
QPushButton* createDefaultLayoutsButton();
|
||||
|
||||
private:
|
||||
Ui::NoLayoutsWidget m_ui;
|
||||
};
|
||||
97
pcsx2-qt/Debugger/Docking/NoLayoutsWidget.ui
Normal file
97
pcsx2-qt/Debugger/Docking/NoLayoutsWidget.ui
Normal file
@@ -0,0 +1,97 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>NoLayoutsWidget</class>
|
||||
<widget class="QWidget" name="NoLayoutsWidget">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>400</width>
|
||||
<height>300</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Form</string>
|
||||
</property>
|
||||
<property name="autoFillBackground">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<spacer name="topSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>40</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="text">
|
||||
<string>There are no layouts.</string>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignCenter</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<item>
|
||||
<spacer name="leftSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="createDefaultLayoutsButton">
|
||||
<property name="text">
|
||||
<string>Create Default Layouts</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="rightSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="bottomSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>40</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
||||
39
pcsx2-qt/Debugger/JsonValueWrapper.h
Normal file
39
pcsx2-qt/Debugger/JsonValueWrapper.h
Normal file
@@ -0,0 +1,39 @@
|
||||
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
|
||||
// SPDX-License-Identifier: GPL-3.0+
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "rapidjson/document.h"
|
||||
|
||||
// Container for a JSON value. This exists solely so that we can forward declare
|
||||
// it to avoid pulling in rapidjson for the entire debugger.
|
||||
class JsonValueWrapper
|
||||
{
|
||||
public:
|
||||
JsonValueWrapper(
|
||||
rapidjson::Value& value,
|
||||
rapidjson::MemoryPoolAllocator<rapidjson::CrtAllocator>& allocator)
|
||||
: m_value(value)
|
||||
, m_allocator(allocator)
|
||||
{
|
||||
}
|
||||
|
||||
rapidjson::Value& value()
|
||||
{
|
||||
return m_value;
|
||||
}
|
||||
|
||||
const rapidjson::Value& value() const
|
||||
{
|
||||
return m_value;
|
||||
}
|
||||
|
||||
rapidjson::MemoryPoolAllocator<rapidjson::CrtAllocator>& allocator()
|
||||
{
|
||||
return m_allocator;
|
||||
}
|
||||
|
||||
private:
|
||||
rapidjson::Value& m_value;
|
||||
rapidjson::MemoryPoolAllocator<rapidjson::CrtAllocator>& m_allocator;
|
||||
};
|
||||
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>
|
||||
467
pcsx2-qt/Debugger/RegisterView.cpp
Normal file
467
pcsx2-qt/Debugger/RegisterView.cpp
Normal file
@@ -0,0 +1,467 @@
|
||||
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
|
||||
// SPDX-License-Identifier: GPL-3.0+
|
||||
|
||||
#include "RegisterView.h"
|
||||
|
||||
#include "Debugger/JsonValueWrapper.h"
|
||||
|
||||
#include "QtUtils.h"
|
||||
#include <QtGui/QMouseEvent>
|
||||
#include <QtWidgets/QTabBar>
|
||||
#include <QtWidgets/QStylePainter>
|
||||
#include <QtWidgets/QStyleOptionTab>
|
||||
#include <QtGui/QClipboard>
|
||||
#include <QtWidgets/QInputDialog>
|
||||
#include <QtWidgets/QProxyStyle>
|
||||
#include <QtWidgets/QMessageBox>
|
||||
|
||||
#include <bit>
|
||||
|
||||
#define CAT_SHOW_FLOAT (categoryIndex == EECAT_FPR && m_showFPRFloat) || (categoryIndex == EECAT_VU0F && m_showVU0FFloat)
|
||||
|
||||
using namespace QtUtils;
|
||||
|
||||
RegisterView::RegisterView(const DebuggerViewParameters& parameters)
|
||||
: DebuggerView(parameters, MONOSPACE_FONT)
|
||||
{
|
||||
this->setContextMenuPolicy(Qt::ContextMenuPolicy::CustomContextMenu);
|
||||
|
||||
ui.setupUi(this);
|
||||
ui.registerTabs->setDrawBase(false);
|
||||
|
||||
connect(this, &RegisterView::customContextMenuRequested, this, &RegisterView::customMenuRequested);
|
||||
connect(ui.registerTabs, &QTabBar::currentChanged, this, &RegisterView::tabCurrentChanged);
|
||||
|
||||
for (int i = 0; i < cpu().getRegisterCategoryCount(); i++)
|
||||
{
|
||||
ui.registerTabs->addTab(cpu().getRegisterCategoryName(i));
|
||||
}
|
||||
|
||||
connect(ui.registerTabs, &QTabBar::currentChanged, [this]() { this->repaint(); });
|
||||
|
||||
receiveEvent<DebuggerEvents::Refresh>([this](const DebuggerEvents::Refresh& event) -> bool {
|
||||
update();
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
RegisterView::~RegisterView()
|
||||
{
|
||||
}
|
||||
|
||||
void RegisterView::toJson(JsonValueWrapper& json)
|
||||
{
|
||||
DebuggerView::toJson(json);
|
||||
|
||||
json.value().AddMember("showVU0FFloat", m_showVU0FFloat, json.allocator());
|
||||
json.value().AddMember("showFPRFloat", m_showFPRFloat, json.allocator());
|
||||
}
|
||||
|
||||
bool RegisterView::fromJson(const JsonValueWrapper& json)
|
||||
{
|
||||
if (!DebuggerView::fromJson(json))
|
||||
return false;
|
||||
|
||||
auto show_vu0f_float = json.value().FindMember("showVU0FFloat");
|
||||
if (show_vu0f_float != json.value().MemberEnd() && show_vu0f_float->value.IsBool())
|
||||
m_showVU0FFloat = show_vu0f_float->value.GetBool();
|
||||
|
||||
auto show_fpr_float = json.value().FindMember("showFPRFloat");
|
||||
if (show_fpr_float != json.value().MemberEnd() && show_fpr_float->value.IsBool())
|
||||
m_showFPRFloat = show_fpr_float->value.GetBool();
|
||||
|
||||
repaint();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void RegisterView::tabCurrentChanged(int cur)
|
||||
{
|
||||
m_rowStart = 0;
|
||||
}
|
||||
|
||||
void RegisterView::paintEvent(QPaintEvent* event)
|
||||
{
|
||||
QPainter painter(this);
|
||||
painter.setPen(this->palette().text().color());
|
||||
m_renderStart = QPoint(0, ui.registerTabs->pos().y() + ui.registerTabs->size().height());
|
||||
const QSize renderSize = QSize(this->size().width(), this->size().height() - ui.registerTabs->size().height());
|
||||
|
||||
m_rowHeight = painter.fontMetrics().height() + 2;
|
||||
m_rowEnd = m_rowStart + (renderSize.height() / m_rowHeight) - 1; // Maybe move this to a onsize event
|
||||
|
||||
bool alternate = m_rowStart % 2;
|
||||
|
||||
const int categoryIndex = ui.registerTabs->currentIndex();
|
||||
|
||||
// Used for 128 bit and VU0f registers
|
||||
const int titleStartX = m_renderStart.x() + (painter.fontMetrics().averageCharWidth() * 6);
|
||||
m_fieldWidth = ((renderSize.width() - (painter.fontMetrics().averageCharWidth() * 6)) / 4);
|
||||
|
||||
m_fieldStartX[0] = titleStartX;
|
||||
m_fieldStartX[1] = titleStartX + m_fieldWidth;
|
||||
m_fieldStartX[2] = titleStartX + (m_fieldWidth * 2);
|
||||
m_fieldStartX[3] = titleStartX + (m_fieldWidth * 3);
|
||||
|
||||
if (categoryIndex == EECAT_VU0F)
|
||||
{
|
||||
painter.fillRect(m_renderStart.x(), m_renderStart.y(), renderSize.width(), m_rowHeight, this->palette().highlight());
|
||||
|
||||
painter.drawText(m_fieldStartX[0], m_renderStart.y(), m_fieldWidth, m_rowHeight, Qt::AlignLeft, "W");
|
||||
painter.drawText(m_fieldStartX[1], m_renderStart.y(), m_fieldWidth, m_rowHeight, Qt::AlignLeft, "Z");
|
||||
painter.drawText(m_fieldStartX[2], m_renderStart.y(), m_fieldWidth, m_rowHeight, Qt::AlignLeft, "Y");
|
||||
painter.drawText(m_fieldStartX[3], m_renderStart.y(), m_fieldWidth, m_rowHeight, Qt::AlignLeft, "X");
|
||||
|
||||
m_renderStart += QPoint(0, m_rowHeight); // Make room for VU0f titles
|
||||
}
|
||||
|
||||
// Find the longest register name and calculate where to place our values
|
||||
// off of that.
|
||||
// Can probably constexpr the loop out as register names are known during runtime
|
||||
int safeValueStartX = 0;
|
||||
for (int i = 0; i < cpu().getRegisterCount(categoryIndex); i++)
|
||||
{
|
||||
const int registerNameWidth = strlen(cpu().getRegisterName(categoryIndex, i));
|
||||
if (safeValueStartX < registerNameWidth)
|
||||
{
|
||||
safeValueStartX = registerNameWidth;
|
||||
}
|
||||
}
|
||||
|
||||
// Add a space between the value and name
|
||||
safeValueStartX += 2;
|
||||
// Convert to width in pixels
|
||||
safeValueStartX *= painter.fontMetrics().averageCharWidth();
|
||||
// Make it relative to where we start rendering
|
||||
safeValueStartX += m_renderStart.x();
|
||||
|
||||
for (s32 i = 0; i < cpu().getRegisterCount(categoryIndex) - m_rowStart; i++)
|
||||
{
|
||||
const s32 registerIndex = i + m_rowStart;
|
||||
const int yStart = (i * m_rowHeight) + m_renderStart.y();
|
||||
|
||||
painter.fillRect(m_renderStart.x(), yStart, renderSize.width(), m_rowHeight, alternate ? this->palette().base() : this->palette().alternateBase());
|
||||
alternate = !alternate;
|
||||
|
||||
// Draw register name
|
||||
painter.setPen(this->palette().text().color());
|
||||
painter.drawText(m_renderStart.x() + painter.fontMetrics().averageCharWidth(), yStart, renderSize.width(), m_rowHeight, Qt::AlignLeft, cpu().getRegisterName(categoryIndex, registerIndex));
|
||||
|
||||
if (cpu().getRegisterSize(categoryIndex) == 128)
|
||||
{
|
||||
const u128 curRegister = cpu().getRegister(categoryIndex, registerIndex);
|
||||
|
||||
int regIndex = 3;
|
||||
for (int j = 0; j < 4; j++)
|
||||
{
|
||||
if (m_selectedRow == registerIndex && m_selected128Field == j)
|
||||
painter.setPen(this->palette().highlight().color());
|
||||
else
|
||||
painter.setPen(this->palette().text().color());
|
||||
|
||||
if (categoryIndex == EECAT_VU0F && m_showVU0FFloat)
|
||||
painter.drawText(m_fieldStartX[j], yStart, m_fieldWidth, m_rowHeight, Qt::AlignLeft,
|
||||
painter.fontMetrics().elidedText(QString::number(std::bit_cast<float>(cpu().getRegister(categoryIndex, registerIndex)._u32[regIndex])), Qt::ElideRight, m_fieldWidth - painter.fontMetrics().averageCharWidth()));
|
||||
else
|
||||
painter.drawText(m_fieldStartX[j], yStart, m_fieldWidth, m_rowHeight,
|
||||
Qt::AlignLeft, FilledQStringFromValue(curRegister._u32[regIndex], 16));
|
||||
regIndex--;
|
||||
}
|
||||
painter.setPen(this->palette().text().color());
|
||||
}
|
||||
else
|
||||
{
|
||||
if (m_selectedRow == registerIndex)
|
||||
painter.setPen(this->palette().highlight().color());
|
||||
else
|
||||
painter.setPen(this->palette().text().color());
|
||||
|
||||
if (categoryIndex == EECAT_FPR && m_showFPRFloat)
|
||||
painter.drawText(safeValueStartX, yStart, renderSize.width(), m_rowHeight, Qt::AlignLeft,
|
||||
QString("%1").arg(QString::number(std::bit_cast<float>(cpu().getRegister(categoryIndex, registerIndex)._u32[0]))).toUpper());
|
||||
else if (cpu().getRegisterSize(categoryIndex) == 64)
|
||||
painter.drawText(safeValueStartX, yStart, renderSize.width(), m_rowHeight, Qt::AlignLeft,
|
||||
FilledQStringFromValue(cpu().getRegister(categoryIndex, registerIndex).lo, 16));
|
||||
else
|
||||
painter.drawText(safeValueStartX, yStart, renderSize.width(), m_rowHeight, Qt::AlignLeft,
|
||||
FilledQStringFromValue(cpu().getRegister(categoryIndex, registerIndex)._u32[0], 16));
|
||||
}
|
||||
}
|
||||
painter.end();
|
||||
}
|
||||
|
||||
void RegisterView::mousePressEvent(QMouseEvent* event)
|
||||
{
|
||||
const int categoryIndex = ui.registerTabs->currentIndex();
|
||||
m_selectedRow = static_cast<int>(((event->position().y() - m_renderStart.y()) / m_rowHeight)) + m_rowStart;
|
||||
|
||||
// For 128 bit types, support selecting segments
|
||||
if (cpu().getRegisterSize(categoryIndex) == 128)
|
||||
{
|
||||
constexpr auto inRange = [](u32 low, u32 high, u32 val) {
|
||||
return (low <= val && val <= high);
|
||||
};
|
||||
|
||||
for (int i = 0; i < 4; i++)
|
||||
{
|
||||
if (inRange(m_fieldStartX[i], m_fieldStartX[i] + m_fieldWidth, event->position().x()))
|
||||
{
|
||||
m_selected128Field = i;
|
||||
}
|
||||
}
|
||||
}
|
||||
this->repaint();
|
||||
}
|
||||
|
||||
void RegisterView::wheelEvent(QWheelEvent* event)
|
||||
{
|
||||
if (event->angleDelta().y() < 0 && m_rowEnd < cpu().getRegisterCount(ui.registerTabs->currentIndex()))
|
||||
{
|
||||
m_rowStart += 1;
|
||||
}
|
||||
else if (event->angleDelta().y() > 0 && m_rowStart > 0)
|
||||
{
|
||||
m_rowStart -= 1;
|
||||
}
|
||||
|
||||
this->repaint();
|
||||
}
|
||||
|
||||
void RegisterView::mouseDoubleClickEvent(QMouseEvent* event)
|
||||
{
|
||||
if (!cpu().isAlive())
|
||||
return;
|
||||
if (m_selectedRow > m_rowEnd) // Unsigned underflow; selectedRow will be > m_rowEnd (technically negative)
|
||||
return;
|
||||
const int categoryIndex = ui.registerTabs->currentIndex();
|
||||
if (cpu().getRegisterSize(categoryIndex) == 128)
|
||||
contextChangeSegment();
|
||||
else
|
||||
contextChangeValue();
|
||||
}
|
||||
|
||||
void RegisterView::customMenuRequested(QPoint pos)
|
||||
{
|
||||
if (!cpu().isAlive())
|
||||
return;
|
||||
|
||||
if (m_selectedRow > m_rowEnd) // Unsigned underflow; selectedRow will be > m_rowEnd (technically negative)
|
||||
return;
|
||||
|
||||
QMenu* menu = new QMenu(this);
|
||||
menu->setAttribute(Qt::WA_DeleteOnClose);
|
||||
|
||||
const int categoryIndex = ui.registerTabs->currentIndex();
|
||||
|
||||
if (categoryIndex == EECAT_FPR)
|
||||
{
|
||||
QAction* action = menu->addAction(tr("Show as Float"));
|
||||
action->setCheckable(true);
|
||||
action->setChecked(m_showFPRFloat);
|
||||
connect(action, &QAction::triggered, this, [this]() {
|
||||
m_showFPRFloat = !m_showFPRFloat;
|
||||
repaint();
|
||||
});
|
||||
|
||||
menu->addSeparator();
|
||||
}
|
||||
|
||||
if (categoryIndex == EECAT_VU0F)
|
||||
{
|
||||
QAction* action = menu->addAction(tr("Show as Float"));
|
||||
action->setCheckable(true);
|
||||
action->setChecked(m_showVU0FFloat);
|
||||
connect(action, &QAction::triggered, this, [this]() {
|
||||
m_showVU0FFloat = !m_showVU0FFloat;
|
||||
repaint();
|
||||
});
|
||||
|
||||
menu->addSeparator();
|
||||
}
|
||||
|
||||
if (cpu().getRegisterSize(categoryIndex) == 128)
|
||||
{
|
||||
connect(menu->addAction(tr("Copy Top Half")), &QAction::triggered, this, &RegisterView::contextCopyTop);
|
||||
connect(menu->addAction(tr("Copy Bottom Half")), &QAction::triggered, this, &RegisterView::contextCopyBottom);
|
||||
connect(menu->addAction(tr("Copy Segment")), &QAction::triggered, this, &RegisterView::contextCopySegment);
|
||||
}
|
||||
else
|
||||
{
|
||||
connect(menu->addAction(tr("Copy Value")), &QAction::triggered, this, &RegisterView::contextCopyValue);
|
||||
}
|
||||
|
||||
menu->addSeparator();
|
||||
|
||||
if (cpu().getRegisterSize(categoryIndex) == 128)
|
||||
{
|
||||
connect(menu->addAction(tr("Change Top Half")), &QAction::triggered,
|
||||
this, &RegisterView::contextChangeTop);
|
||||
connect(menu->addAction(tr("Change Bottom Half")), &QAction::triggered,
|
||||
this, &RegisterView::contextChangeBottom);
|
||||
connect(menu->addAction(tr("Change Segment")), &QAction::triggered,
|
||||
this, &RegisterView::contextChangeSegment);
|
||||
}
|
||||
else
|
||||
{
|
||||
connect(menu->addAction(tr("Change Value")), &QAction::triggered,
|
||||
this, &RegisterView::contextChangeValue);
|
||||
}
|
||||
|
||||
menu->addSeparator();
|
||||
|
||||
createEventActions<DebuggerEvents::GoToAddress>(menu, [this]() {
|
||||
return contextCreateGotoEvent();
|
||||
});
|
||||
|
||||
menu->popup(this->mapToGlobal(pos));
|
||||
}
|
||||
|
||||
|
||||
void RegisterView::contextCopyValue()
|
||||
{
|
||||
const int categoryIndex = ui.registerTabs->currentIndex();
|
||||
const u128 val = cpu().getRegister(categoryIndex, m_selectedRow);
|
||||
if (CAT_SHOW_FLOAT)
|
||||
QApplication::clipboard()->setText(QString("%1").arg(QString::number(std::bit_cast<float>(val._u32[0])).toUpper(), 16));
|
||||
else
|
||||
QApplication::clipboard()->setText(QString("%1").arg(QString::number(val._u64[0], 16).toUpper(), 16));
|
||||
}
|
||||
|
||||
void RegisterView::contextCopyTop()
|
||||
{
|
||||
const int categoryIndex = ui.registerTabs->currentIndex();
|
||||
const u128 val = cpu().getRegister(categoryIndex, m_selectedRow);
|
||||
QApplication::clipboard()->setText(FilledQStringFromValue(val.hi, 16));
|
||||
}
|
||||
|
||||
void RegisterView::contextCopyBottom()
|
||||
{
|
||||
const int categoryIndex = ui.registerTabs->currentIndex();
|
||||
const u128 val = cpu().getRegister(categoryIndex, m_selectedRow);
|
||||
QApplication::clipboard()->setText(FilledQStringFromValue(val.lo, 16));
|
||||
}
|
||||
|
||||
void RegisterView::contextCopySegment()
|
||||
{
|
||||
const int categoryIndex = ui.registerTabs->currentIndex();
|
||||
const u128 val = cpu().getRegister(categoryIndex, m_selectedRow);
|
||||
if (CAT_SHOW_FLOAT)
|
||||
QApplication::clipboard()->setText(FilledQStringFromValue(std::bit_cast<float>(val._u32[3 - m_selected128Field]), 10));
|
||||
else
|
||||
QApplication::clipboard()->setText(FilledQStringFromValue(val._u32[3 - m_selected128Field], 16));
|
||||
}
|
||||
|
||||
bool RegisterView::contextFetchNewValue(u64& out, u64 currentValue, bool segment)
|
||||
{
|
||||
const int categoryIndex = ui.registerTabs->currentIndex();
|
||||
const bool floatingPoint = CAT_SHOW_FLOAT && segment;
|
||||
const int regSize = cpu().getRegisterSize(categoryIndex);
|
||||
bool ok = false;
|
||||
|
||||
QString existingValue("%1");
|
||||
|
||||
if (!floatingPoint)
|
||||
existingValue = existingValue.arg(currentValue, regSize == 64 ? 16 : 8, 16, QChar('0'));
|
||||
else
|
||||
existingValue = existingValue.arg(std::bit_cast<float>((u32)currentValue));
|
||||
|
||||
//: Changing the value in a CPU register (e.g. "Change t0")
|
||||
QString input = QInputDialog::getText(this, tr("Change %1").arg(cpu().getRegisterName(categoryIndex, m_selectedRow)), "",
|
||||
QLineEdit::Normal, existingValue, &ok);
|
||||
|
||||
if (!ok)
|
||||
return false;
|
||||
|
||||
if (!floatingPoint) // Get input as hexadecimal
|
||||
{
|
||||
out = input.toULongLong(&ok, 16);
|
||||
if (!ok)
|
||||
{
|
||||
QMessageBox::warning(this, tr("Invalid register value"), tr("Invalid hexadecimal register value."));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
out = std::bit_cast<u32>(input.toFloat(&ok));
|
||||
if (!ok)
|
||||
{
|
||||
QMessageBox::warning(this, tr("Invalid register value"), tr("Invalid floating-point register value."));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void RegisterView::contextChangeValue()
|
||||
{
|
||||
const int categoryIndex = ui.registerTabs->currentIndex();
|
||||
u64 newVal;
|
||||
if (contextFetchNewValue(newVal, cpu().getRegister(categoryIndex, m_selectedRow).lo))
|
||||
{
|
||||
cpu().setRegister(categoryIndex, m_selectedRow, u128::From64(newVal));
|
||||
DebuggerView::broadcastEvent(DebuggerEvents::VMUpdate());
|
||||
}
|
||||
}
|
||||
|
||||
void RegisterView::contextChangeTop()
|
||||
{
|
||||
u64 newVal;
|
||||
u128 oldVal = cpu().getRegister(ui.registerTabs->currentIndex(), m_selectedRow);
|
||||
if (contextFetchNewValue(newVal, oldVal.hi))
|
||||
{
|
||||
oldVal.hi = newVal;
|
||||
cpu().setRegister(ui.registerTabs->currentIndex(), m_selectedRow, oldVal);
|
||||
DebuggerView::broadcastEvent(DebuggerEvents::VMUpdate());
|
||||
}
|
||||
}
|
||||
|
||||
void RegisterView::contextChangeBottom()
|
||||
{
|
||||
u64 newVal;
|
||||
u128 oldVal = cpu().getRegister(ui.registerTabs->currentIndex(), m_selectedRow);
|
||||
if (contextFetchNewValue(newVal, oldVal.lo))
|
||||
{
|
||||
oldVal.lo = newVal;
|
||||
cpu().setRegister(ui.registerTabs->currentIndex(), m_selectedRow, oldVal);
|
||||
DebuggerView::broadcastEvent(DebuggerEvents::VMUpdate());
|
||||
}
|
||||
}
|
||||
|
||||
void RegisterView::contextChangeSegment()
|
||||
{
|
||||
u64 newVal;
|
||||
u128 oldVal = cpu().getRegister(ui.registerTabs->currentIndex(), m_selectedRow);
|
||||
if (contextFetchNewValue(newVal, oldVal._u32[3 - m_selected128Field], true))
|
||||
{
|
||||
oldVal._u32[3 - m_selected128Field] = (u32)newVal;
|
||||
cpu().setRegister(ui.registerTabs->currentIndex(), m_selectedRow, oldVal);
|
||||
DebuggerView::broadcastEvent(DebuggerEvents::VMUpdate());
|
||||
}
|
||||
}
|
||||
|
||||
std::optional<DebuggerEvents::GoToAddress> RegisterView::contextCreateGotoEvent()
|
||||
{
|
||||
const int categoryIndex = ui.registerTabs->currentIndex();
|
||||
u128 regVal = cpu().getRegister(categoryIndex, m_selectedRow);
|
||||
u32 addr = 0;
|
||||
|
||||
if (cpu().getRegisterSize(categoryIndex) == 128)
|
||||
addr = regVal._u32[3 - m_selected128Field];
|
||||
else
|
||||
addr = regVal._u32[0];
|
||||
|
||||
if (!cpu().isValidAddress(addr))
|
||||
{
|
||||
QMessageBox::warning(
|
||||
this,
|
||||
tr("Invalid target address"),
|
||||
tr("This register holds an invalid address."));
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
DebuggerEvents::GoToAddress event;
|
||||
event.address = addr;
|
||||
return event;
|
||||
}
|
||||
71
pcsx2-qt/Debugger/RegisterView.h
Normal file
71
pcsx2-qt/Debugger/RegisterView.h
Normal file
@@ -0,0 +1,71 @@
|
||||
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
|
||||
// SPDX-License-Identifier: GPL-3.0+
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "ui_RegisterView.h"
|
||||
|
||||
#include "DebuggerView.h"
|
||||
|
||||
#include "DebugTools/DebugInterface.h"
|
||||
#include "DebugTools/DisassemblyManager.h"
|
||||
|
||||
#include <QtWidgets/QMenu>
|
||||
#include <QtWidgets/QTabBar>
|
||||
#include <QtGui/QPainter>
|
||||
|
||||
class RegisterView final : public DebuggerView
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
RegisterView(const DebuggerViewParameters& parameters);
|
||||
~RegisterView();
|
||||
|
||||
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;
|
||||
|
||||
public slots:
|
||||
void customMenuRequested(QPoint pos);
|
||||
void contextCopyValue();
|
||||
void contextCopyTop();
|
||||
void contextCopyBottom();
|
||||
void contextCopySegment();
|
||||
void contextChangeValue();
|
||||
void contextChangeTop();
|
||||
void contextChangeBottom();
|
||||
void contextChangeSegment();
|
||||
|
||||
std::optional<DebuggerEvents::GoToAddress> contextCreateGotoEvent();
|
||||
|
||||
void tabCurrentChanged(int cur);
|
||||
|
||||
private:
|
||||
Ui::RegisterView ui;
|
||||
|
||||
// Returns true on success
|
||||
bool contextFetchNewValue(u64& out, u64 currentValue, bool segment = false);
|
||||
|
||||
// Used for the height offset the tab bar creates
|
||||
// because we share a widget
|
||||
QPoint m_renderStart;
|
||||
|
||||
s32 m_rowStart = 0; // Index, 0 -> VF00, 1 -> VF01 etc
|
||||
s32 m_rowEnd; // Index, what register is the last one drawn
|
||||
s32 m_rowHeight; // The height of each register row
|
||||
// Used for mouse clicks
|
||||
s32 m_fieldStartX[4]; // Where the register segments start
|
||||
s32 m_fieldWidth; // How wide the register segments are
|
||||
|
||||
s32 m_selectedRow = 0; // Index
|
||||
s32 m_selected128Field = 0; // Values are from 0 to 3
|
||||
|
||||
bool m_showVU0FFloat = false;
|
||||
bool m_showFPRFloat = false;
|
||||
};
|
||||
78
pcsx2-qt/Debugger/RegisterView.ui
Normal file
78
pcsx2-qt/Debugger/RegisterView.ui
Normal file
@@ -0,0 +1,78 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>RegisterView</class>
|
||||
<widget class="QWidget" name="RegisterView">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>400</width>
|
||||
<height>316</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>325</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Register View</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="QTabBar" name="registerTabs" native="true">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Minimum">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="verticalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>289</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<customwidgets>
|
||||
<customwidget>
|
||||
<class>QTabBar</class>
|
||||
<extends>QWidget</extends>
|
||||
<header>qtabbar.h</header>
|
||||
</customwidget>
|
||||
</customwidgets>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
||||
120
pcsx2-qt/Debugger/StackModel.cpp
Normal file
120
pcsx2-qt/Debugger/StackModel.cpp
Normal file
@@ -0,0 +1,120 @@
|
||||
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
|
||||
// SPDX-License-Identifier: GPL-3.0+
|
||||
|
||||
#include "StackModel.h"
|
||||
#include "DebugTools/MipsStackWalk.h"
|
||||
#include "DebugTools/BiosDebugData.h"
|
||||
#include "QtUtils.h"
|
||||
|
||||
StackModel::StackModel(DebugInterface& cpu, QObject* parent)
|
||||
: QAbstractTableModel(parent)
|
||||
, m_cpu(cpu)
|
||||
{
|
||||
}
|
||||
|
||||
int StackModel::rowCount(const QModelIndex&) const
|
||||
{
|
||||
return m_stackFrames.size();
|
||||
}
|
||||
|
||||
int StackModel::columnCount(const QModelIndex&) const
|
||||
{
|
||||
return StackModel::COLUMN_COUNT;
|
||||
}
|
||||
|
||||
QVariant StackModel::data(const QModelIndex& index, int role) const
|
||||
{
|
||||
size_t row = static_cast<size_t>(index.row());
|
||||
if (row >= m_stackFrames.size())
|
||||
return QVariant();
|
||||
|
||||
const auto& stackFrame = m_stackFrames[row];
|
||||
|
||||
if (role == Qt::DisplayRole)
|
||||
{
|
||||
switch (index.column())
|
||||
{
|
||||
case StackModel::ENTRY:
|
||||
return QtUtils::FilledQStringFromValue(stackFrame.entry, 16);
|
||||
case StackModel::ENTRY_LABEL:
|
||||
return QString::fromStdString(m_cpu.GetSymbolGuardian().FunctionStartingAtAddress(stackFrame.entry).name);
|
||||
case StackModel::PC:
|
||||
return QtUtils::FilledQStringFromValue(stackFrame.pc, 16);
|
||||
case StackModel::PC_OPCODE:
|
||||
return m_cpu.disasm(stackFrame.pc, true).c_str();
|
||||
case StackModel::SP:
|
||||
return QtUtils::FilledQStringFromValue(stackFrame.sp, 16);
|
||||
case StackModel::SIZE:
|
||||
return QString::number(stackFrame.stackSize);
|
||||
}
|
||||
}
|
||||
else if (role == Qt::UserRole)
|
||||
{
|
||||
const auto& stackFrame = m_stackFrames.at(index.row());
|
||||
switch (index.column())
|
||||
{
|
||||
case StackModel::ENTRY:
|
||||
return stackFrame.entry;
|
||||
case StackModel::ENTRY_LABEL:
|
||||
return QString::fromStdString(m_cpu.GetSymbolGuardian().FunctionStartingAtAddress(stackFrame.entry).name);
|
||||
case StackModel::PC:
|
||||
return stackFrame.pc;
|
||||
case StackModel::PC_OPCODE:
|
||||
return m_cpu.disasm(stackFrame.pc, true).c_str();
|
||||
case StackModel::SP:
|
||||
return stackFrame.sp;
|
||||
case StackModel::SIZE:
|
||||
return stackFrame.stackSize;
|
||||
}
|
||||
}
|
||||
|
||||
return QVariant();
|
||||
}
|
||||
|
||||
QVariant StackModel::headerData(int section, Qt::Orientation orientation, int role) const
|
||||
{
|
||||
if (role == Qt::DisplayRole && orientation == Qt::Horizontal)
|
||||
{
|
||||
switch (section)
|
||||
{
|
||||
case StackColumns::ENTRY:
|
||||
//: Warning: short space limit. Abbreviate if needed.
|
||||
return tr("ENTRY");
|
||||
case StackColumns::ENTRY_LABEL:
|
||||
//: Warning: short space limit. Abbreviate if needed.
|
||||
return tr("LABEL");
|
||||
case StackColumns::PC:
|
||||
//: Warning: short space limit. Abbreviate if needed. PC = Program Counter (location where the CPU is executing).
|
||||
return tr("PC");
|
||||
case StackColumns::PC_OPCODE:
|
||||
//: Warning: short space limit. Abbreviate if needed.
|
||||
return tr("INSTRUCTION");
|
||||
case StackColumns::SP:
|
||||
//: Warning: short space limit. Abbreviate if needed.
|
||||
return tr("STACK POINTER");
|
||||
case StackColumns::SIZE:
|
||||
//: Warning: short space limit. Abbreviate if needed.
|
||||
return tr("SIZE");
|
||||
default:
|
||||
return QVariant();
|
||||
}
|
||||
}
|
||||
return QVariant();
|
||||
}
|
||||
|
||||
void StackModel::refreshData()
|
||||
{
|
||||
// Hopefully in the near future we can get a stack frame for
|
||||
// each thread
|
||||
beginResetModel();
|
||||
for (const auto& thread : m_cpu.GetThreadList())
|
||||
{
|
||||
if (thread->Status() == ThreadStatus::THS_RUN)
|
||||
{
|
||||
m_stackFrames = MipsStackWalk::Walk(&m_cpu, m_cpu.getPC(), m_cpu.getRegister(0, 31), m_cpu.getRegister(0, 29),
|
||||
thread->EntryPoint(), thread->StackTop());
|
||||
break;
|
||||
}
|
||||
}
|
||||
endResetModel();
|
||||
}
|
||||
50
pcsx2-qt/Debugger/StackModel.h
Normal file
50
pcsx2-qt/Debugger/StackModel.h
Normal file
@@ -0,0 +1,50 @@
|
||||
// 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"
|
||||
#include "DebugTools/MipsStackWalk.h"
|
||||
|
||||
class StackModel : public QAbstractTableModel
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
enum StackColumns : int
|
||||
{
|
||||
ENTRY = 0,
|
||||
ENTRY_LABEL,
|
||||
PC,
|
||||
PC_OPCODE,
|
||||
SP,
|
||||
SIZE,
|
||||
COLUMN_COUNT
|
||||
};
|
||||
|
||||
static constexpr QHeaderView::ResizeMode HeaderResizeModes[StackColumns::COLUMN_COUNT] =
|
||||
{
|
||||
QHeaderView::ResizeMode::ResizeToContents,
|
||||
QHeaderView::ResizeMode::Stretch,
|
||||
QHeaderView::ResizeMode::ResizeToContents,
|
||||
QHeaderView::ResizeMode::Stretch,
|
||||
QHeaderView::ResizeMode::ResizeToContents,
|
||||
QHeaderView::ResizeMode::ResizeToContents,
|
||||
};
|
||||
|
||||
explicit StackModel(DebugInterface& cpu, QObject* parent = nullptr);
|
||||
|
||||
int rowCount(const QModelIndex& parent = QModelIndex()) const override;
|
||||
int columnCount(const QModelIndex& parent = QModelIndex()) const override;
|
||||
QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override;
|
||||
QVariant headerData(int section, Qt::Orientation orientation, int role) const override;
|
||||
|
||||
void refreshData();
|
||||
|
||||
private:
|
||||
DebugInterface& m_cpu;
|
||||
std::vector<MipsStackWalk::StackFrame> m_stackFrames;
|
||||
};
|
||||
84
pcsx2-qt/Debugger/StackView.cpp
Normal file
84
pcsx2-qt/Debugger/StackView.cpp
Normal file
@@ -0,0 +1,84 @@
|
||||
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
|
||||
// SPDX-License-Identifier: GPL-3.0+
|
||||
|
||||
#include "StackView.h"
|
||||
|
||||
#include "QtUtils.h"
|
||||
|
||||
#include <QtGui/QClipboard>
|
||||
#include <QtWidgets/QMenu>
|
||||
|
||||
StackView::StackView(const DebuggerViewParameters& parameters)
|
||||
: DebuggerView(parameters, NO_DEBUGGER_FLAGS)
|
||||
, m_model(new StackModel(cpu()))
|
||||
{
|
||||
m_ui.setupUi(this);
|
||||
|
||||
m_ui.stackList->setContextMenuPolicy(Qt::CustomContextMenu);
|
||||
connect(m_ui.stackList, &QTableView::customContextMenuRequested, this, &StackView::openContextMenu);
|
||||
connect(m_ui.stackList, &QTableView::doubleClicked, this, &StackView::onDoubleClick);
|
||||
|
||||
m_ui.stackList->setModel(m_model);
|
||||
for (std::size_t i = 0; auto mode : StackModel::HeaderResizeModes)
|
||||
{
|
||||
m_ui.stackList->horizontalHeader()->setSectionResizeMode(i, mode);
|
||||
i++;
|
||||
}
|
||||
|
||||
receiveEvent<DebuggerEvents::VMUpdate>([this](const DebuggerEvents::VMUpdate& event) -> bool {
|
||||
m_model->refreshData();
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
void StackView::openContextMenu(QPoint pos)
|
||||
{
|
||||
if (!m_ui.stackList->selectionModel()->hasSelection())
|
||||
return;
|
||||
|
||||
QMenu* menu = new QMenu(m_ui.stackList);
|
||||
menu->setAttribute(Qt::WA_DeleteOnClose);
|
||||
|
||||
QAction* copy_action = menu->addAction(tr("Copy"));
|
||||
connect(copy_action, &QAction::triggered, [this]() {
|
||||
const auto* selection_model = m_ui.stackList->selectionModel();
|
||||
if (!selection_model->hasSelection())
|
||||
return;
|
||||
|
||||
QGuiApplication::clipboard()->setText(m_model->data(selection_model->currentIndex()).toString());
|
||||
});
|
||||
|
||||
menu->addSeparator();
|
||||
|
||||
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.stackList->model()));
|
||||
});
|
||||
|
||||
menu->popup(m_ui.stackList->viewport()->mapToGlobal(pos));
|
||||
}
|
||||
|
||||
void StackView::onDoubleClick(const QModelIndex& index)
|
||||
{
|
||||
switch (index.column())
|
||||
{
|
||||
case StackModel::StackModel::ENTRY:
|
||||
case StackModel::StackModel::ENTRY_LABEL:
|
||||
{
|
||||
QModelIndex entry_index = m_model->index(index.row(), StackModel::StackColumns::ENTRY);
|
||||
goToInDisassembler(m_model->data(entry_index, Qt::UserRole).toUInt(), true);
|
||||
break;
|
||||
}
|
||||
case StackModel::StackModel::SP:
|
||||
{
|
||||
goToInMemoryView(m_model->data(index, Qt::UserRole).toUInt(), true);
|
||||
break;
|
||||
}
|
||||
default: // Default to PC
|
||||
{
|
||||
QModelIndex pc_index = m_model->index(index.row(), StackModel::StackColumns::PC);
|
||||
goToInDisassembler(m_model->data(pc_index, Qt::UserRole).toUInt(), true);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
26
pcsx2-qt/Debugger/StackView.h
Normal file
26
pcsx2-qt/Debugger/StackView.h
Normal file
@@ -0,0 +1,26 @@
|
||||
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
|
||||
// SPDX-License-Identifier: GPL-3.0+
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "ui_StackView.h"
|
||||
|
||||
#include "StackModel.h"
|
||||
|
||||
#include "DebuggerView.h"
|
||||
|
||||
class StackView final : public DebuggerView
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
StackView(const DebuggerViewParameters& parameters);
|
||||
|
||||
void openContextMenu(QPoint pos);
|
||||
void onDoubleClick(const QModelIndex& index);
|
||||
|
||||
private:
|
||||
Ui::StackView m_ui;
|
||||
|
||||
StackModel* m_model;
|
||||
};
|
||||
39
pcsx2-qt/Debugger/StackView.ui
Normal file
39
pcsx2-qt/Debugger/StackView.ui
Normal file
@@ -0,0 +1,39 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>StackView</class>
|
||||
<widget class="QWidget" name="StackView">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>400</width>
|
||||
<height>300</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Stack</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="stackList"/>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
||||
314
pcsx2-qt/Debugger/SymbolTree/NewSymbolDialog.ui
Normal file
314
pcsx2-qt/Debugger/SymbolTree/NewSymbolDialog.ui
Normal file
@@ -0,0 +1,314 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>NewSymbolDialog</class>
|
||||
<widget class="QDialog" name="NewSymbolDialog">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>600</width>
|
||||
<height>400</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Minimum" vsizetype="Minimum">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>300</width>
|
||||
<height>200</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>600</width>
|
||||
<height>400</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Dialog</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<widget class="QTabBar" name="storageTabBar" native="true"/>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QFormLayout" name="form">
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="nameLabel">
|
||||
<property name="text">
|
||||
<string>Name</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1">
|
||||
<widget class="QLineEdit" name="nameLineEdit"/>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QLabel" name="addressLabel">
|
||||
<property name="text">
|
||||
<string>Address</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<widget class="QLineEdit" name="addressLineEdit"/>
|
||||
</item>
|
||||
<item row="2" column="0">
|
||||
<widget class="QLabel" name="registerLabel">
|
||||
<property name="text">
|
||||
<string>Register</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="1">
|
||||
<widget class="QComboBox" name="registerComboBox"/>
|
||||
</item>
|
||||
<item row="3" column="0">
|
||||
<widget class="QLabel" name="stackPointerOffsetLabel">
|
||||
<property name="text">
|
||||
<string>Stack Pointer Offset</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="1">
|
||||
<widget class="QSpinBox" name="stackPointerOffsetSpinBox">
|
||||
<property name="maximum">
|
||||
<number>268435456</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="0">
|
||||
<widget class="QLabel" name="sizeLabel">
|
||||
<property name="text">
|
||||
<string>Size</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="1">
|
||||
<layout class="QVBoxLayout" name="sizeLayout">
|
||||
<item>
|
||||
<widget class="QRadioButton" name="fillExistingFunctionRadioButton">
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
<property name="checked">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<attribute name="buttonGroup">
|
||||
<string notr="true">sizeButtonGroup</string>
|
||||
</attribute>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QRadioButton" name="fillEmptySpaceRadioButton">
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
<property name="checked">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<attribute name="buttonGroup">
|
||||
<string notr="true">sizeButtonGroup</string>
|
||||
</attribute>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="customSizeLayout">
|
||||
<item>
|
||||
<widget class="QRadioButton" name="customSizeRadioButton">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Maximum" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Custom</string>
|
||||
</property>
|
||||
<attribute name="buttonGroup">
|
||||
<string notr="true">sizeButtonGroup</string>
|
||||
</attribute>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QSpinBox" name="customSizeSpinBox">
|
||||
<property name="enabled">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="readOnly">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<number>268435456</number>
|
||||
</property>
|
||||
<property name="singleStep">
|
||||
<number>4</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item row="5" column="0">
|
||||
<widget class="QLabel" name="existingFunctionsLabel">
|
||||
<property name="text">
|
||||
<string>Existing Functions</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="5" column="1">
|
||||
<layout class="QVBoxLayout" name="existingFunctionsLayout">
|
||||
<item>
|
||||
<widget class="QRadioButton" name="shrinkExistingRadioButton">
|
||||
<property name="text">
|
||||
<string>Shrink to avoid overlaps</string>
|
||||
</property>
|
||||
<property name="checked">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<attribute name="buttonGroup">
|
||||
<string notr="true">existingFunctionsButtonGroup</string>
|
||||
</attribute>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QRadioButton" name="doNotModifyExistingRadioButton">
|
||||
<property name="text">
|
||||
<string>Do not modify</string>
|
||||
</property>
|
||||
<attribute name="buttonGroup">
|
||||
<string notr="true">existingFunctionsButtonGroup</string>
|
||||
</attribute>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item row="6" column="0">
|
||||
<widget class="QLabel" name="typeLabel">
|
||||
<property name="text">
|
||||
<string>Type</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="6" column="1">
|
||||
<widget class="QLineEdit" name="typeLineEdit"/>
|
||||
</item>
|
||||
<item row="7" column="0">
|
||||
<widget class="QLabel" name="functionLabel">
|
||||
<property name="text">
|
||||
<string>Function</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="7" column="1">
|
||||
<widget class="QComboBox" name="functionComboBox">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>16777215</width>
|
||||
<height>16777215</height>
|
||||
</size>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<item>
|
||||
<widget class="QLabel" name="errorMessage">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="styleSheet">
|
||||
<string notr="true">color: red</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QDialogButtonBox" name="buttonBox">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Minimum" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="standardButtons">
|
||||
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<customwidgets>
|
||||
<customwidget>
|
||||
<class>QTabBar</class>
|
||||
<extends>QWidget</extends>
|
||||
<header location="global">QtWidgets/QTabBar</header>
|
||||
<container>1</container>
|
||||
</customwidget>
|
||||
</customwidgets>
|
||||
<resources/>
|
||||
<connections>
|
||||
<connection>
|
||||
<sender>buttonBox</sender>
|
||||
<signal>accepted()</signal>
|
||||
<receiver>NewSymbolDialog</receiver>
|
||||
<slot>accept()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>248</x>
|
||||
<y>254</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>157</x>
|
||||
<y>274</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
<connection>
|
||||
<sender>buttonBox</sender>
|
||||
<signal>rejected()</signal>
|
||||
<receiver>NewSymbolDialog</receiver>
|
||||
<slot>reject()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>316</x>
|
||||
<y>260</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>286</x>
|
||||
<y>274</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
</connections>
|
||||
<buttongroups>
|
||||
<buttongroup name="sizeButtonGroup"/>
|
||||
<buttongroup name="existingFunctionsButtonGroup"/>
|
||||
</buttongroups>
|
||||
</ui>
|
||||
653
pcsx2-qt/Debugger/SymbolTree/NewSymbolDialogs.cpp
Normal file
653
pcsx2-qt/Debugger/SymbolTree/NewSymbolDialogs.cpp
Normal file
@@ -0,0 +1,653 @@
|
||||
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
|
||||
// SPDX-License-Identifier: GPL-3.0+
|
||||
|
||||
#include "NewSymbolDialogs.h"
|
||||
#include "QtCompatibility.h"
|
||||
|
||||
#include <QtCore/QTimer>
|
||||
#include <QtCore/QMetaMethod>
|
||||
#include <QtWidgets/QMessageBox>
|
||||
#include <QtWidgets/QPushButton>
|
||||
|
||||
#include "TypeString.h"
|
||||
|
||||
NewSymbolDialog::NewSymbolDialog(u32 flags, u32 alignment, DebugInterface& cpu, QWidget* parent)
|
||||
: QDialog(parent)
|
||||
, m_cpu(cpu)
|
||||
, m_alignment(alignment)
|
||||
{
|
||||
m_ui.setupUi(this);
|
||||
|
||||
connect(m_ui.buttonBox, &QDialogButtonBox::accepted, this, &NewSymbolDialog::createSymbol);
|
||||
connect(m_ui.storageTabBar, &QTabBar::currentChanged, this, &NewSymbolDialog::onStorageTabChanged);
|
||||
|
||||
if (flags & GLOBAL_STORAGE)
|
||||
{
|
||||
int tab = m_ui.storageTabBar->addTab(tr("Global"));
|
||||
m_ui.storageTabBar->setTabData(tab, GLOBAL_STORAGE);
|
||||
}
|
||||
|
||||
if (flags & REGISTER_STORAGE)
|
||||
{
|
||||
int tab = m_ui.storageTabBar->addTab(tr("Register"));
|
||||
m_ui.storageTabBar->setTabData(tab, REGISTER_STORAGE);
|
||||
|
||||
setupRegisterField();
|
||||
}
|
||||
|
||||
if (flags & STACK_STORAGE)
|
||||
{
|
||||
int tab = m_ui.storageTabBar->addTab(tr("Stack"));
|
||||
m_ui.storageTabBar->setTabData(tab, STACK_STORAGE);
|
||||
}
|
||||
|
||||
if (m_ui.storageTabBar->count() == 1)
|
||||
m_ui.storageTabBar->hide();
|
||||
|
||||
setFormRowVisible(m_ui.form, Row::SIZE, flags & SIZE_FIELD);
|
||||
setFormRowVisible(m_ui.form, Row::EXISTING_FUNCTIONS, flags & EXISTING_FUNCTIONS_FIELD);
|
||||
setFormRowVisible(m_ui.form, Row::TYPE, flags & TYPE_FIELD);
|
||||
setFormRowVisible(m_ui.form, Row::FUNCTION, flags & FUNCTION_FIELD);
|
||||
|
||||
if (flags & SIZE_FIELD)
|
||||
{
|
||||
setupSizeField();
|
||||
updateSizeField();
|
||||
}
|
||||
|
||||
if (flags & FUNCTION_FIELD)
|
||||
setupFunctionField();
|
||||
|
||||
connectInputWidgets();
|
||||
onStorageTabChanged(0);
|
||||
adjustSize();
|
||||
}
|
||||
|
||||
void NewSymbolDialog::setName(QString name)
|
||||
{
|
||||
m_ui.nameLineEdit->setText(name);
|
||||
}
|
||||
|
||||
void NewSymbolDialog::setAddress(u32 address)
|
||||
{
|
||||
m_ui.addressLineEdit->setText(QString::number(address, 16));
|
||||
}
|
||||
|
||||
void NewSymbolDialog::setCustomSize(u32 size)
|
||||
{
|
||||
m_ui.customSizeRadioButton->setChecked(true);
|
||||
m_ui.customSizeSpinBox->setValue(size);
|
||||
}
|
||||
|
||||
void NewSymbolDialog::setupRegisterField()
|
||||
{
|
||||
m_ui.registerComboBox->clear();
|
||||
for (int i = 0; i < m_cpu.getRegisterCount(0); i++)
|
||||
m_ui.registerComboBox->addItem(m_cpu.getRegisterName(0, i));
|
||||
}
|
||||
|
||||
void NewSymbolDialog::setupSizeField()
|
||||
{
|
||||
connect(m_ui.customSizeRadioButton, &QRadioButton::toggled, m_ui.customSizeSpinBox, &QSpinBox::setEnabled);
|
||||
connect(m_ui.addressLineEdit, &QLineEdit::textChanged, this, &NewSymbolDialog::updateSizeField);
|
||||
}
|
||||
|
||||
void NewSymbolDialog::setupFunctionField()
|
||||
{
|
||||
m_cpu.GetSymbolGuardian().Read([&](const ccc::SymbolDatabase& database) {
|
||||
const ccc::Function* default_function = database.functions.symbol_overlapping_address(m_cpu.getPC());
|
||||
|
||||
for (const ccc::Function& function : database.functions)
|
||||
{
|
||||
QString name = QString::fromStdString(function.name());
|
||||
name.truncate(64);
|
||||
m_ui.functionComboBox->addItem(name);
|
||||
m_functions.emplace_back(function.handle());
|
||||
|
||||
if (default_function && function.handle() == default_function->handle())
|
||||
m_ui.functionComboBox->setCurrentIndex(m_ui.functionComboBox->count() - 1);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void NewSymbolDialog::connectInputWidgets()
|
||||
{
|
||||
QMetaMethod parse_user_input = metaObject()->method(metaObject()->indexOfSlot("parseUserInput()"));
|
||||
for (QObject* child : children())
|
||||
{
|
||||
QWidget* widget = qobject_cast<QWidget*>(child);
|
||||
if (!widget)
|
||||
continue;
|
||||
|
||||
QMetaProperty property = widget->metaObject()->userProperty();
|
||||
if (!property.isValid() || !property.hasNotifySignal())
|
||||
continue;
|
||||
|
||||
connect(widget, property.notifySignal(), this, parse_user_input);
|
||||
}
|
||||
}
|
||||
|
||||
void NewSymbolDialog::updateErrorMessage(QString error_message)
|
||||
{
|
||||
m_ui.buttonBox->button(QDialogButtonBox::Ok)->setEnabled(error_message.isEmpty());
|
||||
m_ui.errorMessage->setText(error_message);
|
||||
}
|
||||
|
||||
NewSymbolDialog::FunctionSizeType NewSymbolDialog::functionSizeType() const
|
||||
{
|
||||
if (m_ui.fillExistingFunctionRadioButton->isChecked())
|
||||
return FILL_EXISTING_FUNCTION;
|
||||
|
||||
if (m_ui.fillEmptySpaceRadioButton->isChecked())
|
||||
return FILL_EMPTY_SPACE;
|
||||
|
||||
return CUSTOM_SIZE;
|
||||
}
|
||||
|
||||
void NewSymbolDialog::updateSizeField()
|
||||
{
|
||||
bool ok;
|
||||
u32 address = m_ui.addressLineEdit->text().toUInt(&ok, 16);
|
||||
if (ok)
|
||||
{
|
||||
m_cpu.GetSymbolGuardian().Read([&](const ccc::SymbolDatabase& database) {
|
||||
std::optional<u32> fill_existing_function_size = fillExistingFunctionSize(address, database);
|
||||
if (fill_existing_function_size.has_value())
|
||||
m_ui.fillExistingFunctionRadioButton->setText(
|
||||
tr("Fill existing function (%1 bytes)").arg(*fill_existing_function_size));
|
||||
else
|
||||
m_ui.fillExistingFunctionRadioButton->setText(
|
||||
tr("Fill existing function (none found)"));
|
||||
m_ui.fillExistingFunctionRadioButton->setEnabled(fill_existing_function_size.has_value());
|
||||
|
||||
std::optional<u32> fill_empty_space_size = fillEmptySpaceSize(address, database);
|
||||
if (fill_empty_space_size.has_value())
|
||||
m_ui.fillEmptySpaceRadioButton->setText(
|
||||
tr("Fill space (%1 bytes)").arg(*fill_empty_space_size));
|
||||
else
|
||||
m_ui.fillEmptySpaceRadioButton->setText(tr("Fill space (no next symbol)"));
|
||||
m_ui.fillEmptySpaceRadioButton->setEnabled(fill_empty_space_size.has_value());
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
// Add some padding to the end of the radio button text so that the
|
||||
// layout engine knows we need some more space for the size.
|
||||
QString padding(16, ' ');
|
||||
m_ui.fillExistingFunctionRadioButton->setText(tr("Fill existing function").append(padding));
|
||||
m_ui.fillEmptySpaceRadioButton->setText(tr("Fill space").append(padding));
|
||||
}
|
||||
}
|
||||
|
||||
std::optional<u32> NewSymbolDialog::fillExistingFunctionSize(u32 address, const ccc::SymbolDatabase& database)
|
||||
{
|
||||
const ccc::Function* existing_function = database.functions.symbol_overlapping_address(address);
|
||||
if (!existing_function)
|
||||
return std::nullopt;
|
||||
|
||||
return existing_function->address_range().high.value - address;
|
||||
}
|
||||
|
||||
std::optional<u32> NewSymbolDialog::fillEmptySpaceSize(u32 address, const ccc::SymbolDatabase& database)
|
||||
{
|
||||
const ccc::Symbol* next_symbol = database.symbol_after_address(
|
||||
address, ccc::FUNCTION | ccc::GLOBAL_VARIABLE | ccc::LOCAL_VARIABLE);
|
||||
if (!next_symbol)
|
||||
return std::nullopt;
|
||||
|
||||
return next_symbol->address().value - address;
|
||||
}
|
||||
|
||||
u32 NewSymbolDialog::storageType() const
|
||||
{
|
||||
return m_ui.storageTabBar->tabData(m_ui.storageTabBar->currentIndex()).toUInt();
|
||||
}
|
||||
|
||||
void NewSymbolDialog::onStorageTabChanged(int index)
|
||||
{
|
||||
u32 storage = m_ui.storageTabBar->tabData(index).toUInt();
|
||||
|
||||
setFormRowVisible(m_ui.form, Row::ADDRESS, storage == GLOBAL_STORAGE);
|
||||
setFormRowVisible(m_ui.form, Row::REGISTER, storage == REGISTER_STORAGE);
|
||||
setFormRowVisible(m_ui.form, Row::STACK_POINTER_OFFSET, storage == STACK_STORAGE);
|
||||
|
||||
QTimer::singleShot(0, this, [&]() {
|
||||
parseUserInput();
|
||||
});
|
||||
}
|
||||
|
||||
std::string NewSymbolDialog::parseName(QString& error_message)
|
||||
{
|
||||
std::string name = m_ui.nameLineEdit->text().toStdString();
|
||||
if (name.empty())
|
||||
error_message = tr("Name is empty.");
|
||||
|
||||
return name;
|
||||
}
|
||||
|
||||
u32 NewSymbolDialog::parseAddress(QString& error_message)
|
||||
{
|
||||
bool ok;
|
||||
u32 address = m_ui.addressLineEdit->text().toUInt(&ok, 16);
|
||||
if (!ok)
|
||||
error_message = tr("Address is not valid.");
|
||||
|
||||
if (address % m_alignment != 0)
|
||||
error_message = tr("Address is not aligned.");
|
||||
|
||||
return address;
|
||||
}
|
||||
|
||||
// *****************************************************************************
|
||||
|
||||
NewFunctionDialog::NewFunctionDialog(DebugInterface& cpu, QWidget* parent)
|
||||
: NewSymbolDialog(GLOBAL_STORAGE | SIZE_FIELD | EXISTING_FUNCTIONS_FIELD, 4, cpu, parent)
|
||||
{
|
||||
setWindowTitle("New Function");
|
||||
|
||||
m_ui.customSizeSpinBox->setValue(8);
|
||||
}
|
||||
|
||||
bool NewFunctionDialog::parseUserInput()
|
||||
{
|
||||
QString error_message;
|
||||
m_cpu.GetSymbolGuardian().Read([&](const ccc::SymbolDatabase& database) {
|
||||
m_name = parseName(error_message);
|
||||
if (!error_message.isEmpty())
|
||||
return;
|
||||
|
||||
m_address = parseAddress(error_message);
|
||||
if (!error_message.isEmpty())
|
||||
return;
|
||||
|
||||
m_size = 0;
|
||||
switch (functionSizeType())
|
||||
{
|
||||
case FILL_EXISTING_FUNCTION:
|
||||
{
|
||||
std::optional<u32> fill_existing_function_size = fillExistingFunctionSize(m_address, database);
|
||||
if (!fill_existing_function_size.has_value())
|
||||
{
|
||||
error_message = tr("No existing function found.");
|
||||
return;
|
||||
}
|
||||
|
||||
m_size = *fill_existing_function_size;
|
||||
|
||||
break;
|
||||
}
|
||||
case FILL_EMPTY_SPACE:
|
||||
{
|
||||
std::optional<u32> fill_space_size = fillEmptySpaceSize(m_address, database);
|
||||
if (!fill_space_size.has_value())
|
||||
{
|
||||
error_message = tr("No next symbol found.");
|
||||
return;
|
||||
}
|
||||
|
||||
m_size = *fill_space_size;
|
||||
|
||||
break;
|
||||
}
|
||||
case CUSTOM_SIZE:
|
||||
{
|
||||
m_size = m_ui.customSizeSpinBox->value();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (m_size == 0 || m_size > 256 * 1024 * 1024)
|
||||
{
|
||||
error_message = tr("Size is invalid.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_size % 4 != 0)
|
||||
{
|
||||
error_message = tr("Size is not a multiple of 4.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle an existing function if it exists.
|
||||
const ccc::Function* existing_function = database.functions.symbol_overlapping_address(m_address);
|
||||
m_existing_function = ccc::FunctionHandle();
|
||||
if (existing_function)
|
||||
{
|
||||
if (existing_function->address().value == m_address)
|
||||
{
|
||||
error_message = tr("A function already exists at that address.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_ui.shrinkExistingRadioButton->isChecked())
|
||||
{
|
||||
m_new_existing_function_size = m_address - existing_function->address().value;
|
||||
m_existing_function = existing_function->handle();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
updateErrorMessage(error_message);
|
||||
return error_message.isEmpty();
|
||||
}
|
||||
|
||||
void NewFunctionDialog::createSymbol()
|
||||
{
|
||||
if (!parseUserInput())
|
||||
return;
|
||||
|
||||
QString error_message;
|
||||
m_cpu.GetSymbolGuardian().ReadWrite([&](ccc::SymbolDatabase& database) {
|
||||
ccc::Result<ccc::SymbolSourceHandle> source = database.get_symbol_source("User-Defined");
|
||||
if (!source.success())
|
||||
{
|
||||
error_message = tr("Cannot create symbol source.");
|
||||
return;
|
||||
}
|
||||
|
||||
ccc::Result<ccc::Function*> function = database.functions.create_symbol(std::move(m_name), m_address, *source, nullptr);
|
||||
if (!function.success())
|
||||
{
|
||||
error_message = tr("Cannot create symbol.");
|
||||
return;
|
||||
}
|
||||
|
||||
(*function)->set_size(m_size);
|
||||
|
||||
ccc::Function* existing_function = database.functions.symbol_from_handle(m_existing_function);
|
||||
if (existing_function)
|
||||
existing_function->set_size(m_new_existing_function_size);
|
||||
});
|
||||
|
||||
if (!error_message.isEmpty())
|
||||
QMessageBox::warning(this, tr("Cannot Create Function"), error_message);
|
||||
}
|
||||
|
||||
// *****************************************************************************
|
||||
|
||||
NewGlobalVariableDialog::NewGlobalVariableDialog(DebugInterface& cpu, QWidget* parent)
|
||||
: NewSymbolDialog(GLOBAL_STORAGE | TYPE_FIELD, 1, cpu, parent)
|
||||
{
|
||||
setWindowTitle("New Global Variable");
|
||||
}
|
||||
|
||||
bool NewGlobalVariableDialog::parseUserInput()
|
||||
{
|
||||
QString error_message;
|
||||
m_cpu.GetSymbolGuardian().Read([&](const ccc::SymbolDatabase& database) {
|
||||
m_name = parseName(error_message);
|
||||
if (!error_message.isEmpty())
|
||||
return;
|
||||
|
||||
m_address = parseAddress(error_message);
|
||||
if (!error_message.isEmpty())
|
||||
return;
|
||||
|
||||
m_type = stringToType(m_ui.typeLineEdit->text().toStdString(), database, error_message);
|
||||
if (!error_message.isEmpty())
|
||||
return;
|
||||
});
|
||||
|
||||
updateErrorMessage(error_message);
|
||||
return error_message.isEmpty();
|
||||
}
|
||||
|
||||
void NewGlobalVariableDialog::createSymbol()
|
||||
{
|
||||
if (!parseUserInput())
|
||||
return;
|
||||
|
||||
QString error_message;
|
||||
m_cpu.GetSymbolGuardian().ReadWrite([&](ccc::SymbolDatabase& database) {
|
||||
ccc::Result<ccc::SymbolSourceHandle> source = database.get_symbol_source("User-Defined");
|
||||
if (!source.success())
|
||||
{
|
||||
error_message = tr("Cannot create symbol source.");
|
||||
return;
|
||||
}
|
||||
|
||||
ccc::Result<ccc::GlobalVariable*> global_variable = database.global_variables.create_symbol(std::move(m_name), m_address, *source, nullptr);
|
||||
if (!global_variable.success())
|
||||
{
|
||||
error_message = tr("Cannot create symbol.");
|
||||
return;
|
||||
}
|
||||
|
||||
(*global_variable)->set_type(std::move(m_type));
|
||||
});
|
||||
|
||||
if (!error_message.isEmpty())
|
||||
QMessageBox::warning(this, tr("Cannot Create Global Variable"), error_message);
|
||||
}
|
||||
|
||||
// *****************************************************************************
|
||||
|
||||
NewLocalVariableDialog::NewLocalVariableDialog(DebugInterface& cpu, QWidget* parent)
|
||||
: NewSymbolDialog(GLOBAL_STORAGE | REGISTER_STORAGE | STACK_STORAGE | TYPE_FIELD | FUNCTION_FIELD, 1, cpu, parent)
|
||||
{
|
||||
setWindowTitle("New Local Variable");
|
||||
}
|
||||
|
||||
bool NewLocalVariableDialog::parseUserInput()
|
||||
{
|
||||
QString error_message;
|
||||
m_cpu.GetSymbolGuardian().Read([&](const ccc::SymbolDatabase& database) {
|
||||
m_name = parseName(error_message);
|
||||
if (!error_message.isEmpty())
|
||||
return;
|
||||
|
||||
int function_index = m_ui.functionComboBox->currentIndex();
|
||||
if (function_index > 0 && function_index < (int)m_functions.size())
|
||||
m_function = m_functions[m_ui.functionComboBox->currentIndex()];
|
||||
else
|
||||
m_function = ccc::FunctionHandle();
|
||||
|
||||
const ccc::Function* function = database.functions.symbol_from_handle(m_function);
|
||||
if (!function)
|
||||
{
|
||||
error_message = tr("Invalid function.");
|
||||
return;
|
||||
}
|
||||
|
||||
switch (storageType())
|
||||
{
|
||||
case GLOBAL_STORAGE:
|
||||
{
|
||||
m_storage.emplace<ccc::GlobalStorage>();
|
||||
|
||||
m_address = parseAddress(error_message);
|
||||
if (!error_message.isEmpty())
|
||||
return;
|
||||
|
||||
break;
|
||||
}
|
||||
case REGISTER_STORAGE:
|
||||
{
|
||||
ccc::RegisterStorage& register_storage = m_storage.emplace<ccc::RegisterStorage>();
|
||||
register_storage.dbx_register_number = m_ui.registerComboBox->currentIndex();
|
||||
break;
|
||||
}
|
||||
case STACK_STORAGE:
|
||||
{
|
||||
ccc::StackStorage& stack_storage = m_storage.emplace<ccc::StackStorage>();
|
||||
stack_storage.stack_pointer_offset = m_ui.stackPointerOffsetSpinBox->value();
|
||||
|
||||
// Convert to caller sp relative.
|
||||
if (std::optional<u32> stack_frame_size = m_cpu.getStackFrameSize(*function))
|
||||
stack_storage.stack_pointer_offset -= *stack_frame_size;
|
||||
else
|
||||
{
|
||||
error_message = tr("Cannot determine stack frame size of selected function.");
|
||||
return;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
std::string type_string = m_ui.typeLineEdit->text().toStdString();
|
||||
m_type = stringToType(type_string, database, error_message);
|
||||
if (!error_message.isEmpty())
|
||||
return;
|
||||
});
|
||||
|
||||
updateErrorMessage(error_message);
|
||||
return error_message.isEmpty();
|
||||
}
|
||||
|
||||
void NewLocalVariableDialog::createSymbol()
|
||||
{
|
||||
if (!parseUserInput())
|
||||
return;
|
||||
|
||||
QString error_message;
|
||||
m_cpu.GetSymbolGuardian().ReadWrite([&](ccc::SymbolDatabase& database) {
|
||||
ccc::Function* function = database.functions.symbol_from_handle(m_function);
|
||||
if (!function)
|
||||
{
|
||||
error_message = tr("Invalid function.");
|
||||
return;
|
||||
}
|
||||
|
||||
ccc::Result<ccc::SymbolSourceHandle> source = database.get_symbol_source("User-Defined");
|
||||
if (!source.success())
|
||||
{
|
||||
error_message = tr("Cannot create symbol source.");
|
||||
return;
|
||||
}
|
||||
|
||||
ccc::Result<ccc::LocalVariable*> local_variable =
|
||||
database.local_variables.create_symbol(std::move(m_name), m_address, *source, nullptr);
|
||||
if (!local_variable.success())
|
||||
{
|
||||
error_message = tr("Cannot create symbol.");
|
||||
return;
|
||||
}
|
||||
|
||||
(*local_variable)->set_type(std::move(m_type));
|
||||
(*local_variable)->storage = m_storage;
|
||||
|
||||
std::vector<ccc::LocalVariableHandle> local_variables;
|
||||
if (function->local_variables().has_value())
|
||||
local_variables = *function->local_variables();
|
||||
local_variables.emplace_back((*local_variable)->handle());
|
||||
function->set_local_variables(local_variables, database);
|
||||
});
|
||||
|
||||
if (!error_message.isEmpty())
|
||||
QMessageBox::warning(this, tr("Cannot Create Local Variable"), error_message);
|
||||
}
|
||||
|
||||
// *****************************************************************************
|
||||
|
||||
NewParameterVariableDialog::NewParameterVariableDialog(DebugInterface& cpu, QWidget* parent)
|
||||
: NewSymbolDialog(REGISTER_STORAGE | STACK_STORAGE | TYPE_FIELD | FUNCTION_FIELD, 1, cpu, parent)
|
||||
{
|
||||
setWindowTitle("New Parameter Variable");
|
||||
}
|
||||
|
||||
bool NewParameterVariableDialog::parseUserInput()
|
||||
{
|
||||
QString error_message;
|
||||
m_cpu.GetSymbolGuardian().Read([&](const ccc::SymbolDatabase& database) {
|
||||
m_name = parseName(error_message);
|
||||
if (!error_message.isEmpty())
|
||||
return;
|
||||
|
||||
int function_index = m_ui.functionComboBox->currentIndex();
|
||||
if (function_index > 0 && function_index < (int)m_functions.size())
|
||||
m_function = m_functions[m_ui.functionComboBox->currentIndex()];
|
||||
else
|
||||
m_function = ccc::FunctionHandle();
|
||||
|
||||
const ccc::Function* function = database.functions.symbol_from_handle(m_function);
|
||||
if (!function)
|
||||
{
|
||||
error_message = tr("Invalid function.");
|
||||
return;
|
||||
}
|
||||
|
||||
std::variant<ccc::RegisterStorage, ccc::StackStorage> storage;
|
||||
switch (storageType())
|
||||
{
|
||||
case GLOBAL_STORAGE:
|
||||
{
|
||||
error_message = tr("Invalid storage type.");
|
||||
return;
|
||||
}
|
||||
case REGISTER_STORAGE:
|
||||
{
|
||||
ccc::RegisterStorage& register_storage = storage.emplace<ccc::RegisterStorage>();
|
||||
register_storage.dbx_register_number = m_ui.registerComboBox->currentIndex();
|
||||
break;
|
||||
}
|
||||
case STACK_STORAGE:
|
||||
{
|
||||
ccc::StackStorage& stack_storage = storage.emplace<ccc::StackStorage>();
|
||||
stack_storage.stack_pointer_offset = m_ui.stackPointerOffsetSpinBox->value();
|
||||
|
||||
// Convert to caller sp relative.
|
||||
if (std::optional<u32> stack_frame_size = m_cpu.getStackFrameSize(*function))
|
||||
stack_storage.stack_pointer_offset -= *stack_frame_size;
|
||||
else
|
||||
{
|
||||
error_message = tr("Cannot determine stack frame size of selected function.");
|
||||
return;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
std::string type_string = m_ui.typeLineEdit->text().toStdString();
|
||||
m_type = stringToType(type_string, database, error_message);
|
||||
if (!error_message.isEmpty())
|
||||
return;
|
||||
});
|
||||
|
||||
updateErrorMessage(error_message);
|
||||
return error_message.isEmpty();
|
||||
}
|
||||
|
||||
void NewParameterVariableDialog::createSymbol()
|
||||
{
|
||||
if (!parseUserInput())
|
||||
return;
|
||||
|
||||
QString error_message;
|
||||
m_cpu.GetSymbolGuardian().ReadWrite([&](ccc::SymbolDatabase& database) {
|
||||
ccc::Function* function = database.functions.symbol_from_handle(m_function);
|
||||
if (!function)
|
||||
{
|
||||
error_message = tr("Invalid function.");
|
||||
return;
|
||||
}
|
||||
|
||||
ccc::Result<ccc::SymbolSourceHandle> source = database.get_symbol_source("User-Defined");
|
||||
if (!source.success())
|
||||
{
|
||||
error_message = tr("Cannot create symbol source.");
|
||||
return;
|
||||
}
|
||||
|
||||
ccc::Result<ccc::ParameterVariable*> parameter_variable =
|
||||
database.parameter_variables.create_symbol(std::move(m_name), *source, nullptr);
|
||||
if (!parameter_variable.success())
|
||||
{
|
||||
error_message = tr("Cannot create symbol.");
|
||||
return;
|
||||
}
|
||||
|
||||
(*parameter_variable)->set_type(std::move(m_type));
|
||||
(*parameter_variable)->storage = m_storage;
|
||||
|
||||
std::vector<ccc::ParameterVariableHandle> parameter_variables;
|
||||
if (function->parameter_variables().has_value())
|
||||
parameter_variables = *function->parameter_variables();
|
||||
parameter_variables.emplace_back((*parameter_variable)->handle());
|
||||
function->set_parameter_variables(parameter_variables, database);
|
||||
});
|
||||
|
||||
if (!error_message.isEmpty())
|
||||
QMessageBox::warning(this, tr("Cannot Create Parameter Variable"), error_message);
|
||||
}
|
||||
156
pcsx2-qt/Debugger/SymbolTree/NewSymbolDialogs.h
Normal file
156
pcsx2-qt/Debugger/SymbolTree/NewSymbolDialogs.h
Normal file
@@ -0,0 +1,156 @@
|
||||
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
|
||||
// SPDX-License-Identifier: GPL-3.0+
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QtWidgets/QDialog>
|
||||
|
||||
#include <ccc/ast.h>
|
||||
|
||||
#include "DebugTools/DebugInterface.h"
|
||||
#include "ui_NewSymbolDialog.h"
|
||||
|
||||
// Base class for symbol creation dialogs.
|
||||
class NewSymbolDialog : public QDialog
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
// Used to apply default settings.
|
||||
void setName(QString name);
|
||||
void setAddress(u32 address);
|
||||
void setCustomSize(u32 size);
|
||||
|
||||
protected:
|
||||
explicit NewSymbolDialog(u32 flags, u32 alignment, DebugInterface& cpu, QWidget* parent = nullptr);
|
||||
|
||||
enum Flags
|
||||
{
|
||||
GLOBAL_STORAGE = 1 << 0,
|
||||
REGISTER_STORAGE = 1 << 1,
|
||||
STACK_STORAGE = 1 << 2,
|
||||
SIZE_FIELD = 1 << 3,
|
||||
EXISTING_FUNCTIONS_FIELD = 1 << 4,
|
||||
TYPE_FIELD = 1 << 5,
|
||||
FUNCTION_FIELD = 1 << 6
|
||||
};
|
||||
|
||||
// Used for setting up row visibility. Keep in sync with the .ui file!
|
||||
enum Row
|
||||
{
|
||||
NAME,
|
||||
ADDRESS,
|
||||
REGISTER,
|
||||
STACK_POINTER_OFFSET,
|
||||
SIZE,
|
||||
EXISTING_FUNCTIONS,
|
||||
TYPE,
|
||||
FUNCTION
|
||||
};
|
||||
|
||||
protected slots:
|
||||
virtual bool parseUserInput() = 0;
|
||||
|
||||
protected:
|
||||
virtual void createSymbol() = 0;
|
||||
|
||||
void setupRegisterField();
|
||||
void setupSizeField();
|
||||
void setupFunctionField();
|
||||
|
||||
void connectInputWidgets();
|
||||
void updateErrorMessage(QString error_message);
|
||||
|
||||
enum FunctionSizeType
|
||||
{
|
||||
FILL_EXISTING_FUNCTION,
|
||||
FILL_EMPTY_SPACE,
|
||||
CUSTOM_SIZE
|
||||
};
|
||||
|
||||
FunctionSizeType functionSizeType() const;
|
||||
void updateSizeField();
|
||||
std::optional<u32> fillExistingFunctionSize(u32 address, const ccc::SymbolDatabase& database);
|
||||
std::optional<u32> fillEmptySpaceSize(u32 address, const ccc::SymbolDatabase& database);
|
||||
|
||||
u32 storageType() const;
|
||||
void onStorageTabChanged(int index);
|
||||
|
||||
std::string parseName(QString& error_message);
|
||||
u32 parseAddress(QString& error_message);
|
||||
|
||||
DebugInterface& m_cpu;
|
||||
Ui::NewSymbolDialog m_ui;
|
||||
|
||||
u32 m_alignment;
|
||||
std::vector<ccc::FunctionHandle> m_functions;
|
||||
};
|
||||
|
||||
class NewFunctionDialog : public NewSymbolDialog
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
NewFunctionDialog(DebugInterface& cpu, QWidget* parent = nullptr);
|
||||
|
||||
protected:
|
||||
bool parseUserInput() override;
|
||||
void createSymbol() override;
|
||||
|
||||
std::string m_name;
|
||||
u32 m_address = 0;
|
||||
u32 m_size = 0;
|
||||
ccc::FunctionHandle m_existing_function;
|
||||
u32 m_new_existing_function_size = 0;
|
||||
};
|
||||
|
||||
class NewGlobalVariableDialog : public NewSymbolDialog
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
NewGlobalVariableDialog(DebugInterface& cpu, QWidget* parent = nullptr);
|
||||
|
||||
protected:
|
||||
bool parseUserInput() override;
|
||||
void createSymbol() override;
|
||||
|
||||
std::string m_name;
|
||||
u32 m_address;
|
||||
std::unique_ptr<ccc::ast::Node> m_type;
|
||||
};
|
||||
|
||||
class NewLocalVariableDialog : public NewSymbolDialog
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
NewLocalVariableDialog(DebugInterface& cpu, QWidget* parent = nullptr);
|
||||
|
||||
protected:
|
||||
bool parseUserInput() override;
|
||||
void createSymbol() override;
|
||||
|
||||
std::string m_name;
|
||||
std::variant<ccc::GlobalStorage, ccc::RegisterStorage, ccc::StackStorage> m_storage;
|
||||
u32 m_address = 0;
|
||||
std::unique_ptr<ccc::ast::Node> m_type;
|
||||
ccc::FunctionHandle m_function;
|
||||
};
|
||||
|
||||
class NewParameterVariableDialog : public NewSymbolDialog
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
NewParameterVariableDialog(DebugInterface& cpu, QWidget* parent = nullptr);
|
||||
|
||||
protected:
|
||||
bool parseUserInput() override;
|
||||
void createSymbol() override;
|
||||
|
||||
std::string m_name;
|
||||
std::variant<ccc::RegisterStorage, ccc::StackStorage> m_storage;
|
||||
std::unique_ptr<ccc::ast::Node> m_type;
|
||||
ccc::FunctionHandle m_function;
|
||||
};
|
||||
484
pcsx2-qt/Debugger/SymbolTree/SymbolTreeDelegates.cpp
Normal file
484
pcsx2-qt/Debugger/SymbolTree/SymbolTreeDelegates.cpp
Normal file
@@ -0,0 +1,484 @@
|
||||
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
|
||||
// SPDX-License-Identifier: LGPL-3.0+
|
||||
|
||||
#include "SymbolTreeDelegates.h"
|
||||
|
||||
#include <QtWidgets/QCheckBox>
|
||||
#include <QtWidgets/QComboBox>
|
||||
#include <QtWidgets/QDoubleSpinBox>
|
||||
#include <QtWidgets/QLineEdit>
|
||||
#include <QtWidgets/QMessageBox>
|
||||
#include "Debugger/SymbolTree/SymbolTreeModel.h"
|
||||
#include "Debugger/SymbolTree/TypeString.h"
|
||||
#include "QtCompatibility.h"
|
||||
|
||||
SymbolTreeValueDelegate::SymbolTreeValueDelegate(
|
||||
DebugInterface& cpu,
|
||||
QObject* parent)
|
||||
: QStyledItemDelegate(parent)
|
||||
, m_cpu(cpu)
|
||||
{
|
||||
}
|
||||
|
||||
QWidget* SymbolTreeValueDelegate::createEditor(QWidget* parent, const QStyleOptionViewItem& option, const QModelIndex& index) const
|
||||
{
|
||||
if (!index.isValid())
|
||||
return nullptr;
|
||||
|
||||
const SymbolTreeModel* tree_model = qobject_cast<const SymbolTreeModel*>(index.model());
|
||||
if (!tree_model)
|
||||
return nullptr;
|
||||
|
||||
SymbolTreeNode* node = tree_model->nodeFromIndex(index);
|
||||
if (!node || !node->type.valid())
|
||||
return nullptr;
|
||||
|
||||
QWidget* result = nullptr;
|
||||
m_cpu.GetSymbolGuardian().Read([&](const ccc::SymbolDatabase& database) {
|
||||
const ccc::ast::Node* logical_type = node->type.lookup_node(database);
|
||||
if (!logical_type)
|
||||
return;
|
||||
|
||||
const ccc::ast::Node& physical_type = *logical_type->physical_type(database).first;
|
||||
QVariant value = node->readValueAsVariant(physical_type, m_cpu, database);
|
||||
|
||||
const ccc::ast::Node& type = *logical_type->physical_type(database).first;
|
||||
switch (type.descriptor)
|
||||
{
|
||||
case ccc::ast::BUILTIN:
|
||||
{
|
||||
const ccc::ast::BuiltIn& builtIn = type.as<ccc::ast::BuiltIn>();
|
||||
|
||||
switch (builtIn.bclass)
|
||||
{
|
||||
case ccc::ast::BuiltInClass::UNSIGNED_8:
|
||||
case ccc::ast::BuiltInClass::UNQUALIFIED_8:
|
||||
case ccc::ast::BuiltInClass::UNSIGNED_16:
|
||||
case ccc::ast::BuiltInClass::UNSIGNED_32:
|
||||
case ccc::ast::BuiltInClass::UNSIGNED_64:
|
||||
{
|
||||
QLineEdit* editor = new QLineEdit(parent);
|
||||
editor->setText(QString::number(value.toULongLong()));
|
||||
result = editor;
|
||||
|
||||
break;
|
||||
}
|
||||
case ccc::ast::BuiltInClass::SIGNED_8:
|
||||
case ccc::ast::BuiltInClass::SIGNED_16:
|
||||
case ccc::ast::BuiltInClass::SIGNED_32:
|
||||
case ccc::ast::BuiltInClass::SIGNED_64:
|
||||
{
|
||||
QLineEdit* editor = new QLineEdit(parent);
|
||||
editor->setText(QString::number(value.toLongLong()));
|
||||
result = editor;
|
||||
|
||||
break;
|
||||
}
|
||||
case ccc::ast::BuiltInClass::BOOL_8:
|
||||
{
|
||||
QCheckBox* editor = new QCheckBox(parent);
|
||||
editor->setChecked(value.toBool());
|
||||
connectCheckStateChanged(editor, this, &SymbolTreeValueDelegate::onCheckBoxStateChanged);
|
||||
result = editor;
|
||||
|
||||
break;
|
||||
}
|
||||
case ccc::ast::BuiltInClass::FLOAT_32:
|
||||
{
|
||||
QLineEdit* editor = new QLineEdit(parent);
|
||||
editor->setText(QString::number(value.toFloat()));
|
||||
result = editor;
|
||||
|
||||
break;
|
||||
}
|
||||
case ccc::ast::BuiltInClass::FLOAT_64:
|
||||
{
|
||||
QLineEdit* editor = new QLineEdit(parent);
|
||||
editor->setText(QString::number(value.toDouble()));
|
||||
result = editor;
|
||||
|
||||
break;
|
||||
}
|
||||
default:
|
||||
{
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case ccc::ast::ENUM:
|
||||
{
|
||||
const ccc::ast::Enum& enumeration = type.as<ccc::ast::Enum>();
|
||||
|
||||
QComboBox* combo_box = new QComboBox(parent);
|
||||
for (s32 i = 0; i < (s32)enumeration.constants.size(); i++)
|
||||
{
|
||||
combo_box->addItem(QString::fromStdString(enumeration.constants[i].second));
|
||||
if (enumeration.constants[i].first == value.toInt())
|
||||
combo_box->setCurrentIndex(i);
|
||||
}
|
||||
connect(combo_box, &QComboBox::currentIndexChanged, this, &SymbolTreeValueDelegate::onComboBoxIndexChanged);
|
||||
result = combo_box;
|
||||
|
||||
break;
|
||||
}
|
||||
case ccc::ast::POINTER_OR_REFERENCE:
|
||||
case ccc::ast::POINTER_TO_DATA_MEMBER:
|
||||
{
|
||||
QLineEdit* editor = new QLineEdit(parent);
|
||||
editor->setText(QString::number(value.toULongLong(), 16));
|
||||
result = editor;
|
||||
|
||||
break;
|
||||
}
|
||||
default:
|
||||
{
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
void SymbolTreeValueDelegate::setEditorData(QWidget* editor, const QModelIndex& index) const
|
||||
{
|
||||
// This function is intentionally left blank to prevent the values of
|
||||
// editors from constantly being reset every time the model is updated.
|
||||
}
|
||||
|
||||
void SymbolTreeValueDelegate::setModelData(QWidget* editor, QAbstractItemModel* model, const QModelIndex& index) const
|
||||
{
|
||||
if (!index.isValid())
|
||||
return;
|
||||
|
||||
const SymbolTreeModel* tree_model = qobject_cast<const SymbolTreeModel*>(index.model());
|
||||
if (!model)
|
||||
return;
|
||||
|
||||
SymbolTreeNode* node = tree_model->nodeFromIndex(index);
|
||||
if (!node || !node->type.valid())
|
||||
return;
|
||||
|
||||
QVariant value;
|
||||
m_cpu.GetSymbolGuardian().Read([&](const ccc::SymbolDatabase& database) {
|
||||
const ccc::ast::Node* logical_type = node->type.lookup_node(database);
|
||||
if (!logical_type)
|
||||
return;
|
||||
|
||||
const ccc::ast::Node& type = *logical_type->physical_type(database).first;
|
||||
switch (type.descriptor)
|
||||
{
|
||||
case ccc::ast::BUILTIN:
|
||||
{
|
||||
const ccc::ast::BuiltIn& builtIn = type.as<ccc::ast::BuiltIn>();
|
||||
|
||||
switch (builtIn.bclass)
|
||||
{
|
||||
case ccc::ast::BuiltInClass::UNSIGNED_8:
|
||||
case ccc::ast::BuiltInClass::UNQUALIFIED_8:
|
||||
case ccc::ast::BuiltInClass::UNSIGNED_16:
|
||||
case ccc::ast::BuiltInClass::UNSIGNED_32:
|
||||
case ccc::ast::BuiltInClass::UNSIGNED_64:
|
||||
{
|
||||
QLineEdit* line_edit = qobject_cast<QLineEdit*>(editor);
|
||||
Q_ASSERT(line_edit);
|
||||
|
||||
bool ok;
|
||||
qulonglong i = line_edit->text().toULongLong(&ok);
|
||||
if (ok)
|
||||
value = i;
|
||||
|
||||
break;
|
||||
}
|
||||
case ccc::ast::BuiltInClass::SIGNED_8:
|
||||
case ccc::ast::BuiltInClass::SIGNED_16:
|
||||
case ccc::ast::BuiltInClass::SIGNED_32:
|
||||
case ccc::ast::BuiltInClass::SIGNED_64:
|
||||
{
|
||||
QLineEdit* line_edit = qobject_cast<QLineEdit*>(editor);
|
||||
Q_ASSERT(line_edit);
|
||||
|
||||
bool ok;
|
||||
qlonglong i = line_edit->text().toLongLong(&ok);
|
||||
if (ok)
|
||||
value = i;
|
||||
|
||||
break;
|
||||
}
|
||||
case ccc::ast::BuiltInClass::BOOL_8:
|
||||
{
|
||||
QCheckBox* check_box = qobject_cast<QCheckBox*>(editor);
|
||||
value = check_box->isChecked();
|
||||
|
||||
break;
|
||||
}
|
||||
case ccc::ast::BuiltInClass::FLOAT_32:
|
||||
{
|
||||
QLineEdit* line_edit = qobject_cast<QLineEdit*>(editor);
|
||||
Q_ASSERT(line_edit);
|
||||
|
||||
bool ok;
|
||||
float f = line_edit->text().toFloat(&ok);
|
||||
if (ok)
|
||||
value = f;
|
||||
|
||||
break;
|
||||
}
|
||||
case ccc::ast::BuiltInClass::FLOAT_64:
|
||||
{
|
||||
QLineEdit* line_edit = qobject_cast<QLineEdit*>(editor);
|
||||
Q_ASSERT(line_edit);
|
||||
|
||||
bool ok;
|
||||
double d = line_edit->text().toDouble(&ok);
|
||||
if (ok)
|
||||
value = d;
|
||||
|
||||
break;
|
||||
}
|
||||
default:
|
||||
{
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case ccc::ast::ENUM:
|
||||
{
|
||||
const ccc::ast::Enum& enumeration = type.as<ccc::ast::Enum>();
|
||||
QComboBox* combo_box = qobject_cast<QComboBox*>(editor);
|
||||
Q_ASSERT(combo_box);
|
||||
|
||||
s32 comboIndex = combo_box->currentIndex();
|
||||
if (comboIndex < 0 || comboIndex >= (s32)enumeration.constants.size())
|
||||
break;
|
||||
|
||||
value = enumeration.constants[comboIndex].first;
|
||||
|
||||
break;
|
||||
}
|
||||
case ccc::ast::POINTER_OR_REFERENCE:
|
||||
case ccc::ast::POINTER_TO_DATA_MEMBER:
|
||||
{
|
||||
QLineEdit* line_edit = qobject_cast<QLineEdit*>(editor);
|
||||
Q_ASSERT(line_edit);
|
||||
|
||||
bool ok;
|
||||
qulonglong address = line_edit->text().toUInt(&ok, 16);
|
||||
if (ok)
|
||||
value = address;
|
||||
|
||||
break;
|
||||
}
|
||||
default:
|
||||
{
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (value.isValid())
|
||||
model->setData(index, value, SymbolTreeModel::EDIT_ROLE);
|
||||
}
|
||||
|
||||
void SymbolTreeValueDelegate::onCheckBoxStateChanged(Qt::CheckState state)
|
||||
{
|
||||
QCheckBox* check_box = qobject_cast<QCheckBox*>(sender());
|
||||
if (check_box)
|
||||
commitData(check_box);
|
||||
}
|
||||
|
||||
void SymbolTreeValueDelegate::onComboBoxIndexChanged(int index)
|
||||
{
|
||||
QComboBox* combo_box = qobject_cast<QComboBox*>(sender());
|
||||
if (combo_box)
|
||||
commitData(combo_box);
|
||||
}
|
||||
|
||||
// *****************************************************************************
|
||||
|
||||
SymbolTreeLocationDelegate::SymbolTreeLocationDelegate(
|
||||
DebugInterface& cpu,
|
||||
u32 alignment,
|
||||
QObject* parent)
|
||||
: QStyledItemDelegate(parent)
|
||||
, m_cpu(cpu)
|
||||
, m_alignment(alignment)
|
||||
{
|
||||
}
|
||||
|
||||
QWidget* SymbolTreeLocationDelegate::createEditor(QWidget* parent, const QStyleOptionViewItem& option, const QModelIndex& index) const
|
||||
{
|
||||
if (!index.isValid())
|
||||
return nullptr;
|
||||
|
||||
const SymbolTreeModel* model = qobject_cast<const SymbolTreeModel*>(index.model());
|
||||
if (!model)
|
||||
return nullptr;
|
||||
|
||||
SymbolTreeNode* node = model->nodeFromIndex(index);
|
||||
if (!node || !node->symbol.valid() || !node->symbol.is_flag_set(ccc::WITH_ADDRESS_MAP))
|
||||
return nullptr;
|
||||
|
||||
if (!node->is_location_editable)
|
||||
return nullptr;
|
||||
|
||||
return new QLineEdit(parent);
|
||||
}
|
||||
|
||||
void SymbolTreeLocationDelegate::setEditorData(QWidget* editor, const QModelIndex& index) const
|
||||
{
|
||||
if (!index.isValid())
|
||||
return;
|
||||
|
||||
const SymbolTreeModel* model = qobject_cast<const SymbolTreeModel*>(index.model());
|
||||
if (!model)
|
||||
return;
|
||||
|
||||
SymbolTreeNode* node = model->nodeFromIndex(index);
|
||||
if (!node || !node->symbol.valid())
|
||||
return;
|
||||
|
||||
QLineEdit* line_edit = qobject_cast<QLineEdit*>(editor);
|
||||
Q_ASSERT(line_edit);
|
||||
|
||||
m_cpu.GetSymbolGuardian().Read([&](const ccc::SymbolDatabase& database) {
|
||||
const ccc::Symbol* symbol = node->symbol.lookup_symbol(database);
|
||||
if (!symbol || !symbol->address().valid())
|
||||
return;
|
||||
|
||||
line_edit->setText(QString::number(symbol->address().value, 16));
|
||||
});
|
||||
}
|
||||
|
||||
void SymbolTreeLocationDelegate::setModelData(QWidget* editor, QAbstractItemModel* model, const QModelIndex& index) const
|
||||
{
|
||||
if (!index.isValid())
|
||||
return;
|
||||
|
||||
const SymbolTreeModel* tree_model = qobject_cast<const SymbolTreeModel*>(index.model());
|
||||
if (!tree_model)
|
||||
return;
|
||||
|
||||
SymbolTreeNode* node = tree_model->nodeFromIndex(index);
|
||||
if (!node || !node->symbol.valid() || !node->symbol.is_flag_set(ccc::WITH_ADDRESS_MAP))
|
||||
return;
|
||||
|
||||
QLineEdit* line_edit = qobject_cast<QLineEdit*>(editor);
|
||||
Q_ASSERT(line_edit);
|
||||
|
||||
SymbolTreeModel* symbol_tree_model = qobject_cast<SymbolTreeModel*>(model);
|
||||
Q_ASSERT(symbol_tree_model);
|
||||
|
||||
bool ok;
|
||||
u32 address = line_edit->text().toUInt(&ok, 16);
|
||||
if (!ok)
|
||||
return;
|
||||
|
||||
address -= address % m_alignment;
|
||||
|
||||
bool success = false;
|
||||
m_cpu.GetSymbolGuardian().ReadWrite([&](ccc::SymbolDatabase& database) {
|
||||
success = node->symbol.move_symbol(address, database);
|
||||
});
|
||||
|
||||
if (success)
|
||||
{
|
||||
node->location = SymbolTreeLocation(SymbolTreeLocation::MEMORY, address);
|
||||
symbol_tree_model->setData(index, QVariant(), SymbolTreeModel::UPDATE_FROM_MEMORY_ROLE);
|
||||
symbol_tree_model->resetChildren(index);
|
||||
}
|
||||
}
|
||||
|
||||
// *****************************************************************************
|
||||
|
||||
SymbolTreeTypeDelegate::SymbolTreeTypeDelegate(
|
||||
DebugInterface& cpu,
|
||||
QObject* parent)
|
||||
: QStyledItemDelegate(parent)
|
||||
, m_cpu(cpu)
|
||||
{
|
||||
}
|
||||
|
||||
QWidget* SymbolTreeTypeDelegate::createEditor(QWidget* parent, const QStyleOptionViewItem& option, const QModelIndex& index) const
|
||||
{
|
||||
if (!index.isValid())
|
||||
return nullptr;
|
||||
|
||||
const SymbolTreeModel* tree_model = qobject_cast<const SymbolTreeModel*>(index.model());
|
||||
if (!tree_model)
|
||||
return nullptr;
|
||||
|
||||
SymbolTreeNode* node = tree_model->nodeFromIndex(index);
|
||||
if (!node || !node->symbol.valid())
|
||||
return nullptr;
|
||||
|
||||
return new QLineEdit(parent);
|
||||
}
|
||||
|
||||
void SymbolTreeTypeDelegate::setEditorData(QWidget* editor, const QModelIndex& index) const
|
||||
{
|
||||
if (!index.isValid())
|
||||
return;
|
||||
|
||||
const SymbolTreeModel* tree_model = qobject_cast<const SymbolTreeModel*>(index.model());
|
||||
if (!tree_model)
|
||||
return;
|
||||
|
||||
SymbolTreeNode* node = tree_model->nodeFromIndex(index);
|
||||
if (!node || !node->symbol.valid())
|
||||
return;
|
||||
|
||||
QLineEdit* line_edit = qobject_cast<QLineEdit*>(editor);
|
||||
Q_ASSERT(line_edit);
|
||||
|
||||
m_cpu.GetSymbolGuardian().Read([&](const ccc::SymbolDatabase& database) {
|
||||
const ccc::Symbol* symbol = node->symbol.lookup_symbol(database);
|
||||
if (!symbol || !symbol->type())
|
||||
return;
|
||||
|
||||
line_edit->setText(typeToString(symbol->type(), database));
|
||||
});
|
||||
}
|
||||
|
||||
void SymbolTreeTypeDelegate::setModelData(QWidget* editor, QAbstractItemModel* model, const QModelIndex& index) const
|
||||
{
|
||||
if (!index.isValid())
|
||||
return;
|
||||
|
||||
const SymbolTreeModel* tree_model = qobject_cast<const SymbolTreeModel*>(index.model());
|
||||
if (!tree_model)
|
||||
return;
|
||||
|
||||
SymbolTreeNode* node = tree_model->nodeFromIndex(index);
|
||||
if (!node || !node->symbol.valid())
|
||||
return;
|
||||
|
||||
QLineEdit* line_edit = qobject_cast<QLineEdit*>(editor);
|
||||
Q_ASSERT(line_edit);
|
||||
|
||||
SymbolTreeModel* symbol_tree_model = qobject_cast<SymbolTreeModel*>(model);
|
||||
Q_ASSERT(symbol_tree_model);
|
||||
|
||||
QString error_message;
|
||||
m_cpu.GetSymbolGuardian().ReadWrite([&](ccc::SymbolDatabase& database) {
|
||||
ccc::Symbol* symbol = node->symbol.lookup_symbol(database);
|
||||
if (!symbol)
|
||||
{
|
||||
error_message = tr("Symbol no longer exists.");
|
||||
return;
|
||||
}
|
||||
|
||||
std::unique_ptr<ccc::ast::Node> type = stringToType(line_edit->text().toStdString(), database, error_message);
|
||||
if (!error_message.isEmpty())
|
||||
return;
|
||||
|
||||
symbol->set_type(std::move(type));
|
||||
node->type = ccc::NodeHandle(node->symbol.descriptor(), *symbol, symbol->type());
|
||||
});
|
||||
|
||||
if (error_message.isEmpty())
|
||||
{
|
||||
symbol_tree_model->setData(index, QVariant(), SymbolTreeModel::UPDATE_FROM_MEMORY_ROLE);
|
||||
symbol_tree_model->resetChildren(index);
|
||||
}
|
||||
else
|
||||
QMessageBox::warning(editor, tr("Cannot Change Type"), error_message);
|
||||
}
|
||||
68
pcsx2-qt/Debugger/SymbolTree/SymbolTreeDelegates.h
Normal file
68
pcsx2-qt/Debugger/SymbolTree/SymbolTreeDelegates.h
Normal file
@@ -0,0 +1,68 @@
|
||||
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
|
||||
// SPDX-License-Identifier: LGPL-3.0+
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QtWidgets/QStyledItemDelegate>
|
||||
|
||||
#include "DebugTools/DebugInterface.h"
|
||||
#include "DebugTools/SymbolGuardian.h"
|
||||
|
||||
class SymbolTreeValueDelegate : public QStyledItemDelegate
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
SymbolTreeValueDelegate(
|
||||
DebugInterface& cpu,
|
||||
QObject* parent = nullptr);
|
||||
|
||||
QWidget* createEditor(QWidget* parent, const QStyleOptionViewItem& option, const QModelIndex& index) const override;
|
||||
void setEditorData(QWidget* editor, const QModelIndex& index) const override;
|
||||
void setModelData(QWidget* editor, QAbstractItemModel* model, const QModelIndex& index) const override;
|
||||
|
||||
protected:
|
||||
// These make it so the values inputted are written back to memory
|
||||
// immediately when the widgets are interacted with rather than when they
|
||||
// are deselected.
|
||||
void onCheckBoxStateChanged(Qt::CheckState state);
|
||||
void onComboBoxIndexChanged(int index);
|
||||
|
||||
DebugInterface& m_cpu;
|
||||
};
|
||||
|
||||
class SymbolTreeLocationDelegate : public QStyledItemDelegate
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
SymbolTreeLocationDelegate(
|
||||
DebugInterface& cpu,
|
||||
u32 alignment,
|
||||
QObject* parent = nullptr);
|
||||
|
||||
QWidget* createEditor(QWidget* parent, const QStyleOptionViewItem& option, const QModelIndex& index) const override;
|
||||
void setEditorData(QWidget* editor, const QModelIndex& index) const override;
|
||||
void setModelData(QWidget* editor, QAbstractItemModel* model, const QModelIndex& index) const override;
|
||||
|
||||
protected:
|
||||
DebugInterface& m_cpu;
|
||||
u32 m_alignment;
|
||||
};
|
||||
|
||||
class SymbolTreeTypeDelegate : public QStyledItemDelegate
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
SymbolTreeTypeDelegate(
|
||||
DebugInterface& cpu,
|
||||
QObject* parent = nullptr);
|
||||
|
||||
QWidget* createEditor(QWidget* parent, const QStyleOptionViewItem& option, const QModelIndex& index) const override;
|
||||
void setEditorData(QWidget* editor, const QModelIndex& index) const override;
|
||||
void setModelData(QWidget* editor, QAbstractItemModel* model, const QModelIndex& index) const override;
|
||||
|
||||
protected:
|
||||
DebugInterface& m_cpu;
|
||||
};
|
||||
222
pcsx2-qt/Debugger/SymbolTree/SymbolTreeLocation.cpp
Normal file
222
pcsx2-qt/Debugger/SymbolTree/SymbolTreeLocation.cpp
Normal file
@@ -0,0 +1,222 @@
|
||||
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
|
||||
// SPDX-License-Identifier: LGPL-3.0+
|
||||
|
||||
#include "SymbolTreeLocation.h"
|
||||
|
||||
#include "DebugTools/DebugInterface.h"
|
||||
|
||||
SymbolTreeLocation::SymbolTreeLocation() = default;
|
||||
|
||||
SymbolTreeLocation::SymbolTreeLocation(Type type_arg, u32 address_arg)
|
||||
: type(type_arg)
|
||||
, address(address_arg)
|
||||
{
|
||||
}
|
||||
|
||||
QString SymbolTreeLocation::toString(DebugInterface& cpu) const
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case REGISTER:
|
||||
if (address < 32)
|
||||
return cpu.getRegisterName(0, address);
|
||||
else
|
||||
return QString("Reg %1").arg(address);
|
||||
case MEMORY:
|
||||
return QString::number(address, 16);
|
||||
default:
|
||||
{
|
||||
}
|
||||
}
|
||||
return QString();
|
||||
}
|
||||
|
||||
SymbolTreeLocation SymbolTreeLocation::addOffset(u32 offset) const
|
||||
{
|
||||
SymbolTreeLocation location;
|
||||
switch (type)
|
||||
{
|
||||
case REGISTER:
|
||||
if (offset == 0)
|
||||
location = *this;
|
||||
break;
|
||||
case MEMORY:
|
||||
location.type = type;
|
||||
location.address = address + offset;
|
||||
break;
|
||||
default:
|
||||
{
|
||||
}
|
||||
}
|
||||
return location;
|
||||
}
|
||||
|
||||
u8 SymbolTreeLocation::read8(DebugInterface& cpu) const
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case REGISTER:
|
||||
if (address < 32)
|
||||
return cpu.getRegister(EECAT_GPR, address)._u8[0];
|
||||
break;
|
||||
case MEMORY:
|
||||
return (u8)cpu.read8(address);
|
||||
default:
|
||||
{
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
u16 SymbolTreeLocation::read16(DebugInterface& cpu) const
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case REGISTER:
|
||||
if (address < 32)
|
||||
return cpu.getRegister(EECAT_GPR, address)._u16[0];
|
||||
break;
|
||||
case MEMORY:
|
||||
return (u16)cpu.read16(address);
|
||||
default:
|
||||
{
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
u32 SymbolTreeLocation::read32(DebugInterface& cpu) const
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case REGISTER:
|
||||
if (address < 32)
|
||||
return cpu.getRegister(EECAT_GPR, address)._u32[0];
|
||||
break;
|
||||
case MEMORY:
|
||||
return cpu.read32(address);
|
||||
default:
|
||||
{
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
u64 SymbolTreeLocation::read64(DebugInterface& cpu) const
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case REGISTER:
|
||||
if (address < 32)
|
||||
return cpu.getRegister(EECAT_GPR, address)._u64[0];
|
||||
break;
|
||||
case MEMORY:
|
||||
return cpu.read64(address);
|
||||
default:
|
||||
{
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
u128 SymbolTreeLocation::read128(DebugInterface& cpu) const
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case REGISTER:
|
||||
if (address < 32)
|
||||
return cpu.getRegister(EECAT_GPR, address);
|
||||
break;
|
||||
case MEMORY:
|
||||
return cpu.read128(address);
|
||||
default:
|
||||
{
|
||||
}
|
||||
}
|
||||
return u128::From32(0);
|
||||
}
|
||||
|
||||
void SymbolTreeLocation::write8(u8 value, DebugInterface& cpu) const
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case REGISTER:
|
||||
if (address < 32)
|
||||
cpu.setRegister(0, address, u128::From32(value));
|
||||
break;
|
||||
case MEMORY:
|
||||
cpu.write8(address, value);
|
||||
break;
|
||||
default:
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void SymbolTreeLocation::write16(u16 value, DebugInterface& cpu) const
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case REGISTER:
|
||||
if (address < 32)
|
||||
cpu.setRegister(0, address, u128::From32(value));
|
||||
break;
|
||||
case MEMORY:
|
||||
cpu.write16(address, value);
|
||||
break;
|
||||
default:
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void SymbolTreeLocation::write32(u32 value, DebugInterface& cpu) const
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case REGISTER:
|
||||
if (address < 32)
|
||||
cpu.setRegister(0, address, u128::From32(value));
|
||||
break;
|
||||
case MEMORY:
|
||||
cpu.write32(address, value);
|
||||
break;
|
||||
default:
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void SymbolTreeLocation::write64(u64 value, DebugInterface& cpu) const
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case REGISTER:
|
||||
if (address < 32)
|
||||
cpu.setRegister(0, address, u128::From64(value));
|
||||
break;
|
||||
case MEMORY:
|
||||
cpu.write64(address, value);
|
||||
break;
|
||||
default:
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void SymbolTreeLocation::write128(u128 value, DebugInterface& cpu) const
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case REGISTER:
|
||||
if (address < 32)
|
||||
cpu.setRegister(0, address, value);
|
||||
break;
|
||||
case MEMORY:
|
||||
cpu.write128(address, value);
|
||||
break;
|
||||
default:
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
46
pcsx2-qt/Debugger/SymbolTree/SymbolTreeLocation.h
Normal file
46
pcsx2-qt/Debugger/SymbolTree/SymbolTreeLocation.h
Normal file
@@ -0,0 +1,46 @@
|
||||
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
|
||||
// SPDX-License-Identifier: LGPL-3.0+
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QtCore/QString>
|
||||
|
||||
#include "common/Pcsx2Types.h"
|
||||
#include "DebugTools/DebugInterface.h"
|
||||
|
||||
class DebugInterface;
|
||||
|
||||
// A memory location, either a register or an address.
|
||||
struct SymbolTreeLocation
|
||||
{
|
||||
enum Type
|
||||
{
|
||||
REGISTER,
|
||||
MEMORY,
|
||||
NONE // Put NONE last so nodes of this type sort to the bottom.
|
||||
};
|
||||
|
||||
Type type = NONE;
|
||||
u32 address = 0;
|
||||
|
||||
SymbolTreeLocation();
|
||||
SymbolTreeLocation(Type type_arg, u32 address_arg);
|
||||
|
||||
QString toString(DebugInterface& cpu) const;
|
||||
|
||||
SymbolTreeLocation addOffset(u32 offset) const;
|
||||
|
||||
u8 read8(DebugInterface& cpu) const;
|
||||
u16 read16(DebugInterface& cpu) const;
|
||||
u32 read32(DebugInterface& cpu) const;
|
||||
u64 read64(DebugInterface& cpu) const;
|
||||
u128 read128(DebugInterface& cpu) const;
|
||||
|
||||
void write8(u8 value, DebugInterface& cpu) const;
|
||||
void write16(u16 value, DebugInterface& cpu) const;
|
||||
void write32(u32 value, DebugInterface& cpu) const;
|
||||
void write64(u64 value, DebugInterface& cpu) const;
|
||||
void write128(u128 value, DebugInterface& cpu) const;
|
||||
|
||||
friend auto operator<=>(const SymbolTreeLocation& lhs, const SymbolTreeLocation& rhs) = default;
|
||||
};
|
||||
523
pcsx2-qt/Debugger/SymbolTree/SymbolTreeModel.cpp
Normal file
523
pcsx2-qt/Debugger/SymbolTree/SymbolTreeModel.cpp
Normal file
@@ -0,0 +1,523 @@
|
||||
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
|
||||
// SPDX-License-Identifier: LGPL-3.0+
|
||||
|
||||
#include "SymbolTreeModel.h"
|
||||
|
||||
#include <QtWidgets/QApplication>
|
||||
#include <QtGui/QBrush>
|
||||
#include <QtGui/QPalette>
|
||||
|
||||
#include "common/Assertions.h"
|
||||
|
||||
#include "TypeString.h"
|
||||
|
||||
SymbolTreeModel::SymbolTreeModel(DebugInterface& cpu, QObject* parent)
|
||||
: QAbstractItemModel(parent)
|
||||
, m_cpu(cpu)
|
||||
{
|
||||
}
|
||||
|
||||
QModelIndex SymbolTreeModel::index(int row, int column, const QModelIndex& parent) const
|
||||
{
|
||||
SymbolTreeNode* parent_node = nodeFromIndex(parent);
|
||||
if (!parent_node)
|
||||
return QModelIndex();
|
||||
|
||||
if (row < 0 || row >= (int)parent_node->children().size())
|
||||
return QModelIndex();
|
||||
|
||||
const SymbolTreeNode* child_node = parent_node->children()[row].get();
|
||||
if (!child_node)
|
||||
return QModelIndex();
|
||||
|
||||
return createIndex(row, column, child_node);
|
||||
}
|
||||
|
||||
QModelIndex SymbolTreeModel::parent(const QModelIndex& index) const
|
||||
{
|
||||
if (!index.isValid())
|
||||
return QModelIndex();
|
||||
|
||||
SymbolTreeNode* child_node = nodeFromIndex(index);
|
||||
if (!child_node)
|
||||
return QModelIndex();
|
||||
|
||||
const SymbolTreeNode* parent_node = child_node->parent();
|
||||
if (!parent_node || parent_node == m_root.get())
|
||||
return QModelIndex();
|
||||
|
||||
return indexFromNode(*parent_node);
|
||||
}
|
||||
|
||||
int SymbolTreeModel::rowCount(const QModelIndex& parent) const
|
||||
{
|
||||
if (parent.column() > 0)
|
||||
return 0;
|
||||
|
||||
SymbolTreeNode* node = nodeFromIndex(parent);
|
||||
if (!node)
|
||||
return 0;
|
||||
|
||||
return (int)node->children().size();
|
||||
}
|
||||
|
||||
int SymbolTreeModel::columnCount(const QModelIndex& parent) const
|
||||
{
|
||||
return COLUMN_COUNT;
|
||||
}
|
||||
|
||||
bool SymbolTreeModel::hasChildren(const QModelIndex& parent) const
|
||||
{
|
||||
if (!parent.isValid())
|
||||
return true;
|
||||
|
||||
SymbolTreeNode* parent_node = nodeFromIndex(parent);
|
||||
if (!parent_node)
|
||||
return true;
|
||||
|
||||
// If a node doesn't have a type, it can't generate any children, so all the
|
||||
// children that will exist must already be there.
|
||||
if (!parent_node->type.valid())
|
||||
return !parent_node->children().empty();
|
||||
|
||||
bool result = true;
|
||||
m_cpu.GetSymbolGuardian().Read([&](const ccc::SymbolDatabase& database) -> void {
|
||||
const ccc::ast::Node* type = parent_node->type.lookup_node(database);
|
||||
if (!type)
|
||||
return;
|
||||
|
||||
result = nodeHasChildren(*type, database);
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
QVariant SymbolTreeModel::data(const QModelIndex& index, int role) const
|
||||
{
|
||||
if (!index.isValid())
|
||||
return QVariant();
|
||||
|
||||
SymbolTreeNode* node = nodeFromIndex(index);
|
||||
if (!node)
|
||||
return QVariant();
|
||||
|
||||
if (role == Qt::ForegroundRole)
|
||||
{
|
||||
bool active = true;
|
||||
|
||||
// Gray out the names of symbols that have been overwritten in memory.
|
||||
if (index.column() == NAME)
|
||||
active = node->matchesMemory();
|
||||
|
||||
// Gray out the values of variables that are dead.
|
||||
if (index.column() == VALUE && node->liveness().has_value())
|
||||
active = *node->liveness();
|
||||
|
||||
QPalette::ColorGroup group = active ? QPalette::Active : QPalette::Disabled;
|
||||
return QBrush(QApplication::palette().color(group, QPalette::Text));
|
||||
}
|
||||
|
||||
if (role != Qt::DisplayRole)
|
||||
return QVariant();
|
||||
|
||||
switch (index.column())
|
||||
{
|
||||
case NAME:
|
||||
{
|
||||
return node->name;
|
||||
}
|
||||
case VALUE:
|
||||
{
|
||||
if (node->tag != SymbolTreeNode::OBJECT)
|
||||
return QVariant();
|
||||
|
||||
return node->display_value();
|
||||
}
|
||||
case LOCATION:
|
||||
{
|
||||
return node->location.toString(m_cpu).rightJustified(8);
|
||||
}
|
||||
case SIZE:
|
||||
{
|
||||
if (!node->size.has_value())
|
||||
return QVariant();
|
||||
|
||||
return QString::number(*node->size);
|
||||
}
|
||||
case TYPE:
|
||||
{
|
||||
QVariant result;
|
||||
m_cpu.GetSymbolGuardian().Read([&](const ccc::SymbolDatabase& database) -> void {
|
||||
const ccc::ast::Node* type = node->type.lookup_node(database);
|
||||
if (!type)
|
||||
return;
|
||||
|
||||
result = typeToString(type, database);
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
case LIVENESS:
|
||||
{
|
||||
if (!node->liveness().has_value())
|
||||
return QVariant();
|
||||
|
||||
return *node->liveness() ? tr("Alive") : tr("Dead");
|
||||
}
|
||||
}
|
||||
|
||||
return QVariant();
|
||||
}
|
||||
|
||||
bool SymbolTreeModel::setData(const QModelIndex& index, const QVariant& value, int role)
|
||||
{
|
||||
if (!index.isValid())
|
||||
return false;
|
||||
|
||||
SymbolTreeNode* node = nodeFromIndex(index);
|
||||
if (!node)
|
||||
return false;
|
||||
|
||||
bool data_changed = false;
|
||||
m_cpu.GetSymbolGuardian().Read([&](const ccc::SymbolDatabase& database) -> void {
|
||||
switch (role)
|
||||
{
|
||||
case EDIT_ROLE:
|
||||
data_changed = node->writeToVM(value, m_cpu, database);
|
||||
break;
|
||||
case UPDATE_FROM_MEMORY_ROLE:
|
||||
data_changed = node->readFromVM(m_cpu, database);
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
if (data_changed)
|
||||
emit dataChanged(index.siblingAtColumn(0), index.siblingAtColumn(COLUMN_COUNT - 1));
|
||||
|
||||
return data_changed;
|
||||
}
|
||||
|
||||
void SymbolTreeModel::fetchMore(const QModelIndex& parent)
|
||||
{
|
||||
if (!parent.isValid())
|
||||
return;
|
||||
|
||||
SymbolTreeNode* parent_node = nodeFromIndex(parent);
|
||||
if (!parent_node || !parent_node->type.valid())
|
||||
return;
|
||||
|
||||
if (!parent_node->children().empty())
|
||||
return;
|
||||
|
||||
std::vector<std::unique_ptr<SymbolTreeNode>> children;
|
||||
m_cpu.GetSymbolGuardian().Read([&](const ccc::SymbolDatabase& database) -> void {
|
||||
const ccc::ast::Node* logical_parent_type = parent_node->type.lookup_node(database);
|
||||
if (!logical_parent_type)
|
||||
return;
|
||||
|
||||
children = populateChildren(
|
||||
parent_node->name, parent_node->location, *logical_parent_type, parent_node->type, m_cpu, database);
|
||||
});
|
||||
|
||||
bool insert_children = !children.empty();
|
||||
if (insert_children)
|
||||
beginInsertRows(parent, 0, children.size() - 1);
|
||||
parent_node->setChildren(std::move(children));
|
||||
if (insert_children)
|
||||
endInsertRows();
|
||||
}
|
||||
|
||||
bool SymbolTreeModel::canFetchMore(const QModelIndex& parent) const
|
||||
{
|
||||
if (!parent.isValid())
|
||||
return false;
|
||||
|
||||
SymbolTreeNode* parent_node = nodeFromIndex(parent);
|
||||
if (!parent_node || !parent_node->type.valid())
|
||||
return false;
|
||||
|
||||
bool result = false;
|
||||
m_cpu.GetSymbolGuardian().Read([&](const ccc::SymbolDatabase& database) -> void {
|
||||
const ccc::ast::Node* parent_type = parent_node->type.lookup_node(database);
|
||||
if (!parent_type)
|
||||
return;
|
||||
|
||||
result = nodeHasChildren(*parent_type, database) && !parent_node->childrenFetched();
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
Qt::ItemFlags SymbolTreeModel::flags(const QModelIndex& index) const
|
||||
{
|
||||
if (!index.isValid())
|
||||
return Qt::NoItemFlags;
|
||||
|
||||
Qt::ItemFlags flags = QAbstractItemModel::flags(index);
|
||||
|
||||
if (index.column() == LOCATION || index.column() == TYPE || index.column() == VALUE)
|
||||
flags |= Qt::ItemIsEditable;
|
||||
|
||||
return flags;
|
||||
}
|
||||
|
||||
QVariant SymbolTreeModel::headerData(int section, Qt::Orientation orientation, int role) const
|
||||
{
|
||||
if (orientation != Qt::Horizontal || role != Qt::DisplayRole)
|
||||
return QVariant();
|
||||
|
||||
switch (section)
|
||||
{
|
||||
case NAME:
|
||||
return tr("Name");
|
||||
case VALUE:
|
||||
return tr("Value");
|
||||
case LOCATION:
|
||||
return tr("Location");
|
||||
case SIZE:
|
||||
return tr("Size");
|
||||
case TYPE:
|
||||
return tr("Type");
|
||||
case LIVENESS:
|
||||
return tr("Liveness");
|
||||
}
|
||||
|
||||
return QVariant();
|
||||
}
|
||||
|
||||
QModelIndex SymbolTreeModel::indexFromNode(const SymbolTreeNode& node) const
|
||||
{
|
||||
int row = 0;
|
||||
if (node.parent())
|
||||
{
|
||||
for (int i = 0; i < (int)node.parent()->children().size(); i++)
|
||||
if (node.parent()->children()[i].get() == &node)
|
||||
row = i;
|
||||
}
|
||||
else
|
||||
row = 0;
|
||||
|
||||
return createIndex(row, 0, &node);
|
||||
}
|
||||
|
||||
SymbolTreeNode* SymbolTreeModel::nodeFromIndex(const QModelIndex& index) const
|
||||
{
|
||||
if (!index.isValid())
|
||||
return m_root.get();
|
||||
|
||||
SymbolTreeNode* node = static_cast<SymbolTreeNode*>(index.internalPointer());
|
||||
if (!node)
|
||||
return m_root.get();
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
void SymbolTreeModel::reset(std::unique_ptr<SymbolTreeNode> new_root)
|
||||
{
|
||||
beginResetModel();
|
||||
m_root = std::move(new_root);
|
||||
endResetModel();
|
||||
}
|
||||
|
||||
void SymbolTreeModel::resetChildren(QModelIndex index)
|
||||
{
|
||||
pxAssertRel(index.isValid(), "Invalid model index.");
|
||||
|
||||
SymbolTreeNode* node = nodeFromIndex(index);
|
||||
if (!node || node->tag != SymbolTreeNode::OBJECT)
|
||||
return;
|
||||
|
||||
resetChildrenRecursive(*node);
|
||||
}
|
||||
|
||||
void SymbolTreeModel::resetChildrenRecursive(SymbolTreeNode& node)
|
||||
{
|
||||
for (const std::unique_ptr<SymbolTreeNode>& child : node.children())
|
||||
resetChildrenRecursive(*child);
|
||||
|
||||
bool remove_rows = !node.children().empty();
|
||||
if (remove_rows)
|
||||
beginRemoveRows(indexFromNode(node), 0, node.children().size() - 1);
|
||||
node.clearChildren();
|
||||
if (remove_rows)
|
||||
endRemoveRows();
|
||||
}
|
||||
|
||||
bool SymbolTreeModel::needsReset() const
|
||||
{
|
||||
if (!m_root)
|
||||
return true;
|
||||
|
||||
bool needs_reset = false;
|
||||
m_cpu.GetSymbolGuardian().Read([&](const ccc::SymbolDatabase& database) {
|
||||
needs_reset = !m_root->anySymbolsValid(database);
|
||||
});
|
||||
|
||||
return needs_reset;
|
||||
}
|
||||
|
||||
std::optional<QString> SymbolTreeModel::changeTypeTemporarily(QModelIndex index, std::string_view type_string)
|
||||
{
|
||||
SymbolTreeNode* node = nodeFromIndex(index);
|
||||
if (!node)
|
||||
return std::nullopt;
|
||||
|
||||
resetChildren(index);
|
||||
|
||||
QString error_message;
|
||||
m_cpu.GetSymbolGuardian().Read([&](const ccc::SymbolDatabase& database) -> void {
|
||||
std::unique_ptr<ccc::ast::Node> type = stringToType(type_string, database, error_message);
|
||||
if (!error_message.isEmpty())
|
||||
return;
|
||||
|
||||
node->temporary_type = std::move(type);
|
||||
node->type = ccc::NodeHandle(node->temporary_type.get());
|
||||
});
|
||||
|
||||
setData(index, QVariant(), UPDATE_FROM_MEMORY_ROLE);
|
||||
|
||||
return error_message;
|
||||
}
|
||||
|
||||
std::optional<QString> SymbolTreeModel::typeFromModelIndexToString(QModelIndex index)
|
||||
{
|
||||
SymbolTreeNode* node = nodeFromIndex(index);
|
||||
if (!node || node->tag != SymbolTreeNode::OBJECT)
|
||||
return std::nullopt;
|
||||
|
||||
QString result;
|
||||
m_cpu.GetSymbolGuardian().Read([&](const ccc::SymbolDatabase& database) -> void {
|
||||
const ccc::ast::Node* type = node->type.lookup_node(database);
|
||||
if (!type)
|
||||
return;
|
||||
|
||||
result = typeToString(type, database);
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
std::vector<std::unique_ptr<SymbolTreeNode>> SymbolTreeModel::populateChildren(
|
||||
const QString& name,
|
||||
SymbolTreeLocation location,
|
||||
const ccc::ast::Node& logical_type,
|
||||
ccc::NodeHandle parent_handle,
|
||||
DebugInterface& cpu,
|
||||
const ccc::SymbolDatabase& database)
|
||||
{
|
||||
auto [physical_type, symbol] = logical_type.physical_type(database);
|
||||
|
||||
// If we went through a type name, we need to make the node handles for the
|
||||
// children point to the new symbol instead of the original one.
|
||||
if (symbol)
|
||||
parent_handle = ccc::NodeHandle(*symbol, nullptr);
|
||||
|
||||
std::vector<std::unique_ptr<SymbolTreeNode>> children;
|
||||
|
||||
switch (physical_type->descriptor)
|
||||
{
|
||||
case ccc::ast::ARRAY:
|
||||
{
|
||||
const ccc::ast::Array& array = physical_type->as<ccc::ast::Array>();
|
||||
|
||||
for (s32 i = 0; i < array.element_count; i++)
|
||||
{
|
||||
SymbolTreeLocation element_location = location.addOffset(i * array.element_type->size_bytes);
|
||||
if (element_location.type == SymbolTreeLocation::NONE)
|
||||
continue;
|
||||
|
||||
std::unique_ptr<SymbolTreeNode> element = std::make_unique<SymbolTreeNode>();
|
||||
element->name = QString("[%1]").arg(i);
|
||||
element->type = parent_handle.handle_for_child(array.element_type.get());
|
||||
element->location = element_location;
|
||||
children.emplace_back(std::move(element));
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case ccc::ast::POINTER_OR_REFERENCE:
|
||||
{
|
||||
const ccc::ast::PointerOrReference& pointer_or_reference = physical_type->as<ccc::ast::PointerOrReference>();
|
||||
|
||||
u32 address = location.read32(cpu);
|
||||
if (!cpu.isValidAddress(address))
|
||||
break;
|
||||
|
||||
std::unique_ptr<SymbolTreeNode> pointee = std::make_unique<SymbolTreeNode>();
|
||||
pointee->name = QString("*%1").arg(name);
|
||||
pointee->type = parent_handle.handle_for_child(pointer_or_reference.value_type.get());
|
||||
pointee->location = SymbolTreeLocation(SymbolTreeLocation::MEMORY, address);
|
||||
children.emplace_back(std::move(pointee));
|
||||
|
||||
break;
|
||||
}
|
||||
case ccc::ast::STRUCT_OR_UNION:
|
||||
{
|
||||
const ccc::ast::StructOrUnion& struct_or_union = physical_type->as<ccc::ast::StructOrUnion>();
|
||||
|
||||
std::vector<ccc::ast::StructOrUnion::FlatField> fields;
|
||||
struct_or_union.flatten_fields(fields, nullptr, database, true);
|
||||
|
||||
for (const ccc::ast::StructOrUnion::FlatField& field : fields)
|
||||
{
|
||||
if (field.symbol)
|
||||
parent_handle = ccc::NodeHandle(*field.symbol, nullptr);
|
||||
|
||||
SymbolTreeLocation field_location = location.addOffset(field.base_offset + field.node->offset_bytes);
|
||||
if (field_location.type == SymbolTreeLocation::NONE)
|
||||
continue;
|
||||
|
||||
std::unique_ptr<SymbolTreeNode> child_node = std::make_unique<SymbolTreeNode>();
|
||||
if (!field.node->name.empty())
|
||||
child_node->name = QString::fromStdString(field.node->name);
|
||||
else
|
||||
child_node->name = QString("(anonymous %1)").arg(ccc::ast::node_type_to_string(*field.node));
|
||||
child_node->type = parent_handle.handle_for_child(field.node);
|
||||
child_node->location = field_location;
|
||||
children.emplace_back(std::move(child_node));
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
default:
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
for (std::unique_ptr<SymbolTreeNode>& child : children)
|
||||
child->readFromVM(cpu, database);
|
||||
|
||||
return children;
|
||||
}
|
||||
|
||||
bool SymbolTreeModel::nodeHasChildren(const ccc::ast::Node& logical_type, const ccc::SymbolDatabase& database)
|
||||
{
|
||||
const ccc::ast::Node& type = *logical_type.physical_type(database).first;
|
||||
|
||||
bool result = false;
|
||||
switch (type.descriptor)
|
||||
{
|
||||
case ccc::ast::ARRAY:
|
||||
{
|
||||
const ccc::ast::Array& array = type.as<ccc::ast::Array>();
|
||||
result = array.element_count > 0;
|
||||
break;
|
||||
}
|
||||
case ccc::ast::POINTER_OR_REFERENCE:
|
||||
{
|
||||
result = true;
|
||||
break;
|
||||
}
|
||||
case ccc::ast::STRUCT_OR_UNION:
|
||||
{
|
||||
const ccc::ast::StructOrUnion& struct_or_union = type.as<ccc::ast::StructOrUnion>();
|
||||
result = !struct_or_union.base_classes.empty() || !struct_or_union.fields.empty();
|
||||
break;
|
||||
}
|
||||
default:
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
82
pcsx2-qt/Debugger/SymbolTree/SymbolTreeModel.h
Normal file
82
pcsx2-qt/Debugger/SymbolTree/SymbolTreeModel.h
Normal file
@@ -0,0 +1,82 @@
|
||||
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
|
||||
// SPDX-License-Identifier: LGPL-3.0+
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QtCore/QAbstractItemModel>
|
||||
|
||||
#include <ccc/ast.h>
|
||||
#include <ccc/symbol_database.h>
|
||||
|
||||
#include "common/Pcsx2Defs.h"
|
||||
#include "DebugTools/DebugInterface.h"
|
||||
#include "SymbolTreeNode.h"
|
||||
|
||||
// Model for the symbol trees. It will dynamically grow itself as the user
|
||||
// chooses to expand different nodes.
|
||||
class SymbolTreeModel : public QAbstractItemModel
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
enum Column
|
||||
{
|
||||
NAME = 0,
|
||||
VALUE = 1,
|
||||
LOCATION = 2,
|
||||
SIZE = 3,
|
||||
TYPE = 4,
|
||||
LIVENESS = 5,
|
||||
COLUMN_COUNT = 6
|
||||
};
|
||||
|
||||
enum SetDataRole
|
||||
{
|
||||
EDIT_ROLE = Qt::EditRole,
|
||||
UPDATE_FROM_MEMORY_ROLE = Qt::UserRole
|
||||
};
|
||||
|
||||
SymbolTreeModel(DebugInterface& cpu, QObject* parent = nullptr);
|
||||
|
||||
QModelIndex index(int row, int column, const QModelIndex& parent = QModelIndex()) const override;
|
||||
QModelIndex parent(const QModelIndex& index) const override;
|
||||
int rowCount(const QModelIndex& parent = QModelIndex()) const override;
|
||||
int columnCount(const QModelIndex& parent = QModelIndex()) const override;
|
||||
bool hasChildren(const QModelIndex& parent = QModelIndex()) const override;
|
||||
QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override;
|
||||
bool setData(const QModelIndex& index, const QVariant& value, int role = EDIT_ROLE) override;
|
||||
void fetchMore(const QModelIndex& parent) override;
|
||||
bool canFetchMore(const QModelIndex& parent) const override;
|
||||
Qt::ItemFlags flags(const QModelIndex& index) const override;
|
||||
QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override;
|
||||
|
||||
QModelIndex indexFromNode(const SymbolTreeNode& node) const;
|
||||
SymbolTreeNode* nodeFromIndex(const QModelIndex& index) const;
|
||||
|
||||
// Reset the whole model.
|
||||
void reset(std::unique_ptr<SymbolTreeNode> new_root);
|
||||
|
||||
// Remove all the children of a given node, and allow fetching again.
|
||||
void resetChildren(QModelIndex index);
|
||||
void resetChildrenRecursive(SymbolTreeNode& node);
|
||||
|
||||
bool needsReset() const;
|
||||
|
||||
std::optional<QString> changeTypeTemporarily(QModelIndex index, std::string_view type_string);
|
||||
std::optional<QString> typeFromModelIndexToString(QModelIndex index);
|
||||
|
||||
protected:
|
||||
static std::vector<std::unique_ptr<SymbolTreeNode>> populateChildren(
|
||||
const QString& name,
|
||||
SymbolTreeLocation location,
|
||||
const ccc::ast::Node& logical_type,
|
||||
ccc::NodeHandle parent_handle,
|
||||
DebugInterface& cpu,
|
||||
const ccc::SymbolDatabase& database);
|
||||
|
||||
static bool nodeHasChildren(const ccc::ast::Node& logical_type, const ccc::SymbolDatabase& database);
|
||||
|
||||
std::unique_ptr<SymbolTreeNode> m_root;
|
||||
QString m_filter;
|
||||
DebugInterface& m_cpu;
|
||||
};
|
||||
711
pcsx2-qt/Debugger/SymbolTree/SymbolTreeNode.cpp
Normal file
711
pcsx2-qt/Debugger/SymbolTree/SymbolTreeNode.cpp
Normal file
@@ -0,0 +1,711 @@
|
||||
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
|
||||
// SPDX-License-Identifier: LGPL-3.0+
|
||||
|
||||
#include "SymbolTreeNode.h"
|
||||
|
||||
#include <ccc/ast.h>
|
||||
|
||||
const QVariant& SymbolTreeNode::value() const
|
||||
{
|
||||
return m_value;
|
||||
}
|
||||
|
||||
const QString& SymbolTreeNode::display_value() const
|
||||
{
|
||||
return m_display_value;
|
||||
}
|
||||
|
||||
std::optional<bool> SymbolTreeNode::liveness()
|
||||
{
|
||||
return m_liveness;
|
||||
}
|
||||
|
||||
bool SymbolTreeNode::readFromVM(DebugInterface& cpu, const ccc::SymbolDatabase& database)
|
||||
{
|
||||
QVariant new_value;
|
||||
|
||||
const ccc::ast::Node* logical_type = type.lookup_node(database);
|
||||
if (logical_type)
|
||||
{
|
||||
const ccc::ast::Node& physical_type = *logical_type->physical_type(database).first;
|
||||
new_value = readValueAsVariant(physical_type, cpu, database);
|
||||
}
|
||||
|
||||
bool data_changed = false;
|
||||
|
||||
if (new_value != m_value)
|
||||
{
|
||||
m_value = std::move(new_value);
|
||||
data_changed = true;
|
||||
}
|
||||
|
||||
data_changed |= updateDisplayString(cpu, database);
|
||||
data_changed |= updateLiveness(cpu);
|
||||
data_changed |= updateMatchesMemory(cpu, database);
|
||||
|
||||
return data_changed;
|
||||
}
|
||||
|
||||
bool SymbolTreeNode::writeToVM(QVariant value, DebugInterface& cpu, const ccc::SymbolDatabase& database)
|
||||
{
|
||||
bool data_changed = false;
|
||||
|
||||
if (value != m_value)
|
||||
{
|
||||
m_value = std::move(value);
|
||||
data_changed = true;
|
||||
}
|
||||
|
||||
const ccc::ast::Node* logical_type = type.lookup_node(database);
|
||||
if (logical_type)
|
||||
{
|
||||
const ccc::ast::Node& physical_type = *logical_type->physical_type(database).first;
|
||||
writeValueFromVariant(m_value, physical_type, cpu);
|
||||
}
|
||||
|
||||
data_changed |= updateDisplayString(cpu, database);
|
||||
data_changed |= updateLiveness(cpu);
|
||||
|
||||
return data_changed;
|
||||
}
|
||||
|
||||
QVariant SymbolTreeNode::readValueAsVariant(const ccc::ast::Node& physical_type, DebugInterface& cpu, const ccc::SymbolDatabase& database) const
|
||||
{
|
||||
switch (physical_type.descriptor)
|
||||
{
|
||||
case ccc::ast::BUILTIN:
|
||||
{
|
||||
const ccc::ast::BuiltIn& builtIn = physical_type.as<ccc::ast::BuiltIn>();
|
||||
switch (builtIn.bclass)
|
||||
{
|
||||
case ccc::ast::BuiltInClass::UNSIGNED_8:
|
||||
return (qulonglong)location.read8(cpu);
|
||||
case ccc::ast::BuiltInClass::SIGNED_8:
|
||||
return (qlonglong)(s8)location.read8(cpu);
|
||||
case ccc::ast::BuiltInClass::UNQUALIFIED_8:
|
||||
return (qulonglong)location.read8(cpu);
|
||||
case ccc::ast::BuiltInClass::BOOL_8:
|
||||
return (bool)location.read8(cpu);
|
||||
case ccc::ast::BuiltInClass::UNSIGNED_16:
|
||||
return (qulonglong)location.read16(cpu);
|
||||
case ccc::ast::BuiltInClass::SIGNED_16:
|
||||
return (qlonglong)(s16)location.read16(cpu);
|
||||
case ccc::ast::BuiltInClass::UNSIGNED_32:
|
||||
return (qulonglong)location.read32(cpu);
|
||||
case ccc::ast::BuiltInClass::SIGNED_32:
|
||||
return (qlonglong)(s32)location.read32(cpu);
|
||||
case ccc::ast::BuiltInClass::FLOAT_32:
|
||||
{
|
||||
u32 value = location.read32(cpu);
|
||||
return *reinterpret_cast<float*>(&value);
|
||||
}
|
||||
case ccc::ast::BuiltInClass::UNSIGNED_64:
|
||||
return (qulonglong)location.read64(cpu);
|
||||
case ccc::ast::BuiltInClass::SIGNED_64:
|
||||
return (qlonglong)(s64)location.read64(cpu);
|
||||
case ccc::ast::BuiltInClass::FLOAT_64:
|
||||
{
|
||||
u64 value = location.read64(cpu);
|
||||
return *reinterpret_cast<double*>(&value);
|
||||
}
|
||||
default:
|
||||
{
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case ccc::ast::ENUM:
|
||||
return location.read32(cpu);
|
||||
case ccc::ast::POINTER_OR_REFERENCE:
|
||||
case ccc::ast::POINTER_TO_DATA_MEMBER:
|
||||
return location.read32(cpu);
|
||||
default:
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
return QVariant();
|
||||
}
|
||||
|
||||
bool SymbolTreeNode::writeValueFromVariant(QVariant value, const ccc::ast::Node& physical_type, DebugInterface& cpu) const
|
||||
{
|
||||
switch (physical_type.descriptor)
|
||||
{
|
||||
case ccc::ast::BUILTIN:
|
||||
{
|
||||
const ccc::ast::BuiltIn& built_in = physical_type.as<ccc::ast::BuiltIn>();
|
||||
|
||||
switch (built_in.bclass)
|
||||
{
|
||||
case ccc::ast::BuiltInClass::UNSIGNED_8:
|
||||
location.write8((u8)value.toULongLong(), cpu);
|
||||
break;
|
||||
case ccc::ast::BuiltInClass::SIGNED_8:
|
||||
location.write8((u8)(s8)value.toLongLong(), cpu);
|
||||
break;
|
||||
case ccc::ast::BuiltInClass::UNQUALIFIED_8:
|
||||
location.write8((u8)value.toULongLong(), cpu);
|
||||
break;
|
||||
case ccc::ast::BuiltInClass::BOOL_8:
|
||||
location.write8((u8)value.toBool(), cpu);
|
||||
break;
|
||||
case ccc::ast::BuiltInClass::UNSIGNED_16:
|
||||
location.write16((u16)value.toULongLong(), cpu);
|
||||
break;
|
||||
case ccc::ast::BuiltInClass::SIGNED_16:
|
||||
location.write16((u16)(s16)value.toLongLong(), cpu);
|
||||
break;
|
||||
case ccc::ast::BuiltInClass::UNSIGNED_32:
|
||||
location.write32((u32)value.toULongLong(), cpu);
|
||||
break;
|
||||
case ccc::ast::BuiltInClass::SIGNED_32:
|
||||
location.write32((u32)(s32)value.toLongLong(), cpu);
|
||||
break;
|
||||
case ccc::ast::BuiltInClass::FLOAT_32:
|
||||
{
|
||||
float f = value.toFloat();
|
||||
location.write32(*reinterpret_cast<u32*>(&f), cpu);
|
||||
break;
|
||||
}
|
||||
case ccc::ast::BuiltInClass::UNSIGNED_64:
|
||||
location.write64((u64)value.toULongLong(), cpu);
|
||||
break;
|
||||
case ccc::ast::BuiltInClass::SIGNED_64:
|
||||
location.write64((u64)(s64)value.toLongLong(), cpu);
|
||||
break;
|
||||
case ccc::ast::BuiltInClass::FLOAT_64:
|
||||
{
|
||||
double d = value.toDouble();
|
||||
location.write64(*reinterpret_cast<u64*>(&d), cpu);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case ccc::ast::ENUM:
|
||||
location.write32((u32)value.toULongLong(), cpu);
|
||||
break;
|
||||
case ccc::ast::POINTER_OR_REFERENCE:
|
||||
case ccc::ast::POINTER_TO_DATA_MEMBER:
|
||||
location.write32((u32)value.toULongLong(), cpu);
|
||||
break;
|
||||
default:
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SymbolTreeNode::updateDisplayString(DebugInterface& cpu, const ccc::SymbolDatabase& database)
|
||||
{
|
||||
QString result;
|
||||
|
||||
const ccc::ast::Node* logical_type = type.lookup_node(database);
|
||||
if (logical_type)
|
||||
{
|
||||
const ccc::ast::Node& physical_type = *logical_type->physical_type(database).first;
|
||||
result = generateDisplayString(physical_type, cpu, database, 0);
|
||||
}
|
||||
|
||||
if (result.isEmpty())
|
||||
{
|
||||
// We don't know how to display objects of this type, so just show the
|
||||
// first 4 bytes of it as a hex dump.
|
||||
u32 value = location.read32(cpu);
|
||||
result = QString("%1 %2 %3 %4")
|
||||
.arg(value & 0xff, 2, 16, QChar('0'))
|
||||
.arg((value >> 8) & 0xff, 2, 16, QChar('0'))
|
||||
.arg((value >> 16) & 0xff, 2, 16, QChar('0'))
|
||||
.arg((value >> 24) & 0xff, 2, 16, QChar('0'));
|
||||
}
|
||||
|
||||
if (result == m_display_value)
|
||||
return false;
|
||||
|
||||
m_display_value = std::move(result);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
QString SymbolTreeNode::generateDisplayString(
|
||||
const ccc::ast::Node& physical_type, DebugInterface& cpu, const ccc::SymbolDatabase& database, s32 depth) const
|
||||
{
|
||||
s32 max_elements_to_display = 0;
|
||||
switch (depth)
|
||||
{
|
||||
case 0:
|
||||
max_elements_to_display = 8;
|
||||
break;
|
||||
case 1:
|
||||
max_elements_to_display = 2;
|
||||
break;
|
||||
}
|
||||
|
||||
switch (physical_type.descriptor)
|
||||
{
|
||||
case ccc::ast::ARRAY:
|
||||
{
|
||||
const ccc::ast::Array& array = physical_type.as<ccc::ast::Array>();
|
||||
const ccc::ast::Node& element_type = *array.element_type->physical_type(database).first;
|
||||
|
||||
if (element_type.name == "char" && location.type == SymbolTreeLocation::MEMORY)
|
||||
{
|
||||
char* string = cpu.stringFromPointer(location.address);
|
||||
if (string)
|
||||
return QString("\"%1\"").arg(string);
|
||||
}
|
||||
|
||||
QString result;
|
||||
result += "{";
|
||||
|
||||
s32 elements_to_display = std::min(array.element_count, max_elements_to_display);
|
||||
for (s32 i = 0; i < elements_to_display; i++)
|
||||
{
|
||||
SymbolTreeNode node;
|
||||
node.location = location.addOffset(i * array.element_type->size_bytes);
|
||||
|
||||
QString element = node.generateDisplayString(element_type, cpu, database, depth + 1);
|
||||
if (element.isEmpty())
|
||||
element = QString("(%1)").arg(ccc::ast::node_type_to_string(element_type));
|
||||
result += element;
|
||||
|
||||
if (i + 1 != array.element_count)
|
||||
result += ",";
|
||||
}
|
||||
|
||||
if (elements_to_display != array.element_count)
|
||||
result += "...";
|
||||
|
||||
result += "}";
|
||||
return result;
|
||||
}
|
||||
case ccc::ast::BUILTIN:
|
||||
{
|
||||
const ccc::ast::BuiltIn& builtIn = physical_type.as<ccc::ast::BuiltIn>();
|
||||
|
||||
QString result;
|
||||
switch (builtIn.bclass)
|
||||
{
|
||||
case ccc::ast::BuiltInClass::UNSIGNED_8:
|
||||
result = QString::number(location.read8(cpu));
|
||||
break;
|
||||
case ccc::ast::BuiltInClass::SIGNED_8:
|
||||
result = QString::number((s8)location.read8(cpu));
|
||||
break;
|
||||
case ccc::ast::BuiltInClass::UNQUALIFIED_8:
|
||||
result = QString::number(location.read8(cpu));
|
||||
break;
|
||||
case ccc::ast::BuiltInClass::BOOL_8:
|
||||
result = location.read8(cpu) ? "true" : "false";
|
||||
break;
|
||||
case ccc::ast::BuiltInClass::UNSIGNED_16:
|
||||
result = QString::number(location.read16(cpu));
|
||||
break;
|
||||
case ccc::ast::BuiltInClass::SIGNED_16:
|
||||
result = QString::number((s16)location.read16(cpu));
|
||||
break;
|
||||
case ccc::ast::BuiltInClass::UNSIGNED_32:
|
||||
result = QString::number(location.read32(cpu));
|
||||
break;
|
||||
case ccc::ast::BuiltInClass::SIGNED_32:
|
||||
result = QString::number((s32)location.read32(cpu));
|
||||
break;
|
||||
case ccc::ast::BuiltInClass::FLOAT_32:
|
||||
{
|
||||
u32 value = location.read32(cpu);
|
||||
result = QString::number(*reinterpret_cast<float*>(&value));
|
||||
break;
|
||||
}
|
||||
case ccc::ast::BuiltInClass::UNSIGNED_64:
|
||||
result = QString::number(location.read64(cpu));
|
||||
break;
|
||||
case ccc::ast::BuiltInClass::SIGNED_64:
|
||||
result = QString::number((s64)location.read64(cpu));
|
||||
break;
|
||||
case ccc::ast::BuiltInClass::FLOAT_64:
|
||||
{
|
||||
u64 value = location.read64(cpu);
|
||||
result = QString::number(*reinterpret_cast<double*>(&value));
|
||||
break;
|
||||
}
|
||||
case ccc::ast::BuiltInClass::UNSIGNED_128:
|
||||
case ccc::ast::BuiltInClass::SIGNED_128:
|
||||
case ccc::ast::BuiltInClass::UNQUALIFIED_128:
|
||||
case ccc::ast::BuiltInClass::FLOAT_128:
|
||||
{
|
||||
if (depth > 0)
|
||||
{
|
||||
result = "(128-bit value)";
|
||||
break;
|
||||
}
|
||||
|
||||
for (s32 i = 0; i < 16; i++)
|
||||
{
|
||||
u8 value = location.addOffset(i).read8(cpu);
|
||||
result += QString("%1 ").arg(value, 2, 16, QChar('0'));
|
||||
if ((i + 1) % 4 == 0)
|
||||
result += " ";
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
default:
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
if (result.isEmpty())
|
||||
break;
|
||||
|
||||
if (builtIn.name == "char")
|
||||
{
|
||||
char c = location.read8(cpu);
|
||||
if (QChar::fromLatin1(c).isPrint())
|
||||
{
|
||||
if (depth == 0)
|
||||
result = result.leftJustified(3);
|
||||
result += QString(" '%1'").arg(c);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
case ccc::ast::ENUM:
|
||||
{
|
||||
s32 value = (s32)location.read32(cpu);
|
||||
const auto& enum_type = physical_type.as<ccc::ast::Enum>();
|
||||
for (auto [test_value, name] : enum_type.constants)
|
||||
{
|
||||
if (test_value == value)
|
||||
return QString::fromStdString(name);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case ccc::ast::POINTER_OR_REFERENCE:
|
||||
{
|
||||
const auto& pointer_or_reference = physical_type.as<ccc::ast::PointerOrReference>();
|
||||
const ccc::ast::Node& value_type =
|
||||
*pointer_or_reference.value_type->physical_type(database).first;
|
||||
|
||||
u32 address = location.read32(cpu);
|
||||
if (address == 0)
|
||||
return "NULL";
|
||||
|
||||
QString result = QString::number(address, 16);
|
||||
|
||||
if (pointer_or_reference.is_pointer && value_type.name == "char")
|
||||
{
|
||||
const char* string = cpu.stringFromPointer(address);
|
||||
if (string)
|
||||
result += QString(" \"%1\"").arg(string);
|
||||
}
|
||||
else if (depth == 0)
|
||||
{
|
||||
QString pointee = generateDisplayString(value_type, cpu, database, depth + 1);
|
||||
if (!pointee.isEmpty())
|
||||
result += QString(" -> %1").arg(pointee);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
case ccc::ast::POINTER_TO_DATA_MEMBER:
|
||||
{
|
||||
return QString::number(location.read32(cpu), 16);
|
||||
}
|
||||
case ccc::ast::STRUCT_OR_UNION:
|
||||
{
|
||||
const ccc::ast::StructOrUnion& struct_or_union = physical_type.as<ccc::ast::StructOrUnion>();
|
||||
|
||||
QString result;
|
||||
result += "{";
|
||||
|
||||
std::vector<ccc::ast::StructOrUnion::FlatField> fields;
|
||||
bool all_fields = struct_or_union.flatten_fields(fields, nullptr, database, true, 0, max_elements_to_display);
|
||||
|
||||
for (size_t i = 0; i < fields.size(); i++)
|
||||
{
|
||||
const ccc::ast::StructOrUnion::FlatField& field = fields[i];
|
||||
|
||||
SymbolTreeNode node;
|
||||
node.location = location.addOffset(field.base_offset + field.node->offset_bytes);
|
||||
|
||||
const ccc::ast::Node& field_type = *field.node->physical_type(database).first;
|
||||
QString field_value = node.generateDisplayString(field_type, cpu, database, depth + 1);
|
||||
if (field_value.isEmpty())
|
||||
field_value = QString("(%1)").arg(ccc::ast::node_type_to_string(field_type));
|
||||
|
||||
QString field_name = QString::fromStdString(field.node->name);
|
||||
result += QString(".%1=%2").arg(field_name).arg(field_value);
|
||||
|
||||
if (i + 1 != fields.size() || !all_fields)
|
||||
result += ",";
|
||||
}
|
||||
|
||||
if (!all_fields)
|
||||
result += "...";
|
||||
|
||||
result += "}";
|
||||
return result;
|
||||
}
|
||||
default:
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
return QString();
|
||||
}
|
||||
|
||||
bool SymbolTreeNode::updateLiveness(DebugInterface& cpu)
|
||||
{
|
||||
std::optional<bool> new_liveness;
|
||||
if (live_range.low.valid() && live_range.high.valid())
|
||||
{
|
||||
u32 pc = cpu.getPC();
|
||||
new_liveness = pc >= live_range.low && pc < live_range.high;
|
||||
}
|
||||
|
||||
if (new_liveness == m_liveness)
|
||||
return false;
|
||||
|
||||
m_liveness = new_liveness;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SymbolTreeNode::updateMatchesMemory(DebugInterface& cpu, const ccc::SymbolDatabase& database)
|
||||
{
|
||||
bool matching = true;
|
||||
|
||||
switch (symbol.descriptor())
|
||||
{
|
||||
case ccc::SymbolDescriptor::FUNCTION:
|
||||
{
|
||||
const ccc::Function* function = database.functions.symbol_from_handle(symbol.handle());
|
||||
if (!function || function->current_hash() == 0 || function->original_hash() == 0)
|
||||
return false;
|
||||
|
||||
matching = function->current_hash() == function->original_hash();
|
||||
|
||||
break;
|
||||
}
|
||||
case ccc::SymbolDescriptor::GLOBAL_VARIABLE:
|
||||
{
|
||||
const ccc::GlobalVariable* global_variable = database.global_variables.symbol_from_handle(symbol.handle());
|
||||
if (!global_variable)
|
||||
return false;
|
||||
|
||||
const ccc::SourceFile* source_file = database.source_files.symbol_from_handle(global_variable->source_file());
|
||||
if (!source_file)
|
||||
return false;
|
||||
|
||||
matching = source_file->functions_match();
|
||||
|
||||
break;
|
||||
}
|
||||
case ccc::SymbolDescriptor::LOCAL_VARIABLE:
|
||||
{
|
||||
const ccc::LocalVariable* local_variable = database.local_variables.symbol_from_handle(symbol.handle());
|
||||
if (!local_variable)
|
||||
return false;
|
||||
|
||||
const ccc::Function* function = database.functions.symbol_from_handle(local_variable->function());
|
||||
if (!function || function->current_hash() == 0 || function->original_hash() == 0)
|
||||
return false;
|
||||
|
||||
matching = function->current_hash() == function->original_hash();
|
||||
|
||||
break;
|
||||
}
|
||||
case ccc::SymbolDescriptor::PARAMETER_VARIABLE:
|
||||
{
|
||||
const ccc::ParameterVariable* parameter_variable = database.parameter_variables.symbol_from_handle(symbol.handle());
|
||||
if (!parameter_variable)
|
||||
return false;
|
||||
|
||||
const ccc::Function* function = database.functions.symbol_from_handle(parameter_variable->function());
|
||||
if (!function || function->current_hash() == 0 || function->original_hash() == 0)
|
||||
return false;
|
||||
|
||||
matching = function->current_hash() == function->original_hash();
|
||||
|
||||
break;
|
||||
}
|
||||
default:
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
if (matching == m_matches_memory)
|
||||
return false;
|
||||
|
||||
m_matches_memory = matching;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SymbolTreeNode::matchesMemory() const
|
||||
{
|
||||
return m_matches_memory;
|
||||
}
|
||||
|
||||
void SymbolTreeNode::updateSymbolHashes(std::span<const SymbolTreeNode*> nodes, DebugInterface& cpu, ccc::SymbolDatabase& database)
|
||||
{
|
||||
std::set<ccc::FunctionHandle> functions;
|
||||
std::set<ccc::SourceFile*> source_files;
|
||||
|
||||
// Determine which functions we need to hash again, and in the case of
|
||||
// global variables, which source files are associated with those functions
|
||||
// so that we can check if they still match.
|
||||
for (const SymbolTreeNode* node : nodes)
|
||||
{
|
||||
switch (node->symbol.descriptor())
|
||||
{
|
||||
case ccc::SymbolDescriptor::FUNCTION:
|
||||
{
|
||||
functions.emplace(node->symbol.handle());
|
||||
break;
|
||||
}
|
||||
case ccc::SymbolDescriptor::GLOBAL_VARIABLE:
|
||||
{
|
||||
const ccc::GlobalVariable* global_variable = database.global_variables.symbol_from_handle(node->symbol.handle());
|
||||
if (!global_variable)
|
||||
continue;
|
||||
|
||||
ccc::SourceFile* source_file = database.source_files.symbol_from_handle(global_variable->source_file());
|
||||
if (!source_file)
|
||||
continue;
|
||||
|
||||
for (ccc::FunctionHandle function : source_file->functions())
|
||||
functions.emplace(function);
|
||||
|
||||
source_files.emplace(source_file);
|
||||
|
||||
break;
|
||||
}
|
||||
case ccc::SymbolDescriptor::LOCAL_VARIABLE:
|
||||
{
|
||||
const ccc::LocalVariable* local_variable = database.local_variables.symbol_from_handle(node->symbol.handle());
|
||||
if (!local_variable)
|
||||
continue;
|
||||
|
||||
functions.emplace(local_variable->function());
|
||||
|
||||
break;
|
||||
}
|
||||
case ccc::SymbolDescriptor::PARAMETER_VARIABLE:
|
||||
{
|
||||
const ccc::ParameterVariable* parameter_variable = database.parameter_variables.symbol_from_handle(node->symbol.handle());
|
||||
if (!parameter_variable)
|
||||
continue;
|
||||
|
||||
functions.emplace(parameter_variable->function());
|
||||
|
||||
break;
|
||||
}
|
||||
default:
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update the hashes for the enumerated functions.
|
||||
for (ccc::FunctionHandle function_handle : functions)
|
||||
{
|
||||
ccc::Function* function = database.functions.symbol_from_handle(function_handle);
|
||||
if (!function || function->original_hash() == 0)
|
||||
continue;
|
||||
|
||||
std::optional<ccc::FunctionHash> hash = SymbolGuardian::HashFunction(*function, cpu);
|
||||
if (!hash.has_value())
|
||||
continue;
|
||||
|
||||
function->set_current_hash(*hash);
|
||||
}
|
||||
|
||||
// Check that the enumerated source files still have matching functions.
|
||||
for (ccc::SourceFile* source_file : source_files)
|
||||
source_file->check_functions_match(database);
|
||||
}
|
||||
|
||||
bool SymbolTreeNode::anySymbolsValid(const ccc::SymbolDatabase& database) const
|
||||
{
|
||||
if (symbol.lookup_symbol(database))
|
||||
return true;
|
||||
|
||||
for (const std::unique_ptr<SymbolTreeNode>& child : children())
|
||||
if (child->anySymbolsValid(database))
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
const SymbolTreeNode* SymbolTreeNode::parent() const
|
||||
{
|
||||
return m_parent;
|
||||
}
|
||||
|
||||
const std::vector<std::unique_ptr<SymbolTreeNode>>& SymbolTreeNode::children() const
|
||||
{
|
||||
return m_children;
|
||||
}
|
||||
|
||||
bool SymbolTreeNode::childrenFetched() const
|
||||
{
|
||||
return m_children_fetched;
|
||||
}
|
||||
|
||||
void SymbolTreeNode::setChildren(std::vector<std::unique_ptr<SymbolTreeNode>> new_children)
|
||||
{
|
||||
for (std::unique_ptr<SymbolTreeNode>& child : new_children)
|
||||
child->m_parent = this;
|
||||
m_children = std::move(new_children);
|
||||
m_children_fetched = true;
|
||||
}
|
||||
|
||||
void SymbolTreeNode::insertChildren(std::vector<std::unique_ptr<SymbolTreeNode>> new_children)
|
||||
{
|
||||
for (std::unique_ptr<SymbolTreeNode>& child : new_children)
|
||||
child->m_parent = this;
|
||||
m_children.insert(m_children.end(),
|
||||
std::make_move_iterator(new_children.begin()),
|
||||
std::make_move_iterator(new_children.end()));
|
||||
m_children_fetched = true;
|
||||
}
|
||||
|
||||
void SymbolTreeNode::emplaceChild(std::unique_ptr<SymbolTreeNode> new_child)
|
||||
{
|
||||
new_child->m_parent = this;
|
||||
m_children.emplace_back(std::move(new_child));
|
||||
m_children_fetched = true;
|
||||
}
|
||||
|
||||
void SymbolTreeNode::clearChildren()
|
||||
{
|
||||
m_children.clear();
|
||||
m_children_fetched = false;
|
||||
}
|
||||
|
||||
void SymbolTreeNode::sortChildrenRecursively(bool sort_by_if_type_is_known)
|
||||
{
|
||||
auto comparator = [&](const std::unique_ptr<SymbolTreeNode>& lhs, const std::unique_ptr<SymbolTreeNode>& rhs) -> bool {
|
||||
if (lhs->tag != rhs->tag)
|
||||
return lhs->tag < rhs->tag;
|
||||
if (sort_by_if_type_is_known && lhs->type.valid() != rhs->type.valid())
|
||||
return lhs->type.valid() > rhs->type.valid();
|
||||
if (lhs->location != rhs->location)
|
||||
return lhs->location < rhs->location;
|
||||
return lhs->name < rhs->name;
|
||||
};
|
||||
|
||||
std::sort(m_children.begin(), m_children.end(), comparator);
|
||||
|
||||
for (std::unique_ptr<SymbolTreeNode>& child : m_children)
|
||||
child->sortChildrenRecursively(sort_by_if_type_is_known);
|
||||
}
|
||||
92
pcsx2-qt/Debugger/SymbolTree/SymbolTreeNode.h
Normal file
92
pcsx2-qt/Debugger/SymbolTree/SymbolTreeNode.h
Normal file
@@ -0,0 +1,92 @@
|
||||
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
|
||||
// SPDX-License-Identifier: LGPL-3.0+
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QtCore/QString>
|
||||
#include <QtCore/QVariant>
|
||||
|
||||
#include "SymbolTreeLocation.h"
|
||||
|
||||
class DebugInterface;
|
||||
|
||||
// A node in a symbol tree model.
|
||||
struct SymbolTreeNode
|
||||
{
|
||||
public:
|
||||
enum Tag
|
||||
{
|
||||
ROOT,
|
||||
UNKNOWN_GROUP,
|
||||
GROUP,
|
||||
OBJECT
|
||||
};
|
||||
|
||||
Tag tag = OBJECT;
|
||||
ccc::MultiSymbolHandle symbol;
|
||||
QString name;
|
||||
QString mangled_name;
|
||||
SymbolTreeLocation location;
|
||||
bool is_location_editable = false;
|
||||
std::optional<u32> size;
|
||||
ccc::NodeHandle type;
|
||||
std::unique_ptr<ccc::ast::Node> temporary_type;
|
||||
ccc::AddressRange live_range;
|
||||
|
||||
SymbolTreeNode() {}
|
||||
~SymbolTreeNode() {}
|
||||
|
||||
SymbolTreeNode(const SymbolTreeNode& rhs) = delete;
|
||||
SymbolTreeNode& operator=(const SymbolTreeNode& rhs) = delete;
|
||||
|
||||
SymbolTreeNode(SymbolTreeNode&& rhs) = delete;
|
||||
SymbolTreeNode& operator=(SymbolTreeNode&& rhs) = delete;
|
||||
|
||||
// Generated from VM state, to be updated regularly.
|
||||
const QVariant& value() const;
|
||||
const QString& display_value() const;
|
||||
std::optional<bool> liveness();
|
||||
|
||||
// Read the value from the VM memory, update liveness information, and
|
||||
// generate a display string. Returns true if the data changed.
|
||||
bool readFromVM(DebugInterface& cpu, const ccc::SymbolDatabase& database);
|
||||
|
||||
// Write the value back to the VM memory. Returns true on success.
|
||||
bool writeToVM(QVariant value, DebugInterface& cpu, const ccc::SymbolDatabase& database);
|
||||
|
||||
QVariant readValueAsVariant(const ccc::ast::Node& physical_type, DebugInterface& cpu, const ccc::SymbolDatabase& database) const;
|
||||
bool writeValueFromVariant(QVariant value, const ccc::ast::Node& physical_type, DebugInterface& cpu) const;
|
||||
|
||||
bool updateDisplayString(DebugInterface& cpu, const ccc::SymbolDatabase& database);
|
||||
QString generateDisplayString(const ccc::ast::Node& physical_type, DebugInterface& cpu, const ccc::SymbolDatabase& database, s32 depth) const;
|
||||
|
||||
bool updateLiveness(DebugInterface& cpu);
|
||||
|
||||
bool updateMatchesMemory(DebugInterface& cpu, const ccc::SymbolDatabase& database);
|
||||
bool matchesMemory() const;
|
||||
|
||||
static void updateSymbolHashes(std::span<const SymbolTreeNode*> nodes, DebugInterface& cpu, ccc::SymbolDatabase& database);
|
||||
|
||||
bool anySymbolsValid(const ccc::SymbolDatabase& database) const;
|
||||
|
||||
const SymbolTreeNode* parent() const;
|
||||
|
||||
const std::vector<std::unique_ptr<SymbolTreeNode>>& children() const;
|
||||
bool childrenFetched() const;
|
||||
void setChildren(std::vector<std::unique_ptr<SymbolTreeNode>> new_children);
|
||||
void insertChildren(std::vector<std::unique_ptr<SymbolTreeNode>> new_children);
|
||||
void emplaceChild(std::unique_ptr<SymbolTreeNode> new_child);
|
||||
void clearChildren();
|
||||
|
||||
void sortChildrenRecursively(bool sort_by_if_type_is_known);
|
||||
|
||||
protected:
|
||||
QVariant m_value;
|
||||
QString m_display_value;
|
||||
std::optional<bool> m_liveness;
|
||||
bool m_matches_memory = true;
|
||||
|
||||
SymbolTreeNode* m_parent = nullptr;
|
||||
std::vector<std::unique_ptr<SymbolTreeNode>> m_children;
|
||||
bool m_children_fetched = false;
|
||||
};
|
||||
86
pcsx2-qt/Debugger/SymbolTree/SymbolTreeView.ui
Normal file
86
pcsx2-qt/Debugger/SymbolTree/SymbolTreeView.ui
Normal file
@@ -0,0 +1,86 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>SymbolTreeView</class>
|
||||
<widget class="QWidget" name="SymbolTreeView">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>400</width>
|
||||
<height>300</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Symbol Tree</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="vertical_layout">
|
||||
<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="QTreeView" name="treeView"/>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="bottomPanel">
|
||||
<property name="spacing">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QPushButton" name="refreshButton">
|
||||
<property name="text">
|
||||
<string>Refresh</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLineEdit" name="filterBox">
|
||||
<property name="placeholderText">
|
||||
<string>Filter</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="newButton">
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>26</width>
|
||||
<height>16777215</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>+</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="deleteButton">
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>26</width>
|
||||
<height>16777215</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>-</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
||||
1203
pcsx2-qt/Debugger/SymbolTree/SymbolTreeViews.cpp
Normal file
1203
pcsx2-qt/Debugger/SymbolTree/SymbolTreeViews.cpp
Normal file
File diff suppressed because it is too large
Load Diff
201
pcsx2-qt/Debugger/SymbolTree/SymbolTreeViews.h
Normal file
201
pcsx2-qt/Debugger/SymbolTree/SymbolTreeViews.h
Normal file
@@ -0,0 +1,201 @@
|
||||
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
|
||||
// SPDX-License-Identifier: GPL-3.0+
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "ui_SymbolTreeView.h"
|
||||
|
||||
#include "Debugger/DebuggerView.h"
|
||||
#include "Debugger/SymbolTree/SymbolTreeModel.h"
|
||||
|
||||
// A symbol tree view with its associated refresh button, filter box and
|
||||
// right-click menu. Supports grouping, sorting and various other settings.
|
||||
class SymbolTreeView : public DebuggerView
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
virtual ~SymbolTreeView();
|
||||
|
||||
void updateModel();
|
||||
void reset();
|
||||
void updateVisibleNodes(bool update_hashes);
|
||||
void expandGroups(QModelIndex index);
|
||||
|
||||
protected:
|
||||
struct SymbolWork
|
||||
{
|
||||
QString name;
|
||||
ccc::SymbolDescriptor descriptor;
|
||||
const ccc::Symbol* symbol = nullptr;
|
||||
const ccc::Module* module_symbol = nullptr;
|
||||
const ccc::Section* section = nullptr;
|
||||
const ccc::SourceFile* source_file = nullptr;
|
||||
};
|
||||
|
||||
SymbolTreeView(
|
||||
u32 flags,
|
||||
s32 symbol_address_alignment,
|
||||
const DebuggerViewParameters& parameters);
|
||||
|
||||
void resizeEvent(QResizeEvent* event) override;
|
||||
|
||||
void toJson(JsonValueWrapper& json) override;
|
||||
bool fromJson(const JsonValueWrapper& json) override;
|
||||
|
||||
void setupTree();
|
||||
std::unique_ptr<SymbolTreeNode> buildTree(const ccc::SymbolDatabase& database);
|
||||
|
||||
std::unique_ptr<SymbolTreeNode> groupBySourceFile(
|
||||
std::unique_ptr<SymbolTreeNode> child,
|
||||
const SymbolWork& child_work,
|
||||
SymbolTreeNode*& prev_group,
|
||||
const SymbolWork*& prev_work);
|
||||
|
||||
std::unique_ptr<SymbolTreeNode> groupBySection(
|
||||
std::unique_ptr<SymbolTreeNode> child,
|
||||
const SymbolWork& child_work,
|
||||
SymbolTreeNode*& prev_group,
|
||||
const SymbolWork*& prev_work);
|
||||
|
||||
std::unique_ptr<SymbolTreeNode> groupByModule(
|
||||
std::unique_ptr<SymbolTreeNode> child,
|
||||
const SymbolWork& child_work,
|
||||
SymbolTreeNode*& prev_group,
|
||||
const SymbolWork*& prev_work);
|
||||
|
||||
void openContextMenu(QPoint pos);
|
||||
|
||||
virtual bool needsReset() const;
|
||||
|
||||
virtual std::vector<SymbolWork> getSymbols(
|
||||
const QString& filter, const ccc::SymbolDatabase& database) = 0;
|
||||
|
||||
virtual std::unique_ptr<SymbolTreeNode> buildNode(
|
||||
SymbolWork& work, const ccc::SymbolDatabase& database) const = 0;
|
||||
|
||||
virtual void configureColumns() = 0;
|
||||
|
||||
virtual void onNewButtonPressed() = 0;
|
||||
void onDeleteButtonPressed();
|
||||
|
||||
void onCopyName();
|
||||
void onCopyMangledName();
|
||||
void onCopyLocation();
|
||||
void onRenameSymbol();
|
||||
void onResetChildren();
|
||||
void onChangeTypeTemporarily();
|
||||
|
||||
void onTreeViewClicked(const QModelIndex& index);
|
||||
|
||||
SymbolTreeNode* currentNode();
|
||||
|
||||
Ui::SymbolTreeView m_ui;
|
||||
|
||||
SymbolTreeModel* m_model = nullptr;
|
||||
|
||||
enum Flags
|
||||
{
|
||||
NO_SYMBOL_TREE_FLAGS = 0,
|
||||
ALLOW_GROUPING = 1 << 0,
|
||||
ALLOW_SORTING_BY_IF_TYPE_IS_KNOWN = 1 << 1,
|
||||
ALLOW_TYPE_ACTIONS = 1 << 2,
|
||||
ALLOW_MANGLED_NAME_ACTIONS = 1 << 3,
|
||||
CLICK_TO_GO_TO_IN_DISASSEMBLER = 1 << 4
|
||||
};
|
||||
|
||||
u32 m_flags;
|
||||
u32 m_symbol_address_alignment;
|
||||
|
||||
bool m_show_size_column = false;
|
||||
bool m_group_by_module = false;
|
||||
bool m_group_by_section = false;
|
||||
bool m_group_by_source_file = false;
|
||||
bool m_sort_by_if_type_is_known = false;
|
||||
};
|
||||
|
||||
class FunctionTreeView : public SymbolTreeView
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit FunctionTreeView(const DebuggerViewParameters& parameters);
|
||||
virtual ~FunctionTreeView();
|
||||
|
||||
protected:
|
||||
std::vector<SymbolWork> getSymbols(
|
||||
const QString& filter, const ccc::SymbolDatabase& database) override;
|
||||
|
||||
std::unique_ptr<SymbolTreeNode> buildNode(
|
||||
SymbolWork& work, const ccc::SymbolDatabase& database) const override;
|
||||
|
||||
void configureColumns() override;
|
||||
|
||||
void onNewButtonPressed() override;
|
||||
};
|
||||
|
||||
class GlobalVariableTreeView : public SymbolTreeView
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit GlobalVariableTreeView(const DebuggerViewParameters& parameters);
|
||||
virtual ~GlobalVariableTreeView();
|
||||
|
||||
protected:
|
||||
std::vector<SymbolWork> getSymbols(
|
||||
const QString& filter, const ccc::SymbolDatabase& database) override;
|
||||
|
||||
std::unique_ptr<SymbolTreeNode> buildNode(
|
||||
SymbolWork& work, const ccc::SymbolDatabase& database) const override;
|
||||
|
||||
void configureColumns() override;
|
||||
|
||||
void onNewButtonPressed() override;
|
||||
};
|
||||
|
||||
class LocalVariableTreeView : public SymbolTreeView
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit LocalVariableTreeView(const DebuggerViewParameters& parameters);
|
||||
virtual ~LocalVariableTreeView();
|
||||
|
||||
protected:
|
||||
bool needsReset() const override;
|
||||
|
||||
std::vector<SymbolWork> getSymbols(
|
||||
const QString& filter, const ccc::SymbolDatabase& database) override;
|
||||
|
||||
std::unique_ptr<SymbolTreeNode> buildNode(
|
||||
SymbolWork& work, const ccc::SymbolDatabase& database) const override;
|
||||
|
||||
void configureColumns() override;
|
||||
|
||||
void onNewButtonPressed() override;
|
||||
|
||||
ccc::FunctionHandle m_function;
|
||||
std::optional<u32> m_caller_stack_pointer;
|
||||
};
|
||||
|
||||
class ParameterVariableTreeView : public SymbolTreeView
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit ParameterVariableTreeView(const DebuggerViewParameters& parameters);
|
||||
virtual ~ParameterVariableTreeView();
|
||||
|
||||
protected:
|
||||
bool needsReset() const override;
|
||||
|
||||
std::vector<SymbolWork> getSymbols(
|
||||
const QString& filter, const ccc::SymbolDatabase& database) override;
|
||||
|
||||
std::unique_ptr<SymbolTreeNode> buildNode(
|
||||
SymbolWork& work, const ccc::SymbolDatabase& database) const override;
|
||||
|
||||
void configureColumns() override;
|
||||
|
||||
void onNewButtonPressed() override;
|
||||
|
||||
ccc::FunctionHandle m_function;
|
||||
std::optional<u32> m_caller_stack_pointer;
|
||||
};
|
||||
162
pcsx2-qt/Debugger/SymbolTree/TypeString.cpp
Normal file
162
pcsx2-qt/Debugger/SymbolTree/TypeString.cpp
Normal file
@@ -0,0 +1,162 @@
|
||||
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
|
||||
// SPDX-License-Identifier: LGPL-3.0+
|
||||
|
||||
#include "TypeString.h"
|
||||
|
||||
#include <QtWidgets/QApplication>
|
||||
|
||||
#include "common/Pcsx2Types.h"
|
||||
|
||||
std::unique_ptr<ccc::ast::Node> stringToType(std::string_view string, const ccc::SymbolDatabase& database, QString& error_out)
|
||||
{
|
||||
if (string.empty())
|
||||
return nullptr;
|
||||
|
||||
size_t i = string.size();
|
||||
|
||||
// Parse array subscripts and pointer characters.
|
||||
std::vector<s32> components;
|
||||
for (; i > 0; i--)
|
||||
{
|
||||
if (string[i - 1] == '*' || string[i - 1] == '&')
|
||||
{
|
||||
components.emplace_back(-string[i - 1]);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (string[i - 1] != ']' || i < 2)
|
||||
break;
|
||||
|
||||
size_t j = i - 1;
|
||||
for (; j > 0; j--)
|
||||
if (string[j - 1] < '0' || string[j - 1] > '9')
|
||||
break;
|
||||
|
||||
if (string[j - 1] != '[')
|
||||
break;
|
||||
|
||||
s32 element_count = atoi(&string[j]);
|
||||
if (element_count < 0 || element_count > 1024 * 1024)
|
||||
{
|
||||
error_out = QCoreApplication::tr("Invalid array subscript.");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
components.emplace_back(element_count);
|
||||
|
||||
i = j;
|
||||
}
|
||||
|
||||
// Lookup the type.
|
||||
std::string type_name_string(string.data(), string.data() + i);
|
||||
if (type_name_string.empty())
|
||||
{
|
||||
error_out = QCoreApplication::tr("No type name provided.");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
ccc::DataTypeHandle handle = database.data_types.first_handle_from_name(type_name_string);
|
||||
const ccc::DataType* data_type = database.data_types.symbol_from_handle(handle);
|
||||
if (!data_type || !data_type->type())
|
||||
{
|
||||
error_out = QCoreApplication::tr("Type '%1' not found.").arg(QString::fromStdString(type_name_string));
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
std::unique_ptr<ccc::ast::Node> result;
|
||||
|
||||
// Create the AST.
|
||||
std::unique_ptr<ccc::ast::TypeName> type_name = std::make_unique<ccc::ast::TypeName>();
|
||||
type_name->size_bytes = data_type->type()->size_bytes;
|
||||
type_name->data_type_handle = data_type->handle();
|
||||
type_name->source = ccc::ast::TypeNameSource::REFERENCE;
|
||||
result = std::move(type_name);
|
||||
|
||||
for (i = components.size(); i > 0; i--)
|
||||
{
|
||||
if (components[i - 1] < 0)
|
||||
{
|
||||
char pointer_character = -components[i - 1];
|
||||
|
||||
std::unique_ptr<ccc::ast::PointerOrReference> pointer_or_reference = std::make_unique<ccc::ast::PointerOrReference>();
|
||||
pointer_or_reference->size_bytes = 4;
|
||||
pointer_or_reference->is_pointer = pointer_character == '*';
|
||||
pointer_or_reference->value_type = std::move(result);
|
||||
result = std::move(pointer_or_reference);
|
||||
}
|
||||
else
|
||||
{
|
||||
s32 element_count = components[i - 1];
|
||||
|
||||
std::unique_ptr<ccc::ast::Array> array = std::make_unique<ccc::ast::Array>();
|
||||
array->size_bytes = element_count * result->size_bytes;
|
||||
array->element_type = std::move(result);
|
||||
array->element_count = element_count;
|
||||
result = std::move(array);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
QString typeToString(const ccc::ast::Node* type, const ccc::SymbolDatabase& database)
|
||||
{
|
||||
QString suffix;
|
||||
|
||||
// Traverse through arrays, pointers and references, and build a string
|
||||
// to be appended to the end of the type name.
|
||||
bool done_finding_arrays_pointers = false;
|
||||
while (!done_finding_arrays_pointers)
|
||||
{
|
||||
switch (type->descriptor)
|
||||
{
|
||||
case ccc::ast::ARRAY:
|
||||
{
|
||||
const ccc::ast::Array& array = type->as<ccc::ast::Array>();
|
||||
suffix.prepend(QString("[%1]").arg(array.element_count));
|
||||
type = array.element_type.get();
|
||||
break;
|
||||
}
|
||||
case ccc::ast::POINTER_OR_REFERENCE:
|
||||
{
|
||||
const ccc::ast::PointerOrReference& pointer_or_reference = type->as<ccc::ast::PointerOrReference>();
|
||||
suffix.prepend(pointer_or_reference.is_pointer ? '*' : '&');
|
||||
type = pointer_or_reference.value_type.get();
|
||||
break;
|
||||
}
|
||||
default:
|
||||
{
|
||||
done_finding_arrays_pointers = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Determine the actual type name, or at the very least the node type.
|
||||
QString name;
|
||||
switch (type->descriptor)
|
||||
{
|
||||
case ccc::ast::BUILTIN:
|
||||
{
|
||||
const ccc::ast::BuiltIn& built_in = type->as<ccc::ast::BuiltIn>();
|
||||
name = ccc::ast::builtin_class_to_string(built_in.bclass);
|
||||
break;
|
||||
}
|
||||
case ccc::ast::TYPE_NAME:
|
||||
{
|
||||
const ccc::ast::TypeName& type_name = type->as<ccc::ast::TypeName>();
|
||||
const ccc::DataType* data_type = database.data_types.symbol_from_handle(type_name.data_type_handle);
|
||||
if (data_type)
|
||||
{
|
||||
name = QString::fromStdString(data_type->name());
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
{
|
||||
name = ccc::ast::node_type_to_string(*type);
|
||||
}
|
||||
}
|
||||
|
||||
return name + suffix;
|
||||
}
|
||||
20
pcsx2-qt/Debugger/SymbolTree/TypeString.h
Normal file
20
pcsx2-qt/Debugger/SymbolTree/TypeString.h
Normal file
@@ -0,0 +1,20 @@
|
||||
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
|
||||
// SPDX-License-Identifier: LGPL-3.0+
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <string_view>
|
||||
|
||||
#include <QtCore/QString>
|
||||
|
||||
#include <ccc/ast.h>
|
||||
|
||||
// Take a string e.g. "int*[3]" and generates an AST. Supports type names by
|
||||
// themselves as well as pointers, references and arrays. Pointer characters
|
||||
// appear in the same order as they would in C source code, however array
|
||||
// subscripts appear in the opposite order, so that it is possible to specify a
|
||||
// pointer to an array.
|
||||
std::unique_ptr<ccc::ast::Node> stringToType(std::string_view string, const ccc::SymbolDatabase& database, QString& error_out);
|
||||
|
||||
// Opposite of stringToType. Takes an AST node and converts it to a string.
|
||||
QString typeToString(const ccc::ast::Node* type, const ccc::SymbolDatabase& database);
|
||||
132
pcsx2-qt/Debugger/ThreadModel.cpp
Normal file
132
pcsx2-qt/Debugger/ThreadModel.cpp
Normal file
@@ -0,0 +1,132 @@
|
||||
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
|
||||
// SPDX-License-Identifier: GPL-3.0+
|
||||
|
||||
#include "ThreadModel.h"
|
||||
|
||||
#include "QtUtils.h"
|
||||
|
||||
ThreadModel::ThreadModel(DebugInterface& cpu, QObject* parent)
|
||||
: QAbstractTableModel(parent)
|
||||
, m_cpu(cpu)
|
||||
{
|
||||
}
|
||||
|
||||
int ThreadModel::rowCount(const QModelIndex&) const
|
||||
{
|
||||
return m_cpu.GetThreadList().size();
|
||||
}
|
||||
|
||||
int ThreadModel::columnCount(const QModelIndex&) const
|
||||
{
|
||||
return ThreadModel::COLUMN_COUNT;
|
||||
}
|
||||
|
||||
QVariant ThreadModel::data(const QModelIndex& index, int role) const
|
||||
{
|
||||
const std::vector<std::unique_ptr<BiosThread>> threads = m_cpu.GetThreadList();
|
||||
|
||||
size_t row = static_cast<size_t>(index.row());
|
||||
if (row >= threads.size())
|
||||
return QVariant();
|
||||
|
||||
const BiosThread* thread = threads[row].get();
|
||||
|
||||
if (role == Qt::DisplayRole)
|
||||
{
|
||||
switch (index.column())
|
||||
{
|
||||
case ThreadModel::ID:
|
||||
return thread->TID();
|
||||
case ThreadModel::PC:
|
||||
{
|
||||
if (thread->Status() == ThreadStatus::THS_RUN)
|
||||
return QtUtils::FilledQStringFromValue(m_cpu.getPC(), 16);
|
||||
|
||||
return QtUtils::FilledQStringFromValue(thread->PC(), 16);
|
||||
}
|
||||
case ThreadModel::ENTRY:
|
||||
return QtUtils::FilledQStringFromValue(thread->EntryPoint(), 16);
|
||||
case ThreadModel::PRIORITY:
|
||||
return QString::number(thread->Priority());
|
||||
case ThreadModel::STATE:
|
||||
{
|
||||
const auto& state = ThreadStateStrings.find(thread->Status());
|
||||
if (state != ThreadStateStrings.end())
|
||||
return state->second;
|
||||
|
||||
return tr("INVALID");
|
||||
}
|
||||
case ThreadModel::WAIT_TYPE:
|
||||
{
|
||||
const auto& waitType = ThreadWaitStrings.find(thread->Wait());
|
||||
if (waitType != ThreadWaitStrings.end())
|
||||
return waitType->second;
|
||||
|
||||
return tr("INVALID");
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (role == Qt::UserRole)
|
||||
{
|
||||
switch (index.column())
|
||||
{
|
||||
case ThreadModel::ID:
|
||||
return thread->TID();
|
||||
case ThreadModel::PC:
|
||||
{
|
||||
if (thread->Status() == ThreadStatus::THS_RUN)
|
||||
return m_cpu.getPC();
|
||||
|
||||
return thread->PC();
|
||||
}
|
||||
case ThreadModel::ENTRY:
|
||||
return thread->EntryPoint();
|
||||
case ThreadModel::PRIORITY:
|
||||
return thread->Priority();
|
||||
case ThreadModel::STATE:
|
||||
return static_cast<u32>(thread->Status());
|
||||
case ThreadModel::WAIT_TYPE:
|
||||
return static_cast<u32>(thread->Wait());
|
||||
default:
|
||||
return QVariant();
|
||||
}
|
||||
}
|
||||
return QVariant();
|
||||
}
|
||||
|
||||
QVariant ThreadModel::headerData(int section, Qt::Orientation orientation, int role) const
|
||||
{
|
||||
if (role == Qt::DisplayRole && orientation == Qt::Horizontal)
|
||||
{
|
||||
switch (section)
|
||||
{
|
||||
case ThreadColumns::ID:
|
||||
//: Warning: short space limit. Abbreviate if needed.
|
||||
return tr("ID");
|
||||
case ThreadColumns::PC:
|
||||
//: Warning: short space limit. Abbreviate if needed. PC = Program Counter (location where the CPU is executing).
|
||||
return tr("PC");
|
||||
case ThreadColumns::ENTRY:
|
||||
//: Warning: short space limit. Abbreviate if needed.
|
||||
return tr("ENTRY");
|
||||
case ThreadColumns::PRIORITY:
|
||||
//: Warning: short space limit. Abbreviate if needed.
|
||||
return tr("PRIORITY");
|
||||
case ThreadColumns::STATE:
|
||||
//: Warning: short space limit. Abbreviate if needed.
|
||||
return tr("STATE");
|
||||
case ThreadColumns::WAIT_TYPE:
|
||||
//: Warning: short space limit. Abbreviate if needed.
|
||||
return tr("WAIT TYPE");
|
||||
default:
|
||||
return QVariant();
|
||||
}
|
||||
}
|
||||
return QVariant();
|
||||
}
|
||||
|
||||
void ThreadModel::refreshData()
|
||||
{
|
||||
beginResetModel();
|
||||
endResetModel();
|
||||
}
|
||||
91
pcsx2-qt/Debugger/ThreadModel.h
Normal file
91
pcsx2-qt/Debugger/ThreadModel.h
Normal file
@@ -0,0 +1,91 @@
|
||||
// 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"
|
||||
#include "DebugTools/BiosDebugData.h"
|
||||
|
||||
#include <map>
|
||||
|
||||
class ThreadModel : public QAbstractTableModel
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
enum ThreadColumns : int
|
||||
{
|
||||
ID = 0,
|
||||
PC,
|
||||
ENTRY,
|
||||
PRIORITY,
|
||||
STATE,
|
||||
WAIT_TYPE,
|
||||
COLUMN_COUNT
|
||||
};
|
||||
|
||||
static constexpr QHeaderView::ResizeMode HeaderResizeModes[ThreadColumns::COLUMN_COUNT] =
|
||||
{
|
||||
QHeaderView::ResizeMode::ResizeToContents,
|
||||
QHeaderView::ResizeMode::ResizeToContents,
|
||||
QHeaderView::ResizeMode::ResizeToContents,
|
||||
QHeaderView::ResizeMode::ResizeToContents,
|
||||
QHeaderView::ResizeMode::Stretch,
|
||||
QHeaderView::ResizeMode::Stretch,
|
||||
};
|
||||
|
||||
explicit ThreadModel(DebugInterface& cpu, QObject* parent = nullptr);
|
||||
|
||||
int rowCount(const QModelIndex& parent = QModelIndex()) const override;
|
||||
int columnCount(const QModelIndex& parent = QModelIndex()) const override;
|
||||
QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override;
|
||||
QVariant headerData(int section, Qt::Orientation orientation, int role) const override;
|
||||
|
||||
void refreshData();
|
||||
|
||||
private:
|
||||
const std::map<ThreadStatus, QString> ThreadStateStrings{
|
||||
//ADDING I18N comments here because the context string added by QtLinguist does not mention that these are thread states.
|
||||
//: Refers to a Thread State in the Debugger.
|
||||
{ThreadStatus::THS_BAD, tr("BAD")},
|
||||
//: Refers to a Thread State in the Debugger.
|
||||
{ThreadStatus::THS_RUN, tr("RUN")},
|
||||
//: Refers to a Thread State in the Debugger.
|
||||
{ThreadStatus::THS_READY, tr("READY")},
|
||||
//: Refers to a Thread State in the Debugger.
|
||||
{ThreadStatus::THS_WAIT, tr("WAIT")},
|
||||
//: Refers to a Thread State in the Debugger.
|
||||
{ThreadStatus::THS_SUSPEND, tr("SUSPEND")},
|
||||
//: Refers to a Thread State in the Debugger.
|
||||
{ThreadStatus::THS_WAIT_SUSPEND, tr("WAIT SUSPEND")},
|
||||
//: Refers to a Thread State in the Debugger.
|
||||
{ThreadStatus::THS_DORMANT, tr("DORMANT")},
|
||||
};
|
||||
|
||||
const std::map<WaitState, QString> ThreadWaitStrings{
|
||||
//ADDING I18N comments here because the context string added by QtLinguist does not mention that these are thread wait states.
|
||||
//: Refers to a Thread Wait State in the Debugger.
|
||||
{WaitState::NONE, tr("NONE")},
|
||||
//: Refers to a Thread Wait State in the Debugger.
|
||||
{WaitState::WAKEUP_REQ, tr("WAKEUP REQUEST")},
|
||||
//: Refers to a Thread Wait State in the Debugger.
|
||||
{WaitState::SEMA, tr("SEMAPHORE")},
|
||||
//: Refers to a Thread Wait State in the Debugger.
|
||||
{WaitState::SLEEP, tr("SLEEP")},
|
||||
//: Refers to a Thread Wait State in the Debugger.
|
||||
{WaitState::DELAY, tr("DELAY")},
|
||||
//: Refers to a Thread Wait State in the Debugger.
|
||||
{WaitState::EVENTFLAG, tr("EVENTFLAG")},
|
||||
//: Refers to a Thread Wait State in the Debugger.
|
||||
{WaitState::MBOX, tr("MBOX")},
|
||||
//: Refers to a Thread Wait State in the Debugger.
|
||||
{WaitState::VPOOL, tr("VPOOL")},
|
||||
//: Refers to a Thread Wait State in the Debugger.
|
||||
{WaitState::FIXPOOL, tr("FIXPOOL")},
|
||||
};
|
||||
|
||||
DebugInterface& m_cpu;
|
||||
};
|
||||
83
pcsx2-qt/Debugger/ThreadView.cpp
Normal file
83
pcsx2-qt/Debugger/ThreadView.cpp
Normal file
@@ -0,0 +1,83 @@
|
||||
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
|
||||
// SPDX-License-Identifier: GPL-3.0+
|
||||
|
||||
#include "ThreadView.h"
|
||||
|
||||
#include "QtUtils.h"
|
||||
|
||||
#include <QtGui/QClipboard>
|
||||
#include <QtWidgets/QMenu>
|
||||
|
||||
ThreadView::ThreadView(const DebuggerViewParameters& parameters)
|
||||
: DebuggerView(parameters, NO_DEBUGGER_FLAGS)
|
||||
, m_model(new ThreadModel(cpu()))
|
||||
, m_proxy_model(new QSortFilterProxyModel())
|
||||
{
|
||||
m_ui.setupUi(this);
|
||||
|
||||
m_ui.threadList->setContextMenuPolicy(Qt::CustomContextMenu);
|
||||
connect(m_ui.threadList, &QTableView::customContextMenuRequested, this, &ThreadView::openContextMenu);
|
||||
connect(m_ui.threadList, &QTableView::doubleClicked, this, &ThreadView::onDoubleClick);
|
||||
|
||||
m_proxy_model->setSourceModel(m_model);
|
||||
m_proxy_model->setSortRole(Qt::UserRole);
|
||||
m_ui.threadList->setModel(m_proxy_model);
|
||||
m_ui.threadList->setSortingEnabled(true);
|
||||
m_ui.threadList->sortByColumn(ThreadModel::ThreadColumns::ID, Qt::SortOrder::AscendingOrder);
|
||||
for (std::size_t i = 0; auto mode : ThreadModel::HeaderResizeModes)
|
||||
{
|
||||
m_ui.threadList->horizontalHeader()->setSectionResizeMode(i, mode);
|
||||
i++;
|
||||
}
|
||||
|
||||
receiveEvent<DebuggerEvents::VMUpdate>([this](const DebuggerEvents::VMUpdate& event) -> bool {
|
||||
m_model->refreshData();
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
void ThreadView::openContextMenu(QPoint pos)
|
||||
{
|
||||
if (!m_ui.threadList->selectionModel()->hasSelection())
|
||||
return;
|
||||
|
||||
QMenu* menu = new QMenu(m_ui.threadList);
|
||||
menu->setAttribute(Qt::WA_DeleteOnClose);
|
||||
|
||||
QAction* copy = menu->addAction(tr("Copy"));
|
||||
connect(copy, &QAction::triggered, [this]() {
|
||||
const QItemSelectionModel* selection_model = m_ui.threadList->selectionModel();
|
||||
if (!selection_model->hasSelection())
|
||||
return;
|
||||
|
||||
QGuiApplication::clipboard()->setText(m_model->data(selection_model->currentIndex()).toString());
|
||||
});
|
||||
|
||||
menu->addSeparator();
|
||||
|
||||
QAction* copy_all_as_csv = menu->addAction(tr("Copy all as CSV"));
|
||||
connect(copy_all_as_csv, &QAction::triggered, [this]() {
|
||||
QGuiApplication::clipboard()->setText(QtUtils::AbstractItemModelToCSV(m_ui.threadList->model()));
|
||||
});
|
||||
|
||||
menu->popup(m_ui.threadList->viewport()->mapToGlobal(pos));
|
||||
}
|
||||
|
||||
void ThreadView::onDoubleClick(const QModelIndex& index)
|
||||
{
|
||||
auto real_index = m_proxy_model->mapToSource(index);
|
||||
switch (index.column())
|
||||
{
|
||||
case ThreadModel::ThreadColumns::ENTRY:
|
||||
{
|
||||
goToInDisassembler(m_model->data(real_index, Qt::UserRole).toUInt(), true);
|
||||
break;
|
||||
}
|
||||
default: // Default to PC
|
||||
{
|
||||
QModelIndex pc_index = m_model->index(real_index.row(), ThreadModel::ThreadColumns::PC);
|
||||
goToInDisassembler(m_model->data(pc_index, Qt::UserRole).toUInt(), true);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
28
pcsx2-qt/Debugger/ThreadView.h
Normal file
28
pcsx2-qt/Debugger/ThreadView.h
Normal file
@@ -0,0 +1,28 @@
|
||||
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
|
||||
// SPDX-License-Identifier: GPL-3.0+
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "ui_ThreadView.h"
|
||||
|
||||
#include "DebuggerView.h"
|
||||
#include "ThreadModel.h"
|
||||
|
||||
#include <QtCore/QSortFilterProxyModel>
|
||||
|
||||
class ThreadView final : public DebuggerView
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
ThreadView(const DebuggerViewParameters& parameters);
|
||||
|
||||
void openContextMenu(QPoint pos);
|
||||
void onDoubleClick(const QModelIndex& index);
|
||||
|
||||
private:
|
||||
Ui::ThreadView m_ui;
|
||||
|
||||
ThreadModel* m_model;
|
||||
QSortFilterProxyModel* m_proxy_model;
|
||||
};
|
||||
39
pcsx2-qt/Debugger/ThreadView.ui
Normal file
39
pcsx2-qt/Debugger/ThreadView.ui
Normal file
@@ -0,0 +1,39 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>ThreadView</class>
|
||||
<widget class="QWidget" name="ThreadView">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>400</width>
|
||||
<height>300</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Threads</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="threadList"/>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
||||
Reference in New Issue
Block a user