First Commit
This commit is contained in:
150
pcsx2-qt/AboutDialog.cpp
Normal file
150
pcsx2-qt/AboutDialog.cpp
Normal file
@@ -0,0 +1,150 @@
|
||||
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
|
||||
// SPDX-License-Identifier: GPL-3.0+
|
||||
|
||||
#include "pcsx2/SupportURLs.h"
|
||||
|
||||
#include "AboutDialog.h"
|
||||
#include "QtHost.h"
|
||||
#include "QtUtils.h"
|
||||
|
||||
#include "common/FileSystem.h"
|
||||
#include "common/Path.h"
|
||||
#include "common/SmallString.h"
|
||||
|
||||
#include <QtCore/QFile>
|
||||
#include <QtCore/QString>
|
||||
#include <QtGui/QDesktopServices>
|
||||
#include <QtWidgets/QDialog>
|
||||
#include <QtWidgets/QDialogButtonBox>
|
||||
#include <QtWidgets/QPushButton>
|
||||
#include <QtWidgets/QTextBrowser>
|
||||
|
||||
static QString GetDocFileUrl(std::string_view name)
|
||||
{
|
||||
#ifdef _WIN32
|
||||
// Windows uses the docs directory in bin.
|
||||
const std::string path = Path::Combine(EmuFolders::AppRoot,
|
||||
TinyString::from_format("docs" FS_OSPATH_SEPARATOR_STR "{}", name));
|
||||
#else
|
||||
// Linux/Mac has this in the Resources directory.
|
||||
const std::string path = Path::Combine(EmuFolders::Resources,
|
||||
TinyString::from_format("docs" FS_OSPATH_SEPARATOR_STR "{}", name));
|
||||
#endif
|
||||
return QUrl::fromLocalFile(QString::fromStdString(path)).toString();
|
||||
}
|
||||
|
||||
AboutDialog::AboutDialog(QWidget* parent)
|
||||
: QDialog(parent)
|
||||
{
|
||||
m_ui.setupUi(this);
|
||||
|
||||
setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint);
|
||||
setFixedSize(geometry().width(), geometry().height());
|
||||
|
||||
m_ui.scmversion->setTextInteractionFlags(Qt::TextSelectableByMouse);
|
||||
m_ui.scmversion->setText(QtHost::GetAppNameAndVersion());
|
||||
|
||||
m_ui.links->setTextInteractionFlags(Qt::TextBrowserInteraction);
|
||||
m_ui.links->setText(QStringLiteral(
|
||||
R"(<a href="%1">%2</a> | <a href="%3">%4</a> | <a href="%5">%6</a> | <a href="%7">%8</a> | <a href="%9">%10</a>)")
|
||||
.arg(getWebsiteUrl())
|
||||
.arg(tr("Website"))
|
||||
.arg(getSupportForumsUrl())
|
||||
.arg(tr("Support Forums"))
|
||||
.arg(getGitHubRepositoryUrl())
|
||||
.arg(tr("GitHub Repository"))
|
||||
.arg(getLicenseUrl())
|
||||
.arg(tr("License"))
|
||||
.arg(getThirdPartyLicensesUrl())
|
||||
.arg(tr("Third-Party Licenses")));
|
||||
|
||||
connect(m_ui.links, &QLabel::linkActivated, this, &AboutDialog::linksLinkActivated);
|
||||
connect(m_ui.buttonBox, &QDialogButtonBox::rejected, this, &QDialog::close);
|
||||
}
|
||||
|
||||
AboutDialog::~AboutDialog() = default;
|
||||
|
||||
QString AboutDialog::getWebsiteUrl()
|
||||
{
|
||||
return QString::fromUtf8(PCSX2_WEBSITE_URL);
|
||||
}
|
||||
|
||||
QString AboutDialog::getSupportForumsUrl()
|
||||
{
|
||||
return QString::fromUtf8(PCSX2_FORUMS_URL);
|
||||
}
|
||||
|
||||
QString AboutDialog::getGitHubRepositoryUrl()
|
||||
{
|
||||
return QString::fromUtf8(PCSX2_GITHUB_URL);
|
||||
}
|
||||
|
||||
QString AboutDialog::getLicenseUrl()
|
||||
{
|
||||
return GetDocFileUrl("GPL.html");
|
||||
}
|
||||
|
||||
QString AboutDialog::getThirdPartyLicensesUrl()
|
||||
{
|
||||
return GetDocFileUrl("ThirdPartyLicenses.html");
|
||||
}
|
||||
|
||||
QString AboutDialog::getWikiUrl()
|
||||
{
|
||||
return QString::fromUtf8(PCSX2_WIKI_URL);
|
||||
}
|
||||
|
||||
QString AboutDialog::getDocumentationUrl()
|
||||
{
|
||||
return QString::fromUtf8(PCSX2_DOCUMENTATION_URL);
|
||||
}
|
||||
|
||||
QString AboutDialog::getDiscordServerUrl()
|
||||
{
|
||||
return QString::fromUtf8(PCSX2_DISCORD_URL);
|
||||
}
|
||||
|
||||
void AboutDialog::linksLinkActivated(const QString& link)
|
||||
{
|
||||
const QUrl url(link);
|
||||
if (!url.isValid())
|
||||
return;
|
||||
|
||||
if (!url.isLocalFile())
|
||||
{
|
||||
QDesktopServices::openUrl(url);
|
||||
return;
|
||||
}
|
||||
|
||||
showHTMLDialog(this, tr("View Document"), url.toLocalFile());
|
||||
}
|
||||
|
||||
void AboutDialog::showHTMLDialog(QWidget* parent, const QString& title, const QString& path)
|
||||
{
|
||||
QDialog dialog(parent);
|
||||
dialog.setMinimumSize(700, 400);
|
||||
dialog.setWindowTitle(title);
|
||||
dialog.setWindowIcon(QtHost::GetAppIcon());
|
||||
|
||||
QVBoxLayout* layout = new QVBoxLayout(&dialog);
|
||||
|
||||
QTextBrowser* tb = new QTextBrowser(&dialog);
|
||||
tb->setAcceptRichText(true);
|
||||
tb->setReadOnly(true);
|
||||
tb->setOpenExternalLinks(true);
|
||||
|
||||
QFile file(path);
|
||||
file.open(QIODevice::ReadOnly);
|
||||
if (const QByteArray data = file.readAll(); !data.isEmpty())
|
||||
tb->setText(QString::fromUtf8(data));
|
||||
else
|
||||
tb->setText(tr("File not found: %1").arg(path));
|
||||
|
||||
layout->addWidget(tb, 1);
|
||||
|
||||
QDialogButtonBox* bb = new QDialogButtonBox(QDialogButtonBox::Close, &dialog);
|
||||
connect(bb->button(QDialogButtonBox::Close), &QPushButton::clicked, &dialog, &QDialog::done);
|
||||
layout->addWidget(bb, 0);
|
||||
|
||||
dialog.exec();
|
||||
}
|
||||
33
pcsx2-qt/AboutDialog.h
Normal file
33
pcsx2-qt/AboutDialog.h
Normal file
@@ -0,0 +1,33 @@
|
||||
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
|
||||
// SPDX-License-Identifier: GPL-3.0+
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "ui_AboutDialog.h"
|
||||
#include <QtWidgets/QDialog>
|
||||
|
||||
class AboutDialog final : public QDialog
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit AboutDialog(QWidget* parent = nullptr);
|
||||
~AboutDialog();
|
||||
|
||||
static QString getWebsiteUrl();
|
||||
static QString getSupportForumsUrl();
|
||||
static QString getGitHubRepositoryUrl();
|
||||
static QString getLicenseUrl();
|
||||
static QString getThirdPartyLicensesUrl();
|
||||
static QString getWikiUrl();
|
||||
static QString getDocumentationUrl();
|
||||
static QString getDiscordServerUrl();
|
||||
|
||||
static void showHTMLDialog(QWidget* parent, const QString& title, const QString& url);
|
||||
|
||||
private Q_SLOTS:
|
||||
void linksLinkActivated(const QString& link);
|
||||
|
||||
private:
|
||||
Ui::AboutDialog m_ui;
|
||||
};
|
||||
178
pcsx2-qt/AboutDialog.ui
Normal file
178
pcsx2-qt/AboutDialog.ui
Normal file
@@ -0,0 +1,178 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>AboutDialog</class>
|
||||
<widget class="QDialog" name="AboutDialog">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>580</width>
|
||||
<height>300</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>About PCSX2</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<item>
|
||||
<spacer name="horizontalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Orientation::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeType">
|
||||
<enum>QSizePolicy::Policy::Preferred</enum>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="icon">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>1</width>
|
||||
<height>1</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
<property name="pixmap">
|
||||
<pixmap resource="resources/resources.qrc">:/icons/PCSX2logo.svg</pixmap>
|
||||
</property>
|
||||
<property name="scaledContents">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignmentFlag::AlignCenter</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer_2">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Orientation::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeType">
|
||||
<enum>QSizePolicy::Policy::Preferred</enum>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="scmversion">
|
||||
<property name="text">
|
||||
<string extracomment="SCM= Source Code Management">SCM Version</string>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignmentFlag::AlignCenter</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="description">
|
||||
<property name="text">
|
||||
<string><html><head/><body><p>PCSX2 is a free and open-source PlayStation 2 (PS2) emulator. Its purpose is to emulate the PS2's hardware, using a combination of MIPS CPU Interpreters, Recompilers and a Virtual Machine which manages hardware states and PS2 system memory. This allows you to play PS2 games on your PC, with many additional features and benefits.</p></body></html></string>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignmentFlag::AlignJustify|Qt::AlignmentFlag::AlignVCenter</set>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="verticalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Orientation::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeType">
|
||||
<enum>QSizePolicy::Policy::Minimum</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="disclaimer">
|
||||
<property name="font">
|
||||
<font>
|
||||
<bold>true</bold>
|
||||
</font>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string><html><head/><body><p>PlayStation 2 and PS2 are registered trademarks of Sony Interactive Entertainment. This application is not affiliated in any way with Sony Interactive Entertainment.</p></body></html></string>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignmentFlag::AlignJustify|Qt::AlignmentFlag::AlignVCenter</set>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="verticalSpacer_2">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Orientation::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeType">
|
||||
<enum>QSizePolicy::Policy::Minimum</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="links">
|
||||
<property name="text">
|
||||
<string notr="true">TextLabel</string>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignmentFlag::AlignCenter</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="verticalSpacer_3">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Orientation::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeType">
|
||||
<enum>QSizePolicy::Policy::Minimum</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QDialogButtonBox" name="buttonBox">
|
||||
<property name="standardButtons">
|
||||
<set>QDialogButtonBox::StandardButton::Close</set>
|
||||
</property>
|
||||
<property name="centerButtons">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources>
|
||||
<include location="resources/resources.qrc"/>
|
||||
</resources>
|
||||
<connections/>
|
||||
</ui>
|
||||
915
pcsx2-qt/AutoUpdaterDialog.cpp
Normal file
915
pcsx2-qt/AutoUpdaterDialog.cpp
Normal file
@@ -0,0 +1,915 @@
|
||||
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
|
||||
// SPDX-License-Identifier: GPL-3.0+
|
||||
|
||||
#include "AutoUpdaterDialog.h"
|
||||
#include "MainWindow.h"
|
||||
#include "QtHost.h"
|
||||
#include "QtProgressCallback.h"
|
||||
#include "QtUtils.h"
|
||||
|
||||
#include "pcsx2/BuildVersion.h"
|
||||
#include "pcsx2/Host.h"
|
||||
|
||||
#include "updater/UpdaterExtractor.h"
|
||||
|
||||
#include "common/Assertions.h"
|
||||
#include "common/CocoaTools.h"
|
||||
#include "common/Console.h"
|
||||
#include "common/Error.h"
|
||||
#include "common/FileSystem.h"
|
||||
#include "common/HTTPDownloader.h"
|
||||
#include "common/Path.h"
|
||||
#include "common/StringUtil.h"
|
||||
|
||||
#include "cpuinfo.h"
|
||||
|
||||
#include <functional>
|
||||
#include <QtCore/QCoreApplication>
|
||||
#include <QtCore/QDir>
|
||||
#include <QtCore/QFile>
|
||||
#include <QtCore/QFileInfo>
|
||||
#include <QtCore/QJsonArray>
|
||||
#include <QtCore/QJsonDocument>
|
||||
#include <QtCore/QJsonObject>
|
||||
#include <QtCore/QJsonValue>
|
||||
#include <QtCore/QProcess>
|
||||
#include <QtCore/QString>
|
||||
#include <QtCore/QTemporaryDir>
|
||||
#include <QtWidgets/QDialog>
|
||||
#include <QtWidgets/QMessageBox>
|
||||
#include <QtWidgets/QProgressDialog>
|
||||
|
||||
#ifdef _WIN32
|
||||
#include "common/RedtapeWindows.h"
|
||||
#include <shellapi.h>
|
||||
#endif
|
||||
|
||||
// Interval at which HTTP requests are polled.
|
||||
static constexpr u32 HTTP_POLL_INTERVAL = 10;
|
||||
|
||||
#if defined(_WIN32)
|
||||
#define UPDATE_PLATFORM_STR "Windows"
|
||||
#elif defined(__linux__)
|
||||
#define UPDATE_PLATFORM_STR "Linux"
|
||||
#elif defined(__APPLE__)
|
||||
#define UPDATE_PLATFORM_STR "MacOS"
|
||||
#endif
|
||||
|
||||
#ifdef MULTI_ISA_SHARED_COMPILATION
|
||||
// #undef UPDATE_ADDITIONAL_TAGS
|
||||
#elif _M_SSE >= 0x501
|
||||
#define UPDATE_ADDITIONAL_TAGS "AVX2"
|
||||
#else
|
||||
#define UPDATE_ADDITIONAL_TAGS "SSE4"
|
||||
#endif
|
||||
|
||||
#define LATEST_RELEASE_URL "https://api.pcsx2.net/v1/%1Releases?pageSize=1"
|
||||
#define CHANGES_URL "https://api.github.com/repos/PCSX2/pcsx2/compare/%1...%2"
|
||||
|
||||
// Available release channels.
|
||||
static const char* UPDATE_TAGS[] = {"stable", "nightly"};
|
||||
|
||||
// TODO: Make manual releases create this file, and make it contain `#define DEFAULT_UPDATER_CHANNEL "stable"`.
|
||||
#if __has_include("DefaultUpdaterChannel.h")
|
||||
#include "DefaultUpdaterChannel.h"
|
||||
#endif
|
||||
#ifndef DEFAULT_UPDATER_CHANNEL
|
||||
#define DEFAULT_UPDATER_CHANNEL "nightly"
|
||||
#endif
|
||||
|
||||
AutoUpdaterDialog::AutoUpdaterDialog(QWidget* parent /* = nullptr */)
|
||||
: QDialog(parent)
|
||||
{
|
||||
m_ui.setupUi(this);
|
||||
|
||||
setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint);
|
||||
|
||||
connect(m_ui.downloadAndInstall, &QPushButton::clicked, this, &AutoUpdaterDialog::downloadUpdateClicked);
|
||||
connect(m_ui.skipThisUpdate, &QPushButton::clicked, this, &AutoUpdaterDialog::skipThisUpdateClicked);
|
||||
connect(m_ui.remindMeLater, &QPushButton::clicked, this, &AutoUpdaterDialog::remindMeLaterClicked);
|
||||
|
||||
m_http = HTTPDownloader::Create(Host::GetHTTPUserAgent());
|
||||
if (!m_http)
|
||||
Console.Error("Failed to create HTTP downloader, auto updater will not be available.");
|
||||
}
|
||||
|
||||
AutoUpdaterDialog::~AutoUpdaterDialog() = default;
|
||||
|
||||
bool AutoUpdaterDialog::isSupported()
|
||||
{
|
||||
// Logic to detect whether we can use the auto updater.
|
||||
// We use tagged commit, because this gets set on nightly builds.
|
||||
if (!BuildVersion::GitTaggedCommit)
|
||||
return false;
|
||||
|
||||
#ifdef __linux__
|
||||
// For Linux, we need to check whether we're running from the appimage.
|
||||
if (!std::getenv("APPIMAGE"))
|
||||
{
|
||||
Console.Warning("We're a tagged commit, but not running from an AppImage. Disabling automatic updater.");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
#elif defined(_WIN32) || defined(__APPLE__)
|
||||
// Windows, MacOS - always supported.
|
||||
return true;
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
QStringList AutoUpdaterDialog::getTagList()
|
||||
{
|
||||
if (!isSupported())
|
||||
return QStringList();
|
||||
|
||||
return QStringList(std::begin(UPDATE_TAGS), std::end(UPDATE_TAGS));
|
||||
}
|
||||
|
||||
std::string AutoUpdaterDialog::getDefaultTag()
|
||||
{
|
||||
if (!isSupported())
|
||||
return {};
|
||||
|
||||
return DEFAULT_UPDATER_CHANNEL;
|
||||
}
|
||||
|
||||
QString AutoUpdaterDialog::getCurrentVersion()
|
||||
{
|
||||
return QString(BuildVersion::GitTag);
|
||||
}
|
||||
|
||||
QString AutoUpdaterDialog::getCurrentVersionDate()
|
||||
{
|
||||
return QString(BuildVersion::GitDate);
|
||||
}
|
||||
|
||||
QString AutoUpdaterDialog::getCurrentUpdateTag() const
|
||||
{
|
||||
if (!isSupported())
|
||||
return QString();
|
||||
|
||||
return QString::fromStdString(Host::GetBaseStringSettingValue("AutoUpdater", "UpdateTag", DEFAULT_UPDATER_CHANNEL));
|
||||
}
|
||||
|
||||
void AutoUpdaterDialog::reportError(const char* msg, ...)
|
||||
{
|
||||
std::va_list ap;
|
||||
va_start(ap, msg);
|
||||
std::string full_msg = StringUtil::StdStringFromFormatV(msg, ap);
|
||||
va_end(ap);
|
||||
|
||||
// don't display errors when we're doing an automatic background check, it's just annoying
|
||||
Console.Error("Updater Error: %s", full_msg.c_str());
|
||||
if (m_display_messages)
|
||||
QMessageBox::critical(this, tr("Updater Error"), QString::fromStdString(full_msg));
|
||||
}
|
||||
|
||||
bool AutoUpdaterDialog::ensureHttpReady()
|
||||
{
|
||||
if (!m_http)
|
||||
return false;
|
||||
|
||||
if (!m_http_poll_timer)
|
||||
{
|
||||
m_http_poll_timer = new QTimer(this);
|
||||
m_http_poll_timer->connect(m_http_poll_timer, &QTimer::timeout, this, &AutoUpdaterDialog::httpPollTimerPoll);
|
||||
}
|
||||
|
||||
if (!m_http_poll_timer->isActive())
|
||||
{
|
||||
m_http_poll_timer->setSingleShot(false);
|
||||
m_http_poll_timer->setInterval(HTTP_POLL_INTERVAL);
|
||||
m_http_poll_timer->start();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void AutoUpdaterDialog::httpPollTimerPoll()
|
||||
{
|
||||
pxAssert(m_http);
|
||||
m_http->PollRequests();
|
||||
|
||||
if (!m_http->HasAnyRequests())
|
||||
{
|
||||
Console.WriteLn("(AutoUpdaterDialog) All HTTP requests done.");
|
||||
m_http_poll_timer->stop();
|
||||
}
|
||||
}
|
||||
|
||||
void AutoUpdaterDialog::queueUpdateCheck(bool display_message)
|
||||
{
|
||||
m_display_messages = display_message;
|
||||
|
||||
if (isSupported())
|
||||
{
|
||||
if (!ensureHttpReady())
|
||||
{
|
||||
emit updateCheckCompleted();
|
||||
return;
|
||||
}
|
||||
|
||||
m_http->CreateRequest(QStringLiteral(LATEST_RELEASE_URL).arg(getCurrentUpdateTag()).toStdString(),
|
||||
std::bind(&AutoUpdaterDialog::getLatestReleaseComplete, this, std::placeholders::_1, std::placeholders::_3));
|
||||
}
|
||||
else
|
||||
{
|
||||
emit updateCheckCompleted();
|
||||
}
|
||||
}
|
||||
|
||||
void AutoUpdaterDialog::getLatestReleaseComplete(s32 status_code, std::vector<u8> data)
|
||||
{
|
||||
#ifdef _M_X86
|
||||
// should already be initialized, but just in case this somehow runs before the CPU thread starts setting up...
|
||||
cpuinfo_initialize();
|
||||
#endif
|
||||
|
||||
if (!isSupported())
|
||||
return;
|
||||
|
||||
bool found_update_info = false;
|
||||
|
||||
if (status_code == HTTPDownloader::HTTP_STATUS_OK)
|
||||
{
|
||||
QJsonParseError parse_error;
|
||||
QJsonDocument doc(QJsonDocument::fromJson(QByteArray(reinterpret_cast<const char*>(data.data()), data.size()), &parse_error));
|
||||
if (doc.isObject())
|
||||
{
|
||||
const QJsonObject doc_object(doc.object());
|
||||
const QJsonArray data_array(doc_object["data"].toArray());
|
||||
if (!data_array.isEmpty())
|
||||
{
|
||||
// just take the first one, that's all we requested anyway
|
||||
const QJsonObject data_object(data_array.first().toObject());
|
||||
const QJsonObject assets_object(data_object["assets"].toObject());
|
||||
const QJsonArray platform_array(assets_object[UPDATE_PLATFORM_STR].toArray());
|
||||
if (!platform_array.isEmpty())
|
||||
{
|
||||
QJsonObject best_asset;
|
||||
int best_asset_score = 0;
|
||||
|
||||
// search for usable files
|
||||
for (const QJsonValue& asset_value : platform_array)
|
||||
{
|
||||
const QJsonObject asset_object(asset_value.toObject());
|
||||
const QJsonArray additional_tags_array(asset_object["additionalTags"].toArray());
|
||||
bool is_symbols = false;
|
||||
bool is_installer = false;
|
||||
bool is_avx2 = false;
|
||||
bool is_sse4 = false;
|
||||
bool is_perfect_match = false;
|
||||
for (const QJsonValue& additional_tag : additional_tags_array)
|
||||
{
|
||||
const QString additional_tag_str(additional_tag.toString());
|
||||
if (additional_tag_str == QStringLiteral("symbols"))
|
||||
{
|
||||
// we're not interested in symbols downloads
|
||||
is_symbols = true;
|
||||
break;
|
||||
}
|
||||
if (additional_tag_str == QStringLiteral("installer"))
|
||||
{
|
||||
// we're not interested in installer download
|
||||
is_installer = true;
|
||||
break;
|
||||
}
|
||||
else if (additional_tag_str == QStringLiteral("SSE4"))
|
||||
{
|
||||
is_sse4 = true;
|
||||
}
|
||||
else if (additional_tag_str == QStringLiteral("AVX2"))
|
||||
{
|
||||
is_avx2 = true;
|
||||
}
|
||||
#ifdef UPDATE_ADDITIONAL_TAGS
|
||||
if (additional_tag_str == QStringLiteral(UPDATE_ADDITIONAL_TAGS))
|
||||
{
|
||||
// Found the same variant as what's currently running! But keep checking in case it's symbols.
|
||||
is_perfect_match = true;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
if (is_symbols)
|
||||
{
|
||||
// skip this asset
|
||||
continue;
|
||||
}
|
||||
|
||||
if (is_installer)
|
||||
{
|
||||
// skip this asset
|
||||
continue;
|
||||
}
|
||||
#ifdef _M_X86
|
||||
if (is_avx2 && cpuinfo_has_x86_avx2())
|
||||
{
|
||||
// skip this asset
|
||||
continue;
|
||||
}
|
||||
#endif
|
||||
|
||||
int score;
|
||||
if (is_perfect_match)
|
||||
score = 4; // #1 choice is the one matching this binary
|
||||
else if (is_avx2)
|
||||
score = 3; // Prefer AVX2 over SSE4 (support test was done above)
|
||||
else if (is_sse4)
|
||||
score = 2; // Prefer SSE4 over one with no tags at all
|
||||
else
|
||||
score = 1; // Multi-ISA builds will have no tags, they'll only get picked because they're the only available build
|
||||
|
||||
if (score > best_asset_score)
|
||||
{
|
||||
best_asset = std::move(asset_object);
|
||||
best_asset_score = score;
|
||||
}
|
||||
}
|
||||
|
||||
if (best_asset_score == 0)
|
||||
{
|
||||
reportError("no matching assets found");
|
||||
}
|
||||
else
|
||||
{
|
||||
m_latest_version = data_object["version"].toString();
|
||||
m_latest_version_timestamp = QDateTime::fromString(data_object["publishedAt"].toString(), QStringLiteral("yyyy-MM-ddThh:mm:ss.zzzZ"));
|
||||
m_download_url = best_asset["url"].toString();
|
||||
m_download_size = best_asset["size"].toInt();
|
||||
found_update_info = true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
reportError("platform not found in assets array");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
reportError("data is not an array");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
reportError("JSON is not an object");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
reportError("Failed to download latest release info: %d", status_code);
|
||||
}
|
||||
|
||||
if (found_update_info)
|
||||
checkIfUpdateNeeded();
|
||||
|
||||
emit updateCheckCompleted();
|
||||
}
|
||||
|
||||
void AutoUpdaterDialog::queueGetChanges()
|
||||
{
|
||||
if (!isSupported() || !ensureHttpReady())
|
||||
return;
|
||||
|
||||
m_http->CreateRequest(QStringLiteral(CHANGES_URL).arg(BuildVersion::GitHash).arg(m_latest_version).toStdString(),
|
||||
std::bind(&AutoUpdaterDialog::getChangesComplete, this, std::placeholders::_1, std::placeholders::_3));
|
||||
}
|
||||
|
||||
void AutoUpdaterDialog::getChangesComplete(s32 status_code, std::vector<u8> data)
|
||||
{
|
||||
if (!isSupported())
|
||||
{
|
||||
m_ui.downloadAndInstall->setEnabled(true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (status_code == HTTPDownloader::HTTP_STATUS_OK)
|
||||
{
|
||||
QJsonParseError parse_error;
|
||||
QJsonDocument doc(QJsonDocument::fromJson(QByteArray(reinterpret_cast<const char*>(data.data()), data.size()), &parse_error));
|
||||
if (doc.isObject())
|
||||
{
|
||||
const QJsonObject doc_object(doc.object());
|
||||
|
||||
QString changes_html = tr("<h2>Changes:</h2>");
|
||||
changes_html += QStringLiteral("<ul>");
|
||||
|
||||
const QJsonArray commits(doc_object["commits"].toArray());
|
||||
bool update_will_break_save_states = false;
|
||||
bool update_increases_settings_version = false;
|
||||
|
||||
for (const QJsonValue& commit : commits)
|
||||
{
|
||||
const QJsonObject commit_obj(commit["commit"].toObject());
|
||||
|
||||
QString message = commit_obj["message"].toString();
|
||||
QString author = commit_obj["author"].toObject()["name"].toString();
|
||||
|
||||
if (message.contains(QStringLiteral("[SAVEVERSION+]")))
|
||||
update_will_break_save_states = true;
|
||||
|
||||
if (message.contains(QStringLiteral("[SETTINGSVERSION+]")))
|
||||
update_increases_settings_version = true;
|
||||
|
||||
const int first_line_terminator = message.indexOf('\n');
|
||||
if (first_line_terminator >= 0)
|
||||
message.remove(first_line_terminator, message.size() - first_line_terminator);
|
||||
if (!message.isEmpty())
|
||||
{
|
||||
changes_html +=
|
||||
QStringLiteral("<li>%1 <i>(%2)</i></li>").arg(message.toHtmlEscaped()).arg(author.toHtmlEscaped());
|
||||
}
|
||||
}
|
||||
|
||||
changes_html += "</ul>";
|
||||
|
||||
if (update_will_break_save_states)
|
||||
{
|
||||
changes_html.prepend(tr("<h2>Save State Warning</h2><p>Installing this update will make your save states "
|
||||
"<b>incompatible</b>. Please ensure you have saved your games to a Memory Card "
|
||||
"before installing this update or you will lose progress.</p>"));
|
||||
|
||||
m_update_will_break_save_states = true;
|
||||
}
|
||||
|
||||
if (update_increases_settings_version)
|
||||
{
|
||||
changes_html.prepend(
|
||||
tr("<h2>Settings Warning</h2><p>Installing this update will reset your program configuration. Please note "
|
||||
"that you will have to reconfigure your settings after this update.</p>"));
|
||||
}
|
||||
m_ui.updateNotes->setText(changes_html);
|
||||
}
|
||||
else
|
||||
{
|
||||
reportError("Change list JSON is not an object");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
reportError("Failed to download change list: %d", status_code);
|
||||
}
|
||||
|
||||
m_ui.downloadAndInstall->setEnabled(true);
|
||||
}
|
||||
|
||||
void AutoUpdaterDialog::downloadUpdateClicked()
|
||||
{
|
||||
if (m_update_will_break_save_states)
|
||||
{
|
||||
QMessageBox msgbox;
|
||||
msgbox.setIcon(QMessageBox::Critical);
|
||||
msgbox.setWindowModality(Qt::ApplicationModal);
|
||||
msgbox.setWindowIcon(QtHost::GetAppIcon());
|
||||
msgbox.setWindowTitle(tr("Savestate Warning"));
|
||||
msgbox.setText(tr("<h1>WARNING</h1><p style='font-size:12pt;'>Installing this update will make your <b>save states incompatible</b>, <i>be sure to save any progress to your memory cards before proceeding</i>.</p><p>Do you wish to continue?</p>"));
|
||||
msgbox.addButton(QMessageBox::Yes);
|
||||
msgbox.addButton(QMessageBox::No);
|
||||
msgbox.setDefaultButton(QMessageBox::No);
|
||||
// This makes the box wider, for some reason sizing boxes in Qt is hard - Source: The internet.
|
||||
QSpacerItem* horizontalSpacer = new QSpacerItem(500, 0, QSizePolicy::Minimum, QSizePolicy::Expanding);
|
||||
QGridLayout* layout = (QGridLayout*)msgbox.layout();
|
||||
layout->addItem(horizontalSpacer, layout->rowCount(), 0, 1, layout->columnCount());
|
||||
if (msgbox.exec() != QMessageBox::Yes)
|
||||
return;
|
||||
}
|
||||
|
||||
m_display_messages = true;
|
||||
|
||||
std::optional<bool> download_result;
|
||||
QtModalProgressCallback progress(this);
|
||||
progress.SetTitle(tr("Automatic Updater").toUtf8().constData());
|
||||
progress.SetStatusText(tr("Downloading %1...").arg(m_latest_version).toUtf8().constData());
|
||||
progress.GetDialog().setWindowIcon(windowIcon());
|
||||
progress.SetCancellable(true);
|
||||
|
||||
m_http->CreateRequest(
|
||||
m_download_url.toStdString(),
|
||||
[this, &download_result, &progress](s32 status_code, const std::string&, std::vector<u8> data) {
|
||||
if (status_code == HTTPDownloader::HTTP_STATUS_CANCELLED)
|
||||
return;
|
||||
|
||||
if (status_code != HTTPDownloader::HTTP_STATUS_OK)
|
||||
{
|
||||
reportError("Download failed: %d", status_code);
|
||||
download_result = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.empty())
|
||||
{
|
||||
reportError("Download failed: Update is empty");
|
||||
download_result = false;
|
||||
return;
|
||||
}
|
||||
|
||||
download_result = processUpdate(data, progress.GetDialog());
|
||||
},
|
||||
&progress);
|
||||
|
||||
|
||||
// Since we're going to block, don't allow the timer to poll, otherwise the progress callback can cause the timer to
|
||||
// run, and recursively poll again.
|
||||
m_http_poll_timer->stop();
|
||||
|
||||
// Block until completion.
|
||||
while (m_http->HasAnyRequests())
|
||||
{
|
||||
QApplication::processEvents(QEventLoop::AllEvents, HTTP_POLL_INTERVAL);
|
||||
m_http->PollRequests();
|
||||
}
|
||||
|
||||
if (download_result.value_or(false))
|
||||
{
|
||||
// updater started. since we're a modal on the main window, we have to queue this.
|
||||
QMetaObject::invokeMethod(g_main_window, "requestExit", Qt::QueuedConnection, Q_ARG(bool, true));
|
||||
done(0);
|
||||
}
|
||||
|
||||
// download error or cancelled
|
||||
}
|
||||
|
||||
void AutoUpdaterDialog::checkIfUpdateNeeded()
|
||||
{
|
||||
const QString last_checked_version(
|
||||
QString::fromStdString(Host::GetBaseStringSettingValue("AutoUpdater", "LastVersion")));
|
||||
|
||||
Console.WriteLn(Color_StrongGreen, "Current version: %s", BuildVersion::GitTag);
|
||||
Console.WriteLn(Color_StrongYellow, "Latest version: %s", m_latest_version.toUtf8().constData());
|
||||
Console.WriteLn(Color_StrongOrange, "Last checked version: %s", last_checked_version.toUtf8().constData());
|
||||
if (m_latest_version == BuildVersion::GitTag || m_latest_version == last_checked_version)
|
||||
{
|
||||
Console.WriteLn(Color_StrongGreen, "No update needed.");
|
||||
|
||||
if (m_display_messages)
|
||||
{
|
||||
QMessageBox::information(this, tr("Automatic Updater"),
|
||||
tr("No updates are currently available. Please try again later."));
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
Console.WriteLn(Color_StrongRed, "Update needed.");
|
||||
|
||||
// Don't show the dialog if a game started while the update info was downloading. Some people have
|
||||
// really slow connections, apparently. If we're a manual triggered update check, then display
|
||||
// regardless. This will fall through and signal main to delete us.
|
||||
if (!m_display_messages &&
|
||||
(QtHost::IsVMValid() || (g_emu_thread->isRunningFullscreenUI() && g_emu_thread->isFullscreen())))
|
||||
{
|
||||
Console.WriteLn(Color_StrongRed, "Not showing update dialog due to active VM.");
|
||||
return;
|
||||
}
|
||||
|
||||
m_ui.currentVersion->setText(tr("Current Version: %1 (%2)").arg(getCurrentVersion()).arg(getCurrentVersionDate()));
|
||||
m_ui.newVersion->setText(tr("New Version: %1 (%2)").arg(m_latest_version).arg(m_latest_version_timestamp.toString()));
|
||||
m_ui.downloadSize->setText(tr("Download Size: %1 MB").arg(static_cast<double>(m_download_size) / 1048576.0, 0, 'f', 2));
|
||||
m_ui.updateNotes->setText(tr("Loading..."));
|
||||
queueGetChanges();
|
||||
|
||||
// We have to defer this, because it comes back through the timer/HTTP callback...
|
||||
QMetaObject::invokeMethod(this, "exec", Qt::QueuedConnection);
|
||||
}
|
||||
|
||||
void AutoUpdaterDialog::skipThisUpdateClicked()
|
||||
{
|
||||
Host::SetBaseStringSettingValue("AutoUpdater", "LastVersion", m_latest_version.toUtf8().constData());
|
||||
Host::CommitBaseSettingChanges();
|
||||
done(0);
|
||||
}
|
||||
|
||||
void AutoUpdaterDialog::remindMeLaterClicked()
|
||||
{
|
||||
done(0);
|
||||
}
|
||||
|
||||
#if defined(_WIN32)
|
||||
|
||||
bool AutoUpdaterDialog::doesUpdaterNeedElevation(const std::string& application_dir) const
|
||||
{
|
||||
// Try to create a dummy text file in the PCSX2 updater directory. If it fails, we probably won't have write permission.
|
||||
const std::string dummy_path = Path::Combine(application_dir, "update.txt");
|
||||
auto fp = FileSystem::OpenManagedCFile(dummy_path.c_str(), "wb");
|
||||
if (!fp)
|
||||
return true;
|
||||
|
||||
fp.reset();
|
||||
FileSystem::DeleteFilePath(dummy_path.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
bool AutoUpdaterDialog::processUpdate(const std::vector<u8>& data, QProgressDialog&)
|
||||
{
|
||||
const std::string& application_dir = EmuFolders::AppRoot;
|
||||
const std::string update_zip_path = Path::Combine(EmuFolders::DataRoot, UPDATER_ARCHIVE_NAME);
|
||||
const std::string updater_path = Path::Combine(EmuFolders::DataRoot, UPDATER_EXECUTABLE);
|
||||
|
||||
if ((FileSystem::FileExists(update_zip_path.c_str()) && !FileSystem::DeleteFilePath(update_zip_path.c_str())))
|
||||
{
|
||||
reportError("Removing existing update zip failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!FileSystem::WriteBinaryFile(update_zip_path.c_str(), data.data(), data.size()))
|
||||
{
|
||||
reportError("Writing update zip to '%s' failed", update_zip_path.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string updater_extract_error;
|
||||
if (!ExtractUpdater(update_zip_path.c_str(), updater_path.c_str(), &updater_extract_error))
|
||||
{
|
||||
reportError("Extracting updater failed: %s", updater_extract_error.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
return doUpdate(application_dir, update_zip_path, updater_path);
|
||||
}
|
||||
|
||||
bool AutoUpdaterDialog::doUpdate(const std::string& application_dir, const std::string& zip_path, const std::string& updater_path)
|
||||
{
|
||||
const std::string program_path = QDir::toNativeSeparators(QCoreApplication::applicationFilePath()).toStdString();
|
||||
if (program_path.empty())
|
||||
{
|
||||
reportError("Failed to get current application path");
|
||||
return false;
|
||||
}
|
||||
|
||||
const std::wstring wupdater_path = StringUtil::UTF8StringToWideString(updater_path);
|
||||
const std::wstring wapplication_dir = StringUtil::UTF8StringToWideString(application_dir);
|
||||
const std::wstring arguments = StringUtil::UTF8StringToWideString(fmt::format("{} \"{}\" \"{}\" \"{}\"",
|
||||
QCoreApplication::applicationPid(), application_dir, zip_path, program_path));
|
||||
|
||||
const bool needs_elevation = doesUpdaterNeedElevation(application_dir);
|
||||
|
||||
SHELLEXECUTEINFOW sei = {};
|
||||
sei.cbSize = sizeof(sei);
|
||||
sei.lpVerb = needs_elevation ? L"runas" : nullptr; // needed to trigger elevation
|
||||
sei.lpFile = wupdater_path.c_str();
|
||||
sei.lpParameters = arguments.c_str();
|
||||
sei.lpDirectory = wapplication_dir.c_str();
|
||||
sei.nShow = SW_SHOWNORMAL;
|
||||
if (!ShellExecuteExW(&sei))
|
||||
{
|
||||
reportError("Failed to start %s: %s", needs_elevation ? "elevated updater" : "updater",
|
||||
Error::CreateWin32(GetLastError()).GetDescription().c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void AutoUpdaterDialog::cleanupAfterUpdate()
|
||||
{
|
||||
// If we weren't portable, then updater executable gets left in the application directory.
|
||||
if (EmuFolders::AppRoot == EmuFolders::DataRoot)
|
||||
return;
|
||||
|
||||
const std::string updater_path = Path::Combine(EmuFolders::DataRoot, UPDATER_EXECUTABLE);
|
||||
if (!FileSystem::FileExists(updater_path.c_str()))
|
||||
return;
|
||||
|
||||
if (!FileSystem::DeleteFilePath(updater_path.c_str()))
|
||||
{
|
||||
QMessageBox::critical(nullptr, tr("Updater Error"), tr("Failed to remove updater exe after update."));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
#elif defined(__linux__)
|
||||
|
||||
bool AutoUpdaterDialog::processUpdate(const std::vector<u8>& data, QProgressDialog&)
|
||||
{
|
||||
const char* appimage_path = std::getenv("APPIMAGE");
|
||||
if (!appimage_path || !FileSystem::FileExists(appimage_path))
|
||||
{
|
||||
reportError("Missing APPIMAGE.");
|
||||
return false;
|
||||
}
|
||||
|
||||
const QString qappimage_path(QString::fromUtf8(appimage_path));
|
||||
if (!QFile::exists(qappimage_path))
|
||||
{
|
||||
reportError("Current AppImage does not exist: %s", appimage_path);
|
||||
return false;
|
||||
}
|
||||
|
||||
const QString new_appimage_path(qappimage_path + QStringLiteral(".new"));
|
||||
const QString backup_appimage_path(qappimage_path + QStringLiteral(".backup"));
|
||||
Console.WriteLn("APPIMAGE = %s", appimage_path);
|
||||
Console.WriteLn("Backup AppImage path = %s", backup_appimage_path.toUtf8().constData());
|
||||
Console.WriteLn("New AppImage path = %s", new_appimage_path.toUtf8().constData());
|
||||
|
||||
// Remove old "new" appimage and existing backup appimage.
|
||||
if (QFile::exists(new_appimage_path) && !QFile::remove(new_appimage_path))
|
||||
{
|
||||
reportError("Failed to remove old destination AppImage: %s", new_appimage_path.toUtf8().constData());
|
||||
return false;
|
||||
}
|
||||
if (QFile::exists(backup_appimage_path) && !QFile::remove(backup_appimage_path))
|
||||
{
|
||||
reportError("Failed to remove old backup AppImage: %s", new_appimage_path.toUtf8().constData());
|
||||
return false;
|
||||
}
|
||||
|
||||
// Write "new" appimage.
|
||||
{
|
||||
// We want to copy the permissions from the old appimage to the new one.
|
||||
QFile old_file(qappimage_path);
|
||||
const QFileDevice::Permissions old_permissions = old_file.permissions();
|
||||
QFile new_file(new_appimage_path);
|
||||
if (!new_file.open(QIODevice::WriteOnly) ||
|
||||
new_file.write(reinterpret_cast<const char*>(data.data()), static_cast<qint64>(data.size())) != static_cast<qint64>(data.size()) ||
|
||||
!new_file.setPermissions(old_permissions))
|
||||
{
|
||||
QFile::remove(new_appimage_path);
|
||||
reportError("Failed to write new destination AppImage: %s", new_appimage_path.toUtf8().constData());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Rename "old" appimage.
|
||||
if (!QFile::rename(qappimage_path, backup_appimage_path))
|
||||
{
|
||||
reportError("Failed to rename old AppImage to %s", backup_appimage_path.toUtf8().constData());
|
||||
QFile::remove(new_appimage_path);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Rename "new" appimage.
|
||||
if (!QFile::rename(new_appimage_path, qappimage_path))
|
||||
{
|
||||
reportError("Failed to rename new AppImage to %s", qappimage_path.toUtf8().constData());
|
||||
return false;
|
||||
}
|
||||
|
||||
// Execute new appimage.
|
||||
QProcess* new_process = new QProcess();
|
||||
new_process->setProgram(qappimage_path);
|
||||
new_process->setArguments(QStringList{QStringLiteral("-updatecleanup")});
|
||||
if (!new_process->startDetached())
|
||||
{
|
||||
reportError("Failed to execute new AppImage.");
|
||||
return false;
|
||||
}
|
||||
|
||||
// We exit once we return.
|
||||
return true;
|
||||
}
|
||||
|
||||
void AutoUpdaterDialog::cleanupAfterUpdate()
|
||||
{
|
||||
// Remove old/backup AppImage.
|
||||
const char* appimage_path = std::getenv("APPIMAGE");
|
||||
if (!appimage_path)
|
||||
return;
|
||||
|
||||
const QString qappimage_path(QString::fromUtf8(appimage_path));
|
||||
const QString backup_appimage_path(qappimage_path + QStringLiteral(".backup"));
|
||||
if (!QFile::exists(backup_appimage_path))
|
||||
return;
|
||||
|
||||
Console.WriteLn(Color_StrongOrange, QStringLiteral("Removing backup AppImage %1").arg(backup_appimage_path).toStdString());
|
||||
if (!QFile::remove(backup_appimage_path))
|
||||
Console.Error(QStringLiteral("Failed to remove backup AppImage %1").arg(backup_appimage_path).toStdString());
|
||||
}
|
||||
|
||||
#elif defined(__APPLE__)
|
||||
|
||||
static QString UpdateVersionNumberInName(QString name, QStringView new_version)
|
||||
{
|
||||
QString current_version_string(BuildVersion::GitTag);
|
||||
QStringView current_version = current_version_string;
|
||||
if (!current_version.empty() && !new_version.empty() && current_version[0] == 'v' && new_version[0] == 'v')
|
||||
{
|
||||
current_version = current_version.mid(1);
|
||||
new_version = new_version.mid(1);
|
||||
}
|
||||
if (!current_version.empty() && !new_version.empty())
|
||||
name.replace(current_version.data(), current_version.size(), new_version.data(), new_version.size());
|
||||
return name;
|
||||
}
|
||||
|
||||
bool AutoUpdaterDialog::processUpdate(const std::vector<u8>& data, QProgressDialog& progress)
|
||||
{
|
||||
std::optional<std::string> path = CocoaTools::GetNonTranslocatedBundlePath();
|
||||
if (!path.has_value())
|
||||
{
|
||||
reportError("Couldn't get bundle path");
|
||||
return false;
|
||||
}
|
||||
|
||||
QFileInfo info(QString::fromStdString(*path));
|
||||
if (!info.isBundle())
|
||||
{
|
||||
reportError("Application %s isn't a bundle", path->c_str());
|
||||
return false;
|
||||
}
|
||||
if (info.suffix() != QStringLiteral("app"))
|
||||
{
|
||||
reportError("Unexpected application suffix %s on %s", info.suffix().toUtf8().constData(), path->c_str());
|
||||
return false;
|
||||
}
|
||||
QString open_path;
|
||||
{
|
||||
QTemporaryDir temp_dir(info.path() + QStringLiteral("/PCSX2-UpdateStaging-XXXXXX"));
|
||||
if (!temp_dir.isValid())
|
||||
{
|
||||
reportError("Failed to create update staging directory");
|
||||
return false;
|
||||
}
|
||||
|
||||
constexpr size_t chunk_size = 65536;
|
||||
progress.setLabelText(QStringLiteral("Unpacking update..."));
|
||||
progress.reset();
|
||||
progress.setRange(0, static_cast<int>((data.size() + chunk_size - 1) / chunk_size));
|
||||
|
||||
QProcess untar;
|
||||
untar.setProgram(QStringLiteral("/usr/bin/tar"));
|
||||
untar.setArguments({QStringLiteral("xC"), temp_dir.path()});
|
||||
untar.start();
|
||||
for (size_t i = 0; i < data.size(); i += chunk_size)
|
||||
{
|
||||
progress.setValue(static_cast<int>(i / chunk_size));
|
||||
const size_t amt = std::min(data.size() - i, chunk_size);
|
||||
if (progress.wasCanceled() ||
|
||||
untar.write(reinterpret_cast<const char*>(data.data() + i), static_cast<qsizetype>(amt)) != static_cast<qsizetype>(amt))
|
||||
{
|
||||
if (!progress.wasCanceled())
|
||||
reportError("Failed to unpack update (write stopped short)");
|
||||
untar.closeWriteChannel();
|
||||
if (!untar.waitForFinished(1000))
|
||||
untar.kill();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
untar.closeWriteChannel();
|
||||
while (!untar.waitForFinished(1000))
|
||||
{
|
||||
if (progress.wasCanceled())
|
||||
{
|
||||
untar.kill();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
progress.setValue(progress.maximum());
|
||||
if (untar.exitCode() != EXIT_SUCCESS)
|
||||
{
|
||||
QByteArray msg = untar.readAllStandardError();
|
||||
const char* join = msg.isEmpty() ? "" : ": ";
|
||||
reportError("Failed to unpack update (tar exited with %u%s%s)", untar.exitCode(), join, msg.toStdString().c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
QFileInfoList temp_dir_contents = QDir(temp_dir.path()).entryInfoList(QDir::Filter::Dirs | QDir::Filter::NoDotAndDotDot);
|
||||
auto new_app = std::find_if(temp_dir_contents.begin(), temp_dir_contents.end(), [](const QFileInfo& file) { return file.suffix() == QStringLiteral("app"); });
|
||||
if (new_app == temp_dir_contents.end())
|
||||
{
|
||||
reportError("Couldn't find application in update package");
|
||||
return false;
|
||||
}
|
||||
QString new_name = UpdateVersionNumberInName(info.completeBaseName(), m_latest_version);
|
||||
std::optional<std::string> trashed_path = CocoaTools::MoveToTrash(*path);
|
||||
if (!trashed_path.has_value())
|
||||
{
|
||||
reportError("Failed to trash old application");
|
||||
return false;
|
||||
}
|
||||
open_path = info.path() + QStringLiteral("/") + new_name + QStringLiteral(".app");
|
||||
if (!QFile::rename(new_app->absoluteFilePath(), open_path))
|
||||
{
|
||||
QFile::rename(QString::fromStdString(*trashed_path), info.filePath());
|
||||
reportError("Failed to move new application into place (couldn't rename '%s' to '%s')",
|
||||
new_app->absoluteFilePath().toUtf8().constData(), open_path.toUtf8().constData());
|
||||
return false;
|
||||
}
|
||||
QDir(QString::fromStdString(*trashed_path)).removeRecursively();
|
||||
}
|
||||
// For some reason if I use QProcess the shell gets killed immediately with SIGKILL, but NSTask is fine...
|
||||
if (!CocoaTools::DelayedLaunch(open_path.toStdString()))
|
||||
{
|
||||
reportError("Failed to start new application");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void AutoUpdaterDialog::cleanupAfterUpdate()
|
||||
{
|
||||
}
|
||||
|
||||
#else
|
||||
|
||||
bool AutoUpdaterDialog::processUpdate(const std::vector<u8>& data, QProgressDialog& progress)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
void AutoUpdaterDialog::cleanupAfterUpdate()
|
||||
{
|
||||
}
|
||||
|
||||
#endif
|
||||
78
pcsx2-qt/AutoUpdaterDialog.h
Normal file
78
pcsx2-qt/AutoUpdaterDialog.h
Normal file
@@ -0,0 +1,78 @@
|
||||
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
|
||||
// SPDX-License-Identifier: GPL-3.0+
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "common/Pcsx2Defs.h"
|
||||
|
||||
#include "ui_AutoUpdaterDialog.h"
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
#include <QtCore/QDateTime>
|
||||
#include <QtCore/QStringList>
|
||||
#include <QtCore/QTimer>
|
||||
#include <QtWidgets/QDialog>
|
||||
|
||||
class HTTPDownloader;
|
||||
class QProgressDialog;
|
||||
|
||||
class AutoUpdaterDialog final : public QDialog
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit AutoUpdaterDialog(QWidget* parent = nullptr);
|
||||
~AutoUpdaterDialog();
|
||||
|
||||
static bool isSupported();
|
||||
static QStringList getTagList();
|
||||
static std::string getDefaultTag();
|
||||
static QString getCurrentVersion();
|
||||
static QString getCurrentVersionDate();
|
||||
static void cleanupAfterUpdate();
|
||||
|
||||
Q_SIGNALS:
|
||||
void updateCheckCompleted();
|
||||
|
||||
public Q_SLOTS:
|
||||
void queueUpdateCheck(bool display_message);
|
||||
|
||||
private Q_SLOTS:
|
||||
void httpPollTimerPoll();
|
||||
void downloadUpdateClicked();
|
||||
void skipThisUpdateClicked();
|
||||
void remindMeLaterClicked();
|
||||
|
||||
private:
|
||||
void reportError(const char* msg, ...);
|
||||
|
||||
bool ensureHttpReady();
|
||||
|
||||
void checkIfUpdateNeeded();
|
||||
QString getCurrentUpdateTag() const;
|
||||
|
||||
void getLatestReleaseComplete(s32 status_code, std::vector<u8> data);
|
||||
|
||||
void queueGetChanges();
|
||||
void getChangesComplete(s32 status_code, std::vector<u8> data);
|
||||
|
||||
bool processUpdate(const std::vector<u8>& data, QProgressDialog& progress);
|
||||
#if defined(_WIN32)
|
||||
bool doesUpdaterNeedElevation(const std::string& application_dir) const;
|
||||
bool doUpdate(const std::string& application_dir, const std::string& zip_path, const std::string& updater_path);
|
||||
#endif
|
||||
|
||||
Ui::AutoUpdaterDialog m_ui;
|
||||
|
||||
std::unique_ptr<HTTPDownloader> m_http;
|
||||
QTimer* m_http_poll_timer = nullptr;
|
||||
QString m_latest_version;
|
||||
QDateTime m_latest_version_timestamp;
|
||||
QString m_download_url;
|
||||
int m_download_size = 0;
|
||||
|
||||
bool m_display_messages = false;
|
||||
bool m_update_will_break_save_states = false;
|
||||
};
|
||||
139
pcsx2-qt/AutoUpdaterDialog.ui
Normal file
139
pcsx2-qt/AutoUpdaterDialog.ui
Normal file
@@ -0,0 +1,139 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>AutoUpdaterDialog</class>
|
||||
<widget class="QDialog" name="AutoUpdaterDialog">
|
||||
<property name="windowModality">
|
||||
<enum>Qt::ApplicationModal</enum>
|
||||
</property>
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>651</width>
|
||||
<height>474</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Automatic Updater</string>
|
||||
</property>
|
||||
<property name="modal">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_2" stretch="0,1">
|
||||
<item>
|
||||
<widget class="QLabel" name="label_2">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Minimum" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>24</width>
|
||||
<height>24</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
<property name="pixmap">
|
||||
<pixmap resource="resources/resources.qrc">:/icons/update.png</pixmap>
|
||||
</property>
|
||||
<property name="scaledContents">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="font">
|
||||
<font>
|
||||
<pointsize>16</pointsize>
|
||||
<bold>true</bold>
|
||||
</font>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Update Available</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="currentVersion">
|
||||
<property name="text">
|
||||
<string>Current Version: </string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="newVersion">
|
||||
<property name="text">
|
||||
<string>New Version: </string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="downloadSize">
|
||||
<property name="text">
|
||||
<string>Download Size: </string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QTextBrowser" name="updateNotes"/>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<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="downloadAndInstall">
|
||||
<property name="enabled">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Download and Install...</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="skipThisUpdate">
|
||||
<property name="text">
|
||||
<string>Skip This Update</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="remindMeLater">
|
||||
<property name="text">
|
||||
<string>Remind Me Later</string>
|
||||
</property>
|
||||
<property name="default">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources>
|
||||
<include location="resources/resources.qrc"/>
|
||||
</resources>
|
||||
<connections/>
|
||||
</ui>
|
||||
320
pcsx2-qt/CMakeLists.txt
Normal file
320
pcsx2-qt/CMakeLists.txt
Normal file
@@ -0,0 +1,320 @@
|
||||
include(CopyBaseTranslations)
|
||||
|
||||
set(CMAKE_AUTOMOC ON)
|
||||
set(CMAKE_AUTORCC ON)
|
||||
set(CMAKE_AUTOUIC ON)
|
||||
|
||||
add_executable(pcsx2-qt)
|
||||
|
||||
target_sources(pcsx2-qt PRIVATE
|
||||
AboutDialog.cpp
|
||||
AboutDialog.h
|
||||
AboutDialog.ui
|
||||
AutoUpdaterDialog.cpp
|
||||
AutoUpdaterDialog.h
|
||||
AutoUpdaterDialog.ui
|
||||
ColorPickerButton.cpp
|
||||
ColorPickerButton.h
|
||||
CoverDownloadDialog.cpp
|
||||
CoverDownloadDialog.h
|
||||
CoverDownloadDialog.ui
|
||||
DisplayWidget.cpp
|
||||
DisplayWidget.h
|
||||
EarlyHardwareCheck.cpp
|
||||
LogWindow.cpp
|
||||
LogWindow.h
|
||||
MainWindow.cpp
|
||||
MainWindow.h
|
||||
MainWindow.ui
|
||||
PrecompiledHeader.cpp
|
||||
PrecompiledHeader.h
|
||||
SettingWidgetBinder.h
|
||||
SetupWizardDialog.cpp
|
||||
SetupWizardDialog.h
|
||||
SetupWizardDialog.ui
|
||||
Themes.cpp
|
||||
Translations.cpp
|
||||
QtHost.cpp
|
||||
QtHost.h
|
||||
QtKeyCodes.cpp
|
||||
QtProgressCallback.cpp
|
||||
QtProgressCallback.h
|
||||
QtUtils.cpp
|
||||
QtUtils.h
|
||||
GameList/EmptyGameListWidget.ui
|
||||
GameList/GameListModel.cpp
|
||||
GameList/GameListModel.h
|
||||
GameList/GameListRefreshThread.cpp
|
||||
GameList/GameListRefreshThread.h
|
||||
GameList/GameListWidget.cpp
|
||||
GameList/GameListWidget.h
|
||||
Settings/AchievementLoginDialog.cpp
|
||||
Settings/AchievementLoginDialog.h
|
||||
Settings/AchievementLoginDialog.ui
|
||||
Settings/AchievementSettingsWidget.cpp
|
||||
Settings/AchievementSettingsWidget.h
|
||||
Settings/AchievementSettingsWidget.ui
|
||||
Settings/AdvancedSettingsWidget.cpp
|
||||
Settings/AdvancedSettingsWidget.h
|
||||
Settings/AdvancedSettingsWidget.ui
|
||||
Settings/AudioExpansionSettingsDialog.ui
|
||||
Settings/AudioSettingsWidget.cpp
|
||||
Settings/AudioSettingsWidget.h
|
||||
Settings/AudioSettingsWidget.ui
|
||||
Settings/AudioStretchSettingsDialog.ui
|
||||
Settings/BIOSSettingsWidget.cpp
|
||||
Settings/BIOSSettingsWidget.h
|
||||
Settings/BIOSSettingsWidget.ui
|
||||
Settings/ControllerBindingWidget_DualShock2.ui
|
||||
Settings/ControllerBindingWidget_Guitar.ui
|
||||
Settings/ControllerBindingWidget_Popn.ui
|
||||
Settings/ControllerBindingWidget.cpp
|
||||
Settings/ControllerBindingWidget.h
|
||||
Settings/ControllerBindingWidget.ui
|
||||
Settings/ControllerLEDSettingsDialog.ui
|
||||
Settings/ControllerGlobalSettingsWidget.cpp
|
||||
Settings/ControllerGlobalSettingsWidget.h
|
||||
Settings/ControllerGlobalSettingsWidget.ui
|
||||
Settings/ControllerMacroEditWidget.ui
|
||||
Settings/ControllerMacroWidget.ui
|
||||
Settings/ControllerMappingSettingsDialog.ui
|
||||
Settings/ControllerMouseSettingsDialog.ui
|
||||
Settings/ControllerSettingsWindow.cpp
|
||||
Settings/ControllerSettingsWindow.h
|
||||
Settings/ControllerSettingsWindow.ui
|
||||
Settings/ControllerSettingWidgetBinder.h
|
||||
Settings/DebugAnalysisSettingsWidget.cpp
|
||||
Settings/DebugAnalysisSettingsWidget.h
|
||||
Settings/DebugAnalysisSettingsWidget.ui
|
||||
Settings/DebugUserInterfaceSettingsWidget.cpp
|
||||
Settings/DebugUserInterfaceSettingsWidget.h
|
||||
Settings/DebugUserInterfaceSettingsWidget.ui
|
||||
Settings/DebugSettingsWidget.cpp
|
||||
Settings/DebugSettingsWidget.h
|
||||
Settings/DebugSettingsWidget.ui
|
||||
Settings/EmulationSettingsWidget.cpp
|
||||
Settings/EmulationSettingsWidget.h
|
||||
Settings/EmulationSettingsWidget.ui
|
||||
Settings/FolderSettingsWidget.cpp
|
||||
Settings/FolderSettingsWidget.h
|
||||
Settings/FolderSettingsWidget.ui
|
||||
Settings/GameCheatSettingsWidget.cpp
|
||||
Settings/GameCheatSettingsWidget.h
|
||||
Settings/GameCheatSettingsWidget.ui
|
||||
Settings/GameFixSettingsWidget.cpp
|
||||
Settings/GameFixSettingsWidget.h
|
||||
Settings/GameFixSettingsWidget.ui
|
||||
Settings/GameListSettingsWidget.cpp
|
||||
Settings/GameListSettingsWidget.h
|
||||
Settings/GameListSettingsWidget.ui
|
||||
Settings/GamePatchDetailsWidget.ui
|
||||
Settings/GamePatchSettingsWidget.cpp
|
||||
Settings/GamePatchSettingsWidget.h
|
||||
Settings/GamePatchSettingsWidget.ui
|
||||
Settings/GameSummaryWidget.cpp
|
||||
Settings/GameSummaryWidget.h
|
||||
Settings/GameSummaryWidget.ui
|
||||
Settings/GraphicsSettingsWidget.cpp
|
||||
Settings/GraphicsSettingsWidget.h
|
||||
Settings/GraphicsSettingsWidget.ui
|
||||
Settings/HotkeySettingsWidget.cpp
|
||||
Settings/HotkeySettingsWidget.h
|
||||
Settings/InputBindingDialog.cpp
|
||||
Settings/InputBindingDialog.h
|
||||
Settings/InputBindingDialog.ui
|
||||
Settings/InputBindingWidget.cpp
|
||||
Settings/InputBindingWidget.h
|
||||
Settings/InterfaceSettingsWidget.cpp
|
||||
Settings/InterfaceSettingsWidget.h
|
||||
Settings/InterfaceSettingsWidget.ui
|
||||
Settings/MemoryCardConvertDialog.cpp
|
||||
Settings/MemoryCardConvertDialog.h
|
||||
Settings/MemoryCardConvertDialog.ui
|
||||
Settings/MemoryCardConvertWorker.cpp
|
||||
Settings/MemoryCardConvertWorker.h
|
||||
Settings/MemoryCardCreateDialog.cpp
|
||||
Settings/MemoryCardCreateDialog.h
|
||||
Settings/MemoryCardCreateDialog.ui
|
||||
Settings/MemoryCardSettingsWidget.cpp
|
||||
Settings/MemoryCardSettingsWidget.h
|
||||
Settings/MemoryCardSettingsWidget.ui
|
||||
Settings/DEV9DnsHostDialog.cpp
|
||||
Settings/DEV9DnsHostDialog.h
|
||||
Settings/DEV9DnsHostDialog.ui
|
||||
Settings/DEV9SettingsWidget.cpp
|
||||
Settings/DEV9SettingsWidget.h
|
||||
Settings/DEV9SettingsWidget.ui
|
||||
Settings/DEV9UiCommon.cpp
|
||||
Settings/DEV9UiCommon.h
|
||||
Settings/HddCreateQt.cpp
|
||||
Settings/HddCreateQt.h
|
||||
Settings/PatchDetailsWidget.ui
|
||||
Settings/SettingsWindow.cpp
|
||||
Settings/SettingsWindow.h
|
||||
Settings/SettingsWindow.ui
|
||||
Settings/USBBindingWidget_DenshaCon.ui
|
||||
Settings/USBBindingWidget_DrivingForce.ui
|
||||
Settings/USBBindingWidget_GTForce.ui
|
||||
Settings/USBBindingWidget_GunCon2.ui
|
||||
Settings/USBBindingWidget_RyojouhenCon.ui
|
||||
Settings/USBBindingWidget_ShinkansenCon.ui
|
||||
Debugger/AnalysisOptionsDialog.cpp
|
||||
Debugger/AnalysisOptionsDialog.h
|
||||
Debugger/AnalysisOptionsDialog.ui
|
||||
Debugger/DebuggerSettingsManager.cpp
|
||||
Debugger/DebuggerSettingsManager.h
|
||||
Debugger/DebuggerEvents.h
|
||||
Debugger/DebuggerView.cpp
|
||||
Debugger/DebuggerView.h
|
||||
Debugger/DebuggerWindow.cpp
|
||||
Debugger/DebuggerWindow.h
|
||||
Debugger/DebuggerWindow.ui
|
||||
Debugger/DisassemblyView.cpp
|
||||
Debugger/DisassemblyView.h
|
||||
Debugger/DisassemblyView.ui
|
||||
Debugger/JsonValueWrapper.h
|
||||
Debugger/RegisterView.cpp
|
||||
Debugger/RegisterView.h
|
||||
Debugger/RegisterView.ui
|
||||
Debugger/StackModel.cpp
|
||||
Debugger/StackModel.h
|
||||
Debugger/StackView.cpp
|
||||
Debugger/StackView.h
|
||||
Debugger/ThreadModel.cpp
|
||||
Debugger/ThreadModel.h
|
||||
Debugger/ThreadView.cpp
|
||||
Debugger/ThreadView.h
|
||||
Debugger/Breakpoints/BreakpointDialog.cpp
|
||||
Debugger/Breakpoints/BreakpointDialog.h
|
||||
Debugger/Breakpoints/BreakpointDialog.ui
|
||||
Debugger/Breakpoints/BreakpointModel.cpp
|
||||
Debugger/Breakpoints/BreakpointModel.h
|
||||
Debugger/Breakpoints/BreakpointView.cpp
|
||||
Debugger/Breakpoints/BreakpointView.h
|
||||
Debugger/Breakpoints/BreakpointView.ui
|
||||
Debugger/Docking/DockLayout.cpp
|
||||
Debugger/Docking/DockLayout.h
|
||||
Debugger/Docking/DockManager.cpp
|
||||
Debugger/Docking/DockManager.h
|
||||
Debugger/Docking/DockMenuBar.cpp
|
||||
Debugger/Docking/DockMenuBar.h
|
||||
Debugger/Docking/DockTables.cpp
|
||||
Debugger/Docking/DockTables.h
|
||||
Debugger/Docking/DockUtils.cpp
|
||||
Debugger/Docking/DockUtils.h
|
||||
Debugger/Docking/DockViews.cpp
|
||||
Debugger/Docking/DockViews.h
|
||||
Debugger/Docking/DropIndicators.cpp
|
||||
Debugger/Docking/DropIndicators.h
|
||||
Debugger/Docking/LayoutEditorDialog.cpp
|
||||
Debugger/Docking/LayoutEditorDialog.h
|
||||
Debugger/Docking/LayoutEditorDialog.ui
|
||||
Debugger/Docking/NoLayoutsWidget.cpp
|
||||
Debugger/Docking/NoLayoutsWidget.h
|
||||
Debugger/Docking/NoLayoutsWidget.ui
|
||||
Debugger/Memory/MemorySearchView.cpp
|
||||
Debugger/Memory/MemorySearchView.h
|
||||
Debugger/Memory/MemorySearchView.ui
|
||||
Debugger/Memory/MemoryView.cpp
|
||||
Debugger/Memory/MemoryView.h
|
||||
Debugger/Memory/MemoryView.ui
|
||||
Debugger/Memory/SavedAddressesModel.cpp
|
||||
Debugger/Memory/SavedAddressesModel.h
|
||||
Debugger/Memory/SavedAddressesView.cpp
|
||||
Debugger/Memory/SavedAddressesView.h
|
||||
Debugger/Memory/SavedAddressesView.ui
|
||||
Debugger/SymbolTree/NewSymbolDialogs.cpp
|
||||
Debugger/SymbolTree/NewSymbolDialogs.h
|
||||
Debugger/SymbolTree/NewSymbolDialog.ui
|
||||
Debugger/SymbolTree/SymbolTreeLocation.cpp
|
||||
Debugger/SymbolTree/SymbolTreeLocation.h
|
||||
Debugger/SymbolTree/SymbolTreeModel.cpp
|
||||
Debugger/SymbolTree/SymbolTreeModel.h
|
||||
Debugger/SymbolTree/SymbolTreeNode.cpp
|
||||
Debugger/SymbolTree/SymbolTreeNode.h
|
||||
Debugger/SymbolTree/SymbolTreeDelegates.cpp
|
||||
Debugger/SymbolTree/SymbolTreeDelegates.h
|
||||
Debugger/SymbolTree/SymbolTreeViews.cpp
|
||||
Debugger/SymbolTree/SymbolTreeViews.h
|
||||
Debugger/SymbolTree/SymbolTreeView.ui
|
||||
Debugger/SymbolTree/TypeString.cpp
|
||||
Debugger/SymbolTree/TypeString.h
|
||||
Tools/InputRecording/NewInputRecordingDlg.cpp
|
||||
Tools/InputRecording/NewInputRecordingDlg.h
|
||||
Tools/InputRecording/NewInputRecordingDlg.ui
|
||||
Tools/InputRecording/InputRecordingViewer.cpp
|
||||
Tools/InputRecording/InputRecordingViewer.h
|
||||
Tools/InputRecording/InputRecordingViewer.ui
|
||||
resources/resources.qrc
|
||||
)
|
||||
|
||||
file(GLOB TS_FILES ${CMAKE_CURRENT_SOURCE_DIR}/Translations/*.ts)
|
||||
|
||||
target_precompile_headers(pcsx2-qt PRIVATE PrecompiledHeader.h)
|
||||
set_source_files_properties(PrecompiledHeader.cpp PROPERTIES HEADER_FILE_ONLY TRUE)
|
||||
|
||||
target_include_directories(pcsx2-qt PRIVATE
|
||||
${Qt6Gui_PRIVATE_INCLUDE_DIRS}
|
||||
"${CMAKE_BINARY_DIR}/common/include"
|
||||
"${CMAKE_SOURCE_DIR}/pcsx2"
|
||||
"${CMAKE_SOURCE_DIR}/pcsx2-qt"
|
||||
)
|
||||
|
||||
target_link_libraries(pcsx2-qt PRIVATE
|
||||
PCSX2_FLAGS
|
||||
PCSX2
|
||||
Qt6::Core
|
||||
Qt6::Gui
|
||||
Qt6::Widgets
|
||||
KDAB::kddockwidgets
|
||||
)
|
||||
|
||||
# Our Qt builds may have exceptions on, so force them off.
|
||||
target_compile_definitions(pcsx2-qt PRIVATE QT_NO_EXCEPTIONS)
|
||||
|
||||
if(WIN32)
|
||||
target_sources(pcsx2-qt PRIVATE VCRuntimeChecker.cpp)
|
||||
set_source_files_properties(${TS_FILES} PROPERTIES OUTPUT_LOCATION "${CMAKE_SOURCE_DIR}/bin/translations")
|
||||
qt_add_lrelease(pcsx2-qt TS_FILES ${TS_FILES})
|
||||
copy_base_translations(pcsx2-qt)
|
||||
elseif(APPLE)
|
||||
qt_add_lrelease(pcsx2-qt TS_FILES ${TS_FILES} QM_FILES_OUTPUT_VARIABLE QM_FILES)
|
||||
set(PCSX2_MACOS_LOCALIZATIONS)
|
||||
foreach (TS_FILE IN LISTS TS_FILES)
|
||||
get_filename_component(TS_FILE_NAME ${TS_FILE} NAME)
|
||||
set(regex "^pcsx2-qt_([a-zA-Z0-9\\-]+)\\.ts$")
|
||||
if (TS_FILE_NAME MATCHES ${regex})
|
||||
string(REGEX REPLACE ${regex} "\\1" language ${TS_FILE_NAME})
|
||||
string(REPLACE "-" "_" language ${language})
|
||||
set(PCSX2_MACOS_LOCALIZATIONS "${PCSX2_MACOS_LOCALIZATIONS}\n\t\t<string>${language}</string>")
|
||||
else()
|
||||
message(WARNING "Unrecognized ts file ${TS_FILE_NAME}")
|
||||
endif()
|
||||
endforeach()
|
||||
foreach (QM_FILE IN LISTS QM_FILES)
|
||||
target_sources(pcsx2-qt PRIVATE ${QM_FILE})
|
||||
set_source_files_properties(${QM_FILE} PROPERTIES MACOSX_PACKAGE_LOCATION Resources/translations/)
|
||||
endforeach()
|
||||
copy_base_translations(pcsx2-qt)
|
||||
extract_translation_from_ts(Translations/pcsx2-qt_en.ts source PermissionsDialogMicrophone PCSX2_MICROPHONE_USAGE_DESCRIPTION)
|
||||
else()
|
||||
qt_add_lrelease(pcsx2-qt TS_FILES ${TS_FILES} QM_FILES_OUTPUT_VARIABLE QM_FILES)
|
||||
set(QM_OUTPUT_DIR "$<TARGET_FILE_DIR:pcsx2-qt>/translations")
|
||||
add_custom_command(TARGET pcsx2-qt POST_BUILD COMMAND "${CMAKE_COMMAND}" -E make_directory "${QM_OUTPUT_DIR}")
|
||||
foreach (QM_FILE IN LISTS QM_FILES)
|
||||
get_filename_component(QM_FILE_NAME ${QM_FILE} NAME)
|
||||
add_custom_command(TARGET pcsx2-qt POST_BUILD COMMAND "${CMAKE_COMMAND}" -E copy_if_different "${QM_FILE}" "${QM_OUTPUT_DIR}/${QM_FILE_NAME}")
|
||||
endforeach()
|
||||
copy_base_translations(pcsx2-qt)
|
||||
endif()
|
||||
|
||||
|
||||
# Currently, 7z is only needed for the Windows updater.
|
||||
if(WIN32)
|
||||
target_link_libraries(pcsx2-qt PRIVATE
|
||||
LZMA::LZMA
|
||||
)
|
||||
endif()
|
||||
|
||||
fixup_file_properties(pcsx2-qt)
|
||||
setup_main_executable(pcsx2-qt)
|
||||
53
pcsx2-qt/ColorPickerButton.cpp
Normal file
53
pcsx2-qt/ColorPickerButton.cpp
Normal file
@@ -0,0 +1,53 @@
|
||||
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
|
||||
// SPDX-License-Identifier: GPL-3.0+
|
||||
|
||||
#include "ColorPickerButton.h"
|
||||
#include "QtUtils.h"
|
||||
|
||||
#include <QtWidgets/QColorDialog>
|
||||
|
||||
ColorPickerButton::ColorPickerButton(QWidget* parent)
|
||||
: QPushButton(parent)
|
||||
{
|
||||
connect(this, &QPushButton::clicked, this, &ColorPickerButton::onClicked);
|
||||
updateBackgroundColor();
|
||||
}
|
||||
|
||||
u32 ColorPickerButton::color()
|
||||
{
|
||||
return m_color;
|
||||
}
|
||||
|
||||
void ColorPickerButton::setColor(u32 rgb)
|
||||
{
|
||||
if (m_color == rgb)
|
||||
return;
|
||||
|
||||
m_color = rgb;
|
||||
updateBackgroundColor();
|
||||
}
|
||||
|
||||
void ColorPickerButton::updateBackgroundColor()
|
||||
{
|
||||
setStyleSheet(QStringLiteral("background-color: #%1;").arg(static_cast<uint>(m_color), 6, 16, QChar('0')));
|
||||
}
|
||||
|
||||
void ColorPickerButton::onClicked()
|
||||
{
|
||||
const u32 red = (m_color >> 16) & 0xff;
|
||||
const u32 green = (m_color >> 8) & 0xff;
|
||||
const u32 blue = m_color & 0xff;
|
||||
|
||||
const QColor initial(QColor::fromRgb(red, green, blue));
|
||||
const QColor selected(QColorDialog::getColor(initial, QtUtils::GetRootWidget(this), tr("Select LED Color")));
|
||||
|
||||
// QColorDialog returns Invalid on cancel, and apparently initial == Invalid is true...
|
||||
if (!selected.isValid() || initial == selected)
|
||||
return;
|
||||
|
||||
const u32 new_rgb =
|
||||
(static_cast<u32>(selected.red()) << 16) | (static_cast<u32>(selected.green()) << 8) | static_cast<u32>(selected.blue());
|
||||
m_color = new_rgb;
|
||||
updateBackgroundColor();
|
||||
emit colorChanged(new_rgb);
|
||||
}
|
||||
30
pcsx2-qt/ColorPickerButton.h
Normal file
30
pcsx2-qt/ColorPickerButton.h
Normal file
@@ -0,0 +1,30 @@
|
||||
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
|
||||
// SPDX-License-Identifier: GPL-3.0+
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "common/Pcsx2Defs.h"
|
||||
#include <QtWidgets/QPushButton>
|
||||
|
||||
class ColorPickerButton : public QPushButton
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
ColorPickerButton(QWidget* parent);
|
||||
|
||||
Q_SIGNALS:
|
||||
void colorChanged(quint32 new_color);
|
||||
|
||||
public Q_SLOTS:
|
||||
quint32 color();
|
||||
void setColor(quint32 rgb);
|
||||
|
||||
private Q_SLOTS:
|
||||
void onClicked();
|
||||
|
||||
private:
|
||||
void updateBackgroundColor();
|
||||
|
||||
u32 m_color = 0;
|
||||
};
|
||||
126
pcsx2-qt/CoverDownloadDialog.cpp
Normal file
126
pcsx2-qt/CoverDownloadDialog.cpp
Normal file
@@ -0,0 +1,126 @@
|
||||
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
|
||||
// SPDX-License-Identifier: GPL-3.0+
|
||||
|
||||
#include "CoverDownloadDialog.h"
|
||||
#include "QtUtils.h"
|
||||
|
||||
#include "pcsx2/GameList.h"
|
||||
|
||||
#include "common/Assertions.h"
|
||||
|
||||
CoverDownloadDialog::CoverDownloadDialog(QWidget* parent /*= nullptr*/)
|
||||
: QDialog(parent)
|
||||
{
|
||||
m_ui.setupUi(this);
|
||||
QtUtils::SetScalableIcon(m_ui.coverIcon, QIcon::fromTheme(QStringLiteral("artboard-2-line")), QSize(32, 32));
|
||||
updateEnabled();
|
||||
|
||||
connect(m_ui.start, &QPushButton::clicked, this, &CoverDownloadDialog::onStartClicked);
|
||||
connect(m_ui.close, &QPushButton::clicked, this, &CoverDownloadDialog::onCloseClicked);
|
||||
connect(m_ui.urls, &QTextEdit::textChanged, this, &CoverDownloadDialog::updateEnabled);
|
||||
}
|
||||
|
||||
CoverDownloadDialog::~CoverDownloadDialog()
|
||||
{
|
||||
pxAssert(!m_thread);
|
||||
}
|
||||
|
||||
void CoverDownloadDialog::closeEvent(QCloseEvent* ev)
|
||||
{
|
||||
cancelThread();
|
||||
}
|
||||
|
||||
void CoverDownloadDialog::onDownloadStatus(const QString& text)
|
||||
{
|
||||
m_ui.status->setText(text);
|
||||
}
|
||||
|
||||
void CoverDownloadDialog::onDownloadProgress(int value, int range)
|
||||
{
|
||||
// Limit to once every five seconds, otherwise it's way too flickery.
|
||||
// Ideally in the future we'd have some way to invalidate only a single cover.
|
||||
if (m_last_refresh_time.GetTimeSeconds() >= 5.0f)
|
||||
{
|
||||
emit coverRefreshRequested();
|
||||
m_last_refresh_time.Reset();
|
||||
}
|
||||
|
||||
if (range != m_ui.progress->maximum())
|
||||
m_ui.progress->setMaximum(range);
|
||||
m_ui.progress->setValue(value);
|
||||
}
|
||||
|
||||
void CoverDownloadDialog::onDownloadComplete()
|
||||
{
|
||||
emit coverRefreshRequested();
|
||||
|
||||
if (m_thread)
|
||||
{
|
||||
m_thread->join();
|
||||
m_thread.reset();
|
||||
}
|
||||
|
||||
updateEnabled();
|
||||
|
||||
m_ui.status->setText(tr("Download complete."));
|
||||
}
|
||||
|
||||
void CoverDownloadDialog::onStartClicked()
|
||||
{
|
||||
if (m_thread)
|
||||
cancelThread();
|
||||
else
|
||||
startThread();
|
||||
}
|
||||
|
||||
void CoverDownloadDialog::onCloseClicked()
|
||||
{
|
||||
if (m_thread)
|
||||
cancelThread();
|
||||
|
||||
done(0);
|
||||
}
|
||||
|
||||
void CoverDownloadDialog::updateEnabled()
|
||||
{
|
||||
const bool running = static_cast<bool>(m_thread);
|
||||
m_ui.start->setText(running ? tr("Stop") : tr("Start"));
|
||||
m_ui.start->setEnabled(running || !m_ui.urls->toPlainText().isEmpty());
|
||||
m_ui.close->setEnabled(!running);
|
||||
m_ui.urls->setEnabled(!running);
|
||||
}
|
||||
|
||||
void CoverDownloadDialog::startThread()
|
||||
{
|
||||
m_thread = std::make_unique<CoverDownloadThread>(this, m_ui.urls->toPlainText(), !m_ui.useTitleFileNames->isChecked());
|
||||
m_last_refresh_time.Reset();
|
||||
connect(m_thread.get(), &CoverDownloadThread::statusUpdated, this, &CoverDownloadDialog::onDownloadStatus);
|
||||
connect(m_thread.get(), &CoverDownloadThread::progressUpdated, this, &CoverDownloadDialog::onDownloadProgress);
|
||||
connect(m_thread.get(), &CoverDownloadThread::threadFinished, this, &CoverDownloadDialog::onDownloadComplete);
|
||||
m_thread->start();
|
||||
updateEnabled();
|
||||
}
|
||||
|
||||
void CoverDownloadDialog::cancelThread()
|
||||
{
|
||||
if (!m_thread)
|
||||
return;
|
||||
|
||||
m_thread->requestInterruption();
|
||||
m_thread->join();
|
||||
m_thread.reset();
|
||||
}
|
||||
|
||||
CoverDownloadDialog::CoverDownloadThread::CoverDownloadThread(QWidget* parent, const QString& urls, bool use_serials)
|
||||
: QtAsyncProgressThread(parent), m_use_serials(use_serials)
|
||||
{
|
||||
for (const QString& str : urls.split(QChar('\n')))
|
||||
m_urls.push_back(str.toStdString());
|
||||
}
|
||||
|
||||
CoverDownloadDialog::CoverDownloadThread::~CoverDownloadThread() = default;
|
||||
|
||||
void CoverDownloadDialog::CoverDownloadThread::runAsync()
|
||||
{
|
||||
GameList::DownloadCovers(m_urls, m_use_serials, this);
|
||||
}
|
||||
57
pcsx2-qt/CoverDownloadDialog.h
Normal file
57
pcsx2-qt/CoverDownloadDialog.h
Normal file
@@ -0,0 +1,57 @@
|
||||
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
|
||||
// SPDX-License-Identifier: GPL-3.0+
|
||||
|
||||
#pragma once
|
||||
#include "common/Timer.h"
|
||||
#include "common/Pcsx2Defs.h"
|
||||
#include "QtProgressCallback.h"
|
||||
#include "ui_CoverDownloadDialog.h"
|
||||
#include <QtWidgets/QDialog>
|
||||
#include <array>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
class CoverDownloadDialog final : public QDialog
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
CoverDownloadDialog(QWidget* parent = nullptr);
|
||||
~CoverDownloadDialog();
|
||||
|
||||
Q_SIGNALS:
|
||||
void coverRefreshRequested();
|
||||
|
||||
protected:
|
||||
void closeEvent(QCloseEvent* ev);
|
||||
|
||||
private Q_SLOTS:
|
||||
void onDownloadStatus(const QString& text);
|
||||
void onDownloadProgress(int value, int range);
|
||||
void onDownloadComplete();
|
||||
void onStartClicked();
|
||||
void onCloseClicked();
|
||||
void updateEnabled();
|
||||
|
||||
private:
|
||||
class CoverDownloadThread : public QtAsyncProgressThread
|
||||
{
|
||||
public:
|
||||
CoverDownloadThread(QWidget* parent, const QString& urls, bool use_serials);
|
||||
~CoverDownloadThread();
|
||||
|
||||
protected:
|
||||
void runAsync() override;
|
||||
|
||||
private:
|
||||
std::vector<std::string> m_urls;
|
||||
bool m_use_serials;
|
||||
};
|
||||
|
||||
void startThread();
|
||||
void cancelThread();
|
||||
|
||||
Ui::CoverDownloadDialog m_ui;
|
||||
std::unique_ptr<CoverDownloadThread> m_thread;
|
||||
Common::Timer m_last_refresh_time;
|
||||
};
|
||||
117
pcsx2-qt/CoverDownloadDialog.ui
Normal file
117
pcsx2-qt/CoverDownloadDialog.ui
Normal file
@@ -0,0 +1,117 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>CoverDownloadDialog</class>
|
||||
<widget class="QDialog" name="CoverDownloadDialog">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>720</width>
|
||||
<height>380</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Download Covers</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_2" stretch="0,1">
|
||||
<property name="spacing">
|
||||
<number>10</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QLabel" name="coverIcon">
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
<property name="pixmap">
|
||||
<pixmap resource="resources/resources.qrc">:/icons/black/svg/artboard-2-line.svg</pixmap>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="text">
|
||||
<string>PCSX2 can automatically download covers for games which do not currently have a cover set. We do not host any cover images, the user must provide their own source for images.</string>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="label_3">
|
||||
<property name="text">
|
||||
<string><html><head/><body><p>In the box below, specify the URLs to download covers from, with one template URL per line. The following variables are available:</p><p><span style=" font-style:italic;">${title}:</span> Title of the game.<br/><span style=" font-style:italic;">${filetitle}:</span> Name component of the game's filename.<br/><span style=" font-style:italic;">${serial}:</span> Serial of the game.</p><p><span style=" font-weight:700;">Example:</span> https://www.example-not-a-real-domain.com/covers/${serial}.jpg</p></body></html></string>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QTextEdit" name="urls"/>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="label_2">
|
||||
<property name="text">
|
||||
<string>By default, the downloaded covers will be saved with the game's serial to ensure covers do not break with GameDB changes and that titles with multiple regions do not conflict. If this is not desired, you can check the "Use Title File Names" box below.</string>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="useTitleFileNames">
|
||||
<property name="text">
|
||||
<string>Use Title File Names</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="status">
|
||||
<property name="text">
|
||||
<string>Waiting to start...</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<item>
|
||||
<widget class="QProgressBar" name="progress"/>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="start">
|
||||
<property name="enabled">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Start</string>
|
||||
</property>
|
||||
<property name="default">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="close">
|
||||
<property name="text">
|
||||
<string>Close</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources>
|
||||
<include location="resources/resources.qrc"/>
|
||||
</resources>
|
||||
<connections/>
|
||||
</ui>
|
||||
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>
|
||||
480
pcsx2-qt/DisplayWidget.cpp
Normal file
480
pcsx2-qt/DisplayWidget.cpp
Normal file
@@ -0,0 +1,480 @@
|
||||
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
|
||||
// SPDX-License-Identifier: GPL-3.0+
|
||||
|
||||
#include "DisplayWidget.h"
|
||||
#include "MainWindow.h"
|
||||
#include "QtHost.h"
|
||||
#include "QtUtils.h"
|
||||
|
||||
#include "pcsx2/ImGui/FullscreenUI.h"
|
||||
#include "pcsx2/ImGui/ImGuiManager.h"
|
||||
|
||||
#include "common/Assertions.h"
|
||||
#include "common/Console.h"
|
||||
|
||||
#include <QtCore/QDebug>
|
||||
#include <QtGui/QGuiApplication>
|
||||
#include <QtGui/QKeyEvent>
|
||||
#include <QtGui/QResizeEvent>
|
||||
#include <QtGui/QScreen>
|
||||
#include <QtGui/QWindow>
|
||||
#include <QtGui/QWindowStateChangeEvent>
|
||||
|
||||
#include <bit>
|
||||
#include <cmath>
|
||||
|
||||
#if defined(_WIN32)
|
||||
#include "common/RedtapeWindows.h"
|
||||
#elif !defined(APPLE)
|
||||
#include <qpa/qplatformnativeinterface.h>
|
||||
#endif
|
||||
|
||||
DisplayWidget::DisplayWidget(QWidget* parent)
|
||||
: QWidget(parent)
|
||||
{
|
||||
// We want a native window for both D3D and OpenGL.
|
||||
setAutoFillBackground(false);
|
||||
setAttribute(Qt::WA_NativeWindow, true);
|
||||
setAttribute(Qt::WA_NoSystemBackground, true);
|
||||
setAttribute(Qt::WA_PaintOnScreen, true);
|
||||
setAttribute(Qt::WA_KeyCompression, false);
|
||||
setFocusPolicy(Qt::StrongFocus);
|
||||
setMouseTracking(true);
|
||||
}
|
||||
|
||||
DisplayWidget::~DisplayWidget()
|
||||
{
|
||||
#ifdef _WIN32
|
||||
if (m_clip_mouse_enabled)
|
||||
ClipCursor(nullptr);
|
||||
#endif
|
||||
}
|
||||
|
||||
int DisplayWidget::scaledWindowWidth() const
|
||||
{
|
||||
return std::max(static_cast<int>(std::ceil(static_cast<qreal>(width()) * devicePixelRatioF())), 1);
|
||||
}
|
||||
|
||||
int DisplayWidget::scaledWindowHeight() const
|
||||
{
|
||||
return std::max(static_cast<int>(std::ceil(static_cast<qreal>(height()) * devicePixelRatioF())), 1);
|
||||
}
|
||||
|
||||
std::optional<WindowInfo> DisplayWidget::getWindowInfo()
|
||||
{
|
||||
std::optional<WindowInfo> ret(QtUtils::GetWindowInfoForWidget(this));
|
||||
if (ret.has_value())
|
||||
{
|
||||
m_last_window_width = ret->surface_width;
|
||||
m_last_window_height = ret->surface_height;
|
||||
m_last_window_scale = ret->surface_scale;
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
void DisplayWidget::updateRelativeMode(bool enabled)
|
||||
{
|
||||
#ifdef _WIN32
|
||||
// prefer ClipCursor() over warping movement when we're using raw input
|
||||
bool clip_cursor = enabled && false /*InputManager::IsUsingRawInput()*/;
|
||||
if (m_relative_mouse_enabled == enabled && m_clip_mouse_enabled == clip_cursor)
|
||||
return;
|
||||
|
||||
DevCon.WriteLn("updateRelativeMode(): relative=%s, clip=%s", enabled ? "yes" : "no", clip_cursor ? "yes" : "no");
|
||||
|
||||
if (!clip_cursor && m_clip_mouse_enabled)
|
||||
{
|
||||
m_clip_mouse_enabled = false;
|
||||
ClipCursor(nullptr);
|
||||
}
|
||||
#else
|
||||
if (m_relative_mouse_enabled == enabled)
|
||||
return;
|
||||
|
||||
DevCon.WriteLn("updateRelativeMode(): relative=%s", enabled ? "yes" : "no");
|
||||
#endif
|
||||
|
||||
if (enabled)
|
||||
{
|
||||
#ifdef _WIN32
|
||||
m_relative_mouse_enabled = !clip_cursor;
|
||||
m_clip_mouse_enabled = clip_cursor;
|
||||
#else
|
||||
m_relative_mouse_enabled = true;
|
||||
#endif
|
||||
m_relative_mouse_start_pos = QCursor::pos();
|
||||
updateCenterPos();
|
||||
grabMouse();
|
||||
}
|
||||
else if (m_relative_mouse_enabled)
|
||||
{
|
||||
m_relative_mouse_enabled = false;
|
||||
QCursor::setPos(m_relative_mouse_start_pos);
|
||||
releaseMouse();
|
||||
}
|
||||
}
|
||||
|
||||
void DisplayWidget::updateCursor(bool hidden)
|
||||
{
|
||||
if (m_cursor_hidden == hidden)
|
||||
return;
|
||||
|
||||
m_cursor_hidden = hidden;
|
||||
if (hidden)
|
||||
{
|
||||
DevCon.WriteLn("updateCursor(): Cursor is now hidden");
|
||||
setCursor(Qt::BlankCursor);
|
||||
}
|
||||
else
|
||||
{
|
||||
DevCon.WriteLn("updateCursor(): Cursor is now shown");
|
||||
unsetCursor();
|
||||
}
|
||||
}
|
||||
|
||||
void DisplayWidget::handleCloseEvent(QCloseEvent* event)
|
||||
{
|
||||
// Closing the separate widget will either cancel the close, or trigger shutdown.
|
||||
// In the latter case, it's going to destroy us, so don't let Qt do it first.
|
||||
// Treat a close event while fullscreen as an exit, that way ALT+F4 closes PCSX2,
|
||||
// rather than just the game.
|
||||
if (QtHost::IsVMValid() && !isActuallyFullscreen())
|
||||
{
|
||||
QMetaObject::invokeMethod(g_main_window, "requestShutdown", Q_ARG(bool, true),
|
||||
Q_ARG(bool, true), Q_ARG(bool, false));
|
||||
}
|
||||
else
|
||||
{
|
||||
QMetaObject::invokeMethod(g_main_window, "requestExit", Q_ARG(bool, true));
|
||||
}
|
||||
|
||||
// Cancel the event from closing the window.
|
||||
event->ignore();
|
||||
}
|
||||
|
||||
void DisplayWidget::destroy()
|
||||
{
|
||||
m_destroying = true;
|
||||
|
||||
#ifdef __APPLE__
|
||||
// See Qt documentation, entire application is in full screen state, and the main
|
||||
// window will get reopened fullscreen instead of windowed if we don't close the
|
||||
// fullscreen window first.
|
||||
if (isActuallyFullscreen())
|
||||
close();
|
||||
#endif
|
||||
deleteLater();
|
||||
}
|
||||
|
||||
bool DisplayWidget::isActuallyFullscreen() const
|
||||
{
|
||||
// I hate you QtWayland... have to check the parent, not ourselves.
|
||||
QWidget* container = qobject_cast<QWidget*>(parent());
|
||||
return container ? container->isFullScreen() : isFullScreen();
|
||||
}
|
||||
|
||||
void DisplayWidget::updateCenterPos()
|
||||
{
|
||||
#ifdef _WIN32
|
||||
if (m_clip_mouse_enabled)
|
||||
{
|
||||
RECT rc;
|
||||
if (GetWindowRect(reinterpret_cast<HWND>(winId()), &rc))
|
||||
ClipCursor(&rc);
|
||||
}
|
||||
else if (m_relative_mouse_enabled)
|
||||
{
|
||||
RECT rc;
|
||||
if (GetWindowRect(reinterpret_cast<HWND>(winId()), &rc))
|
||||
{
|
||||
m_relative_mouse_center_pos.setX(((rc.right - rc.left) / 2) + rc.left);
|
||||
m_relative_mouse_center_pos.setY(((rc.bottom - rc.top) / 2) + rc.top);
|
||||
SetCursorPos(m_relative_mouse_center_pos.x(), m_relative_mouse_center_pos.y());
|
||||
}
|
||||
}
|
||||
#else
|
||||
if (m_relative_mouse_enabled)
|
||||
{
|
||||
// we do a round trip here because these coordinates are dpi-unscaled
|
||||
m_relative_mouse_center_pos = mapToGlobal(QPoint((width() + 1) / 2, (height() + 1) / 2));
|
||||
QCursor::setPos(m_relative_mouse_center_pos);
|
||||
m_relative_mouse_center_pos = QCursor::pos();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
QPaintEngine* DisplayWidget::paintEngine() const
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool DisplayWidget::event(QEvent* event)
|
||||
{
|
||||
switch (event->type())
|
||||
{
|
||||
case QEvent::KeyPress:
|
||||
case QEvent::KeyRelease:
|
||||
{
|
||||
const QKeyEvent* key_event = static_cast<QKeyEvent*>(event);
|
||||
|
||||
// Forward text input to imgui.
|
||||
if (ImGuiManager::WantsTextInput() && key_event->type() == QEvent::KeyPress)
|
||||
{
|
||||
// Don't forward backspace characters. We send the backspace as a normal key event,
|
||||
// so if we send the character too, it double-deletes.
|
||||
QString text(key_event->text());
|
||||
text.remove(QChar('\b'));
|
||||
if (!text.isEmpty())
|
||||
ImGuiManager::AddTextInput(text.toStdString());
|
||||
}
|
||||
|
||||
if (key_event->isAutoRepeat())
|
||||
return true;
|
||||
|
||||
// For some reason, Windows sends "fake" key events.
|
||||
// Scenario: Press shift, press F1, release shift, release F1.
|
||||
// Events: Shift=Pressed, F1=Pressed, Shift=Released, **F1=Pressed**, F1=Released.
|
||||
// To work around this, we keep track of keys pressed with modifiers in a list, and
|
||||
// discard the press event when it's been previously activated. It's pretty gross,
|
||||
// but I can't think of a better way of handling it, and there doesn't appear to be
|
||||
// any window flag which changes this behavior that I can see.
|
||||
|
||||
const u32 key = QtUtils::KeyEventToCode(key_event);
|
||||
const Qt::KeyboardModifiers modifiers = key_event->modifiers();
|
||||
const bool pressed = (key_event->type() == QEvent::KeyPress);
|
||||
const auto it = std::find(m_keys_pressed_with_modifiers.begin(), m_keys_pressed_with_modifiers.end(), key);
|
||||
if (it != m_keys_pressed_with_modifiers.end())
|
||||
{
|
||||
if (pressed)
|
||||
return true;
|
||||
else
|
||||
m_keys_pressed_with_modifiers.erase(it);
|
||||
}
|
||||
else if (modifiers != Qt::NoModifier && modifiers != Qt::KeypadModifier && pressed)
|
||||
{
|
||||
m_keys_pressed_with_modifiers.push_back(key);
|
||||
}
|
||||
|
||||
Host::RunOnCPUThread([key, pressed]() {
|
||||
InputManager::InvokeEvents(InputManager::MakeHostKeyboardKey(key), static_cast<float>(pressed));
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
case QEvent::MouseMove:
|
||||
{
|
||||
const QMouseEvent* mouse_event = static_cast<QMouseEvent*>(event);
|
||||
|
||||
if (!m_relative_mouse_enabled)
|
||||
{
|
||||
const qreal dpr = devicePixelRatioF();
|
||||
const QPoint mouse_pos = mouse_event->pos();
|
||||
|
||||
const float scaled_x = static_cast<float>(static_cast<qreal>(mouse_pos.x()) * dpr);
|
||||
const float scaled_y = static_cast<float>(static_cast<qreal>(mouse_pos.y()) * dpr);
|
||||
InputManager::UpdatePointerAbsolutePosition(0, scaled_x, scaled_y);
|
||||
}
|
||||
else
|
||||
{
|
||||
// On windows, we use winapi here. The reason being that the coordinates in QCursor
|
||||
// are un-dpi-scaled, so we lose precision at higher desktop scalings.
|
||||
float dx = 0.0f, dy = 0.0f;
|
||||
|
||||
#ifndef _WIN32
|
||||
const QPoint mouse_pos = QCursor::pos();
|
||||
if (mouse_pos != m_relative_mouse_center_pos)
|
||||
{
|
||||
dx = static_cast<float>(mouse_pos.x() - m_relative_mouse_center_pos.x());
|
||||
dy = static_cast<float>(mouse_pos.y() - m_relative_mouse_center_pos.y());
|
||||
QCursor::setPos(m_relative_mouse_center_pos);
|
||||
}
|
||||
#else
|
||||
POINT mouse_pos;
|
||||
if (GetCursorPos(&mouse_pos))
|
||||
{
|
||||
dx = static_cast<float>(mouse_pos.x - m_relative_mouse_center_pos.x());
|
||||
dy = static_cast<float>(mouse_pos.y - m_relative_mouse_center_pos.y());
|
||||
SetCursorPos(m_relative_mouse_center_pos.x(), m_relative_mouse_center_pos.y());
|
||||
}
|
||||
#endif
|
||||
|
||||
if (dx != 0.0f)
|
||||
InputManager::UpdatePointerRelativeDelta(0, InputPointerAxis::X, dx);
|
||||
if (dy != 0.0f)
|
||||
InputManager::UpdatePointerRelativeDelta(0, InputPointerAxis::Y, dy);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
case QEvent::MouseButtonPress:
|
||||
case QEvent::MouseButtonDblClick:
|
||||
case QEvent::MouseButtonRelease:
|
||||
{
|
||||
if (const u32 button_mask = static_cast<u32>(static_cast<const QMouseEvent*>(event)->button()))
|
||||
{
|
||||
Host::RunOnCPUThread([button_index = std::countr_zero(button_mask),
|
||||
pressed = (event->type() != QEvent::MouseButtonRelease)]() {
|
||||
InputManager::InvokeEvents(
|
||||
InputManager::MakePointerButtonKey(0, button_index), static_cast<float>(pressed));
|
||||
});
|
||||
}
|
||||
|
||||
// don't toggle fullscreen when we're bound.. that wouldn't end well.
|
||||
if (event->type() == QEvent::MouseButtonDblClick &&
|
||||
static_cast<const QMouseEvent*>(event)->button() == Qt::LeftButton &&
|
||||
QtHost::IsVMValid() && !FullscreenUI::HasActiveWindow() &&
|
||||
((!QtHost::IsVMPaused() && !InputManager::HasAnyBindingsForKey(InputManager::MakePointerButtonKey(0, 0))) ||
|
||||
(QtHost::IsVMPaused() && !ImGuiManager::WantsMouseInput())) &&
|
||||
Host::GetBoolSettingValue("UI", "DoubleClickTogglesFullscreen", true))
|
||||
{
|
||||
g_emu_thread->toggleFullscreen();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
case QEvent::Wheel:
|
||||
{
|
||||
const QPoint delta_angle(static_cast<QWheelEvent*>(event)->angleDelta());
|
||||
const float dx = std::clamp(static_cast<float>(delta_angle.x()) / QtUtils::MOUSE_WHEEL_DELTA, -1.0f, 1.0f);
|
||||
if (dx != 0.0f)
|
||||
InputManager::UpdatePointerRelativeDelta(0, InputPointerAxis::WheelX, dx);
|
||||
|
||||
const float dy = std::clamp(static_cast<float>(delta_angle.y()) / QtUtils::MOUSE_WHEEL_DELTA, -1.0f, 1.0f);
|
||||
if (dy != 0.0f)
|
||||
InputManager::UpdatePointerRelativeDelta(0, InputPointerAxis::WheelY, dy);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
#if QT_VERSION < QT_VERSION_CHECK(6, 6, 0)
|
||||
case QEvent::Paint:
|
||||
#else
|
||||
case QEvent::DevicePixelRatioChange:
|
||||
#endif
|
||||
case QEvent::Resize:
|
||||
{
|
||||
QWidget::event(event);
|
||||
|
||||
const float dpr = devicePixelRatioF();
|
||||
const u32 scaled_width = static_cast<u32>(std::max(static_cast<int>(std::ceil(static_cast<qreal>(width()) * dpr)), 1));
|
||||
const u32 scaled_height = static_cast<u32>(std::max(static_cast<int>(std::ceil(static_cast<qreal>(height()) * dpr)), 1));
|
||||
|
||||
// avoid spamming resize events for paint events (sent on move on windows)
|
||||
if (m_last_window_width != scaled_width || m_last_window_height != scaled_height || m_last_window_scale != dpr)
|
||||
{
|
||||
m_last_window_width = scaled_width;
|
||||
m_last_window_height = scaled_height;
|
||||
m_last_window_scale = dpr;
|
||||
emit windowResizedEvent(scaled_width, scaled_height, dpr);
|
||||
}
|
||||
|
||||
updateCenterPos();
|
||||
return true;
|
||||
}
|
||||
|
||||
case QEvent::Move:
|
||||
{
|
||||
updateCenterPos();
|
||||
return true;
|
||||
}
|
||||
|
||||
case QEvent::Close:
|
||||
{
|
||||
if (m_destroying)
|
||||
return QWidget::event(event);
|
||||
|
||||
handleCloseEvent(static_cast<QCloseEvent*>(event));
|
||||
return true;
|
||||
}
|
||||
|
||||
case QEvent::WindowStateChange:
|
||||
{
|
||||
QWidget::event(event);
|
||||
|
||||
if (static_cast<QWindowStateChangeEvent*>(event)->oldState() & Qt::WindowMinimized)
|
||||
emit windowRestoredEvent();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
default:
|
||||
return QWidget::event(event);
|
||||
}
|
||||
}
|
||||
|
||||
DisplayContainer::DisplayContainer()
|
||||
: QStackedWidget(nullptr)
|
||||
{
|
||||
}
|
||||
|
||||
DisplayContainer::~DisplayContainer() = default;
|
||||
|
||||
bool DisplayContainer::isNeeded(bool fullscreen, bool render_to_main)
|
||||
{
|
||||
#if defined(_WIN32) || defined(__APPLE__)
|
||||
return false;
|
||||
#else
|
||||
if (!isRunningOnWayland())
|
||||
return false;
|
||||
|
||||
// We only need this on Wayland because of client-side decorations...
|
||||
return (fullscreen || !render_to_main);
|
||||
#endif
|
||||
}
|
||||
|
||||
bool DisplayContainer::isRunningOnWayland()
|
||||
{
|
||||
#if defined(_WIN32) || defined(__APPLE__)
|
||||
return false;
|
||||
#else
|
||||
const QString platform_name = QGuiApplication::platformName();
|
||||
return (platform_name == QStringLiteral("wayland"));
|
||||
#endif
|
||||
}
|
||||
|
||||
void DisplayContainer::setDisplayWidget(DisplayWidget* widget)
|
||||
{
|
||||
pxAssert(!m_display_widget);
|
||||
m_display_widget = widget;
|
||||
addWidget(widget);
|
||||
}
|
||||
|
||||
DisplayWidget* DisplayContainer::removeDisplayWidget()
|
||||
{
|
||||
DisplayWidget* widget = m_display_widget;
|
||||
pxAssert(widget);
|
||||
m_display_widget = nullptr;
|
||||
removeWidget(widget);
|
||||
return widget;
|
||||
}
|
||||
|
||||
bool DisplayContainer::event(QEvent* event)
|
||||
{
|
||||
if (event->type() == QEvent::Close && m_display_widget)
|
||||
{
|
||||
m_display_widget->handleCloseEvent(static_cast<QCloseEvent*>(event));
|
||||
return true;
|
||||
}
|
||||
|
||||
const bool res = QStackedWidget::event(event);
|
||||
if (!m_display_widget)
|
||||
return res;
|
||||
|
||||
switch (event->type())
|
||||
{
|
||||
case QEvent::WindowStateChange:
|
||||
{
|
||||
if (static_cast<QWindowStateChangeEvent*>(event)->oldState() & Qt::WindowMinimized)
|
||||
emit m_display_widget->windowRestoredEvent();
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
82
pcsx2-qt/DisplayWidget.h
Normal file
82
pcsx2-qt/DisplayWidget.h
Normal file
@@ -0,0 +1,82 @@
|
||||
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
|
||||
// SPDX-License-Identifier: GPL-3.0+
|
||||
|
||||
#pragma once
|
||||
#include "common/WindowInfo.h"
|
||||
#include <QtWidgets/QStackedWidget>
|
||||
#include <QtWidgets/QWidget>
|
||||
#include <optional>
|
||||
#include <vector>
|
||||
|
||||
class QCloseEvent;
|
||||
|
||||
class DisplayWidget final : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit DisplayWidget(QWidget* parent);
|
||||
~DisplayWidget();
|
||||
|
||||
QPaintEngine* paintEngine() const override;
|
||||
|
||||
int scaledWindowWidth() const;
|
||||
int scaledWindowHeight() const;
|
||||
|
||||
std::optional<WindowInfo> getWindowInfo();
|
||||
|
||||
void updateRelativeMode(bool enabled);
|
||||
void updateCursor(bool hidden);
|
||||
|
||||
void handleCloseEvent(QCloseEvent* event);
|
||||
void destroy();
|
||||
|
||||
Q_SIGNALS:
|
||||
void windowResizedEvent(int width, int height, float scale);
|
||||
void windowRestoredEvent();
|
||||
|
||||
protected:
|
||||
bool event(QEvent* event) override;
|
||||
|
||||
private:
|
||||
bool isActuallyFullscreen() const;
|
||||
void updateCenterPos();
|
||||
|
||||
QPoint m_relative_mouse_start_pos{};
|
||||
QPoint m_relative_mouse_center_pos{};
|
||||
bool m_relative_mouse_enabled = false;
|
||||
#ifdef _WIN32
|
||||
bool m_clip_mouse_enabled = false;
|
||||
#endif
|
||||
bool m_cursor_hidden = false;
|
||||
bool m_destroying = false;
|
||||
|
||||
std::vector<int> m_keys_pressed_with_modifiers;
|
||||
|
||||
u32 m_last_window_width = 0;
|
||||
u32 m_last_window_height = 0;
|
||||
float m_last_window_scale = 1.0f;
|
||||
};
|
||||
|
||||
class DisplayContainer final : public QStackedWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
DisplayContainer();
|
||||
~DisplayContainer();
|
||||
|
||||
// Wayland is broken in lots of ways, so we need to check for it.
|
||||
static bool isRunningOnWayland();
|
||||
|
||||
static bool isNeeded(bool fullscreen, bool render_to_main);
|
||||
|
||||
void setDisplayWidget(DisplayWidget* widget);
|
||||
DisplayWidget* removeDisplayWidget();
|
||||
|
||||
protected:
|
||||
bool event(QEvent* event) override;
|
||||
|
||||
private:
|
||||
DisplayWidget* m_display_widget = nullptr;
|
||||
};
|
||||
47
pcsx2-qt/EarlyHardwareCheck.cpp
Normal file
47
pcsx2-qt/EarlyHardwareCheck.cpp
Normal file
@@ -0,0 +1,47 @@
|
||||
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
|
||||
// SPDX-License-Identifier: GPL-3.0+
|
||||
|
||||
#if defined(_WIN32) && defined(_MSC_VER)
|
||||
|
||||
#include "pcsx2/VMManager.h"
|
||||
|
||||
#include "common/RedtapeWindows.h"
|
||||
|
||||
#pragma optimize("", off)
|
||||
|
||||
// The problem with AVX2 builds on Windows, is that MSVC generates AVX instructions for zeroing memory,
|
||||
// which is pretty common in our global object constructors. So, we have to use a special object which
|
||||
// gets initialized before all other global objects, that does the hardware check, and terminates the
|
||||
// process before main() or any of the other objects are constructed (which would subsequently crash).
|
||||
struct EarlyHardwareCheckObject
|
||||
{
|
||||
EarlyHardwareCheckObject()
|
||||
{
|
||||
const char* error;
|
||||
if (VMManager::PerformEarlyHardwareChecks(&error))
|
||||
return;
|
||||
|
||||
// we can't use StringUtil::UTF8StringToWideString because *that* constructor uses AVX..
|
||||
const int error_len = static_cast<int>(std::strlen(error));
|
||||
int wlen = MultiByteToWideChar(CP_UTF8, 0, error, error_len, nullptr, 0);
|
||||
if (wlen > 0)
|
||||
{
|
||||
wchar_t* werror = static_cast<wchar_t*>(HeapAlloc(GetProcessHeap(), 0, sizeof(wchar_t) * (error_len + 1)));
|
||||
if (werror && (wlen = MultiByteToWideChar(CP_UTF8, 0, error, error_len, werror, wlen)) > 0)
|
||||
{
|
||||
werror[wlen] = 0;
|
||||
MessageBoxW(NULL, werror, L"Hardware Check Failed", MB_ICONERROR);
|
||||
HeapFree(GetProcessHeap(), 0, werror);
|
||||
}
|
||||
}
|
||||
|
||||
TerminateProcess(GetCurrentProcess(), 0xFFFFFFFF);
|
||||
}
|
||||
};
|
||||
#pragma warning(disable : 4075) // warning C4075: initializers put in unrecognized initialization area
|
||||
#pragma init_seg(".CRT$XCT")
|
||||
EarlyHardwareCheckObject s_hardware_checker;
|
||||
|
||||
#pragma optimize("", on)
|
||||
|
||||
#endif
|
||||
138
pcsx2-qt/GameList/EmptyGameListWidget.ui
Normal file
138
pcsx2-qt/GameList/EmptyGameListWidget.ui
Normal file
@@ -0,0 +1,138 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>EmptyGameListWidget</class>
|
||||
<widget class="QWidget" name="EmptyGameListWidget">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>687</width>
|
||||
<height>470</height>
|
||||
</rect>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<spacer name="verticalSpacer_2">
|
||||
<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><html><head/><body><p><span style=" font-weight:700;">No games in supported formats were found.</span></p><p>Please add a directory with games to begin.</p><p>Game dumps in the following formats will be scanned and listed:</p></body></html></string>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignCenter</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="supportedFormats">
|
||||
<property name="text">
|
||||
<string notr="true">TextLabel</string>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignCenter</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<item>
|
||||
<spacer name="horizontalSpacer_2">
|
||||
<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="addGameDirectory">
|
||||
<property name="text">
|
||||
<string>Add Game Directory...</string>
|
||||
</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>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_2">
|
||||
<item>
|
||||
<spacer name="horizontalSpacer_3">
|
||||
<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="scanForNewGames">
|
||||
<property name="text">
|
||||
<string>Scan For New Games</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer_4">
|
||||
<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="verticalSpacer">
|
||||
<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>
|
||||
613
pcsx2-qt/GameList/GameListModel.cpp
Normal file
613
pcsx2-qt/GameList/GameListModel.cpp
Normal file
@@ -0,0 +1,613 @@
|
||||
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
|
||||
// SPDX-License-Identifier: GPL-3.0+
|
||||
|
||||
#include "GameListModel.h"
|
||||
#include "QtHost.h"
|
||||
#include "QtUtils.h"
|
||||
#include "common/FileSystem.h"
|
||||
#include "common/Path.h"
|
||||
#include "common/StringUtil.h"
|
||||
#include "fmt/format.h"
|
||||
#include <QtCore/QDate>
|
||||
#include <QtCore/QDateTime>
|
||||
#include <QtCore/QFuture>
|
||||
#include <QtCore/QFutureWatcher>
|
||||
#include <QtConcurrent/QtConcurrent>
|
||||
#include <QtGui/QGuiApplication>
|
||||
#include <QtGui/QIcon>
|
||||
#include <QtGui/QPainter>
|
||||
|
||||
static constexpr std::array<const char*, GameListModel::Column_Count> s_column_names = {
|
||||
{"Type", "Code", "Title", "File Title", "CRC", "Time Played", "Last Played", "Size", "Region", "Compatibility", "Cover"}};
|
||||
|
||||
static constexpr int COVER_ART_WIDTH = 350;
|
||||
static constexpr int COVER_ART_HEIGHT = 512;
|
||||
static constexpr int COVER_ART_SPACING = 32;
|
||||
static constexpr int MIN_COVER_CACHE_SIZE = 256;
|
||||
|
||||
static int DPRScale(int size, qreal dpr)
|
||||
{
|
||||
return static_cast<int>(static_cast<qreal>(size) * dpr);
|
||||
}
|
||||
|
||||
static int DPRUnscale(int size, qreal dpr)
|
||||
{
|
||||
return static_cast<int>(static_cast<qreal>(size) / dpr);
|
||||
}
|
||||
|
||||
static void resizeAndPadPixmap(QPixmap* pm, int expected_width, int expected_height, qreal dpr)
|
||||
{
|
||||
const int dpr_expected_width = DPRScale(expected_width, dpr);
|
||||
const int dpr_expected_height = DPRScale(expected_height, dpr);
|
||||
if (pm->width() == dpr_expected_width && pm->height() == dpr_expected_height)
|
||||
return;
|
||||
|
||||
*pm = pm->scaled(dpr_expected_width, dpr_expected_height, Qt::KeepAspectRatio, Qt::SmoothTransformation);
|
||||
if (pm->width() == dpr_expected_width && pm->height() == dpr_expected_height)
|
||||
return;
|
||||
|
||||
// QPainter works in unscaled coordinates.
|
||||
int xoffs = 0;
|
||||
int yoffs = 0;
|
||||
if (pm->width() < dpr_expected_width)
|
||||
xoffs = DPRUnscale((dpr_expected_width - pm->width()) / 2, dpr);
|
||||
if (pm->height() < dpr_expected_height)
|
||||
yoffs = DPRUnscale((dpr_expected_height - pm->height()) / 2, dpr);
|
||||
|
||||
QPixmap padded_image(dpr_expected_width, dpr_expected_height);
|
||||
padded_image.setDevicePixelRatio(dpr);
|
||||
padded_image.fill(Qt::transparent);
|
||||
QPainter painter;
|
||||
if (painter.begin(&padded_image))
|
||||
{
|
||||
painter.setCompositionMode(QPainter::CompositionMode_Source);
|
||||
painter.drawPixmap(xoffs, yoffs, *pm);
|
||||
painter.setCompositionMode(QPainter::CompositionMode_Destination);
|
||||
painter.fillRect(padded_image.rect(), QColor(0, 0, 0, 0));
|
||||
painter.end();
|
||||
}
|
||||
|
||||
*pm = padded_image;
|
||||
}
|
||||
|
||||
static QPixmap createPlaceholderImage(const QPixmap& placeholder_pixmap, int width, int height, float scale,
|
||||
qreal dpr, const std::string& title)
|
||||
{
|
||||
QPixmap pm(placeholder_pixmap.copy());
|
||||
pm.setDevicePixelRatio(dpr);
|
||||
if (pm.isNull())
|
||||
return QPixmap();
|
||||
|
||||
resizeAndPadPixmap(&pm, width, height, dpr);
|
||||
QPainter painter;
|
||||
if (painter.begin(&pm))
|
||||
{
|
||||
QFont font;
|
||||
font.setPointSize(std::max(static_cast<int>(32.0f * scale), 1));
|
||||
painter.setFont(font);
|
||||
painter.setPen(Qt::white);
|
||||
|
||||
const QRect text_rc(0, 0, static_cast<int>(static_cast<float>(width)),
|
||||
static_cast<int>(static_cast<float>(height)));
|
||||
painter.drawText(text_rc, Qt::AlignCenter | Qt::TextWordWrap, QString::fromStdString(title));
|
||||
painter.end();
|
||||
}
|
||||
|
||||
return pm;
|
||||
}
|
||||
|
||||
std::optional<GameListModel::Column> GameListModel::getColumnIdForName(std::string_view name)
|
||||
{
|
||||
for (int column = 0; column < Column_Count; column++)
|
||||
{
|
||||
if (name == s_column_names[column])
|
||||
return static_cast<Column>(column);
|
||||
}
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
const char* GameListModel::getColumnName(Column col)
|
||||
{
|
||||
return s_column_names[static_cast<int>(col)];
|
||||
}
|
||||
|
||||
GameListModel::GameListModel(float cover_scale, bool show_cover_titles, qreal dpr, QObject* parent /* = nullptr */)
|
||||
: QAbstractTableModel(parent)
|
||||
, m_show_titles_for_covers(show_cover_titles)
|
||||
, m_dpr{dpr}
|
||||
{
|
||||
loadSettings();
|
||||
loadCommonImages();
|
||||
setCoverScale(cover_scale);
|
||||
setColumnDisplayNames();
|
||||
}
|
||||
GameListModel::~GameListModel() = default;
|
||||
|
||||
void GameListModel::reloadThemeSpecificImages()
|
||||
{
|
||||
loadThemeSpecificImages();
|
||||
refresh();
|
||||
}
|
||||
|
||||
void GameListModel::setCoverScale(float scale)
|
||||
{
|
||||
if (m_cover_scale == scale)
|
||||
return;
|
||||
|
||||
m_cover_pixmap_cache.Clear();
|
||||
m_cover_scale = scale;
|
||||
m_cover_scale_counter.fetch_add(1, std::memory_order_release);
|
||||
m_loading_pixmap = QPixmap(getCoverArtWidth(), getCoverArtHeight());
|
||||
m_loading_pixmap.fill(QColor(0, 0, 0, 0));
|
||||
|
||||
emit coverScaleChanged();
|
||||
}
|
||||
|
||||
void GameListModel::refreshCovers()
|
||||
{
|
||||
m_cover_pixmap_cache.Clear();
|
||||
refresh();
|
||||
}
|
||||
|
||||
void GameListModel::updateCacheSize(int width, int height)
|
||||
{
|
||||
// This is a bit conversative, since it doesn't consider padding, but better to be over than under.
|
||||
const int cover_width = getCoverArtWidth();
|
||||
const int cover_height = getCoverArtHeight();
|
||||
const int num_columns = ((width + (cover_width - 1)) / cover_width);
|
||||
const int num_rows = ((height + (cover_height - 1)) / cover_height);
|
||||
m_cover_pixmap_cache.SetMaxCapacity(static_cast<int>(std::max(num_columns * num_rows, MIN_COVER_CACHE_SIZE)));
|
||||
}
|
||||
|
||||
void GameListModel::setDevicePixelRatio(qreal dpr)
|
||||
{
|
||||
m_dpr = dpr;
|
||||
loadCommonImages();
|
||||
refreshCovers();
|
||||
}
|
||||
|
||||
void GameListModel::loadOrGenerateCover(const GameList::Entry* ge)
|
||||
{
|
||||
// Why this counter: Every time we change the cover scale, we increment the counter variable. This way if the scale is changed
|
||||
// while there's outstanding jobs, the old jobs won't proceed (at the wrong size), or get added into the grid.
|
||||
const u32 counter = m_cover_scale_counter.load(std::memory_order_acquire);
|
||||
|
||||
QFuture<QPixmap> future = QtConcurrent::run([this, entry = *ge, counter]() -> QPixmap {
|
||||
QPixmap image;
|
||||
if (m_cover_scale_counter.load(std::memory_order_acquire) == counter)
|
||||
{
|
||||
const std::string cover_path(GameList::GetCoverImagePathForEntry(&entry));
|
||||
if (!cover_path.empty())
|
||||
{
|
||||
image = QPixmap(QString::fromStdString(cover_path));
|
||||
if (!image.isNull())
|
||||
{
|
||||
image.setDevicePixelRatio(m_dpr);
|
||||
resizeAndPadPixmap(&image, getCoverArtWidth(), getCoverArtHeight(), m_dpr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const std::string& title = entry.GetTitle(m_prefer_english_titles);
|
||||
|
||||
if (image.isNull())
|
||||
image = createPlaceholderImage(m_placeholder_pixmap, getCoverArtWidth(), getCoverArtHeight(), m_cover_scale, m_dpr, title);
|
||||
|
||||
if (m_cover_scale_counter.load(std::memory_order_acquire) != counter)
|
||||
image = {};
|
||||
|
||||
return image;
|
||||
});
|
||||
|
||||
// Context must be 'this' so we run on the UI thread.
|
||||
future.then(this, [this, path = ge->path, counter](QPixmap pm) {
|
||||
if (m_cover_scale_counter.load(std::memory_order_acquire) != counter)
|
||||
return;
|
||||
|
||||
m_cover_pixmap_cache.Insert(std::move(path), std::move(pm));
|
||||
invalidateCoverForPath(path);
|
||||
});
|
||||
}
|
||||
|
||||
void GameListModel::invalidateCoverForPath(const std::string& path)
|
||||
{
|
||||
// This isn't ideal, but not sure how else we can get the row, when it might change while scanning...
|
||||
auto lock = GameList::GetLock();
|
||||
const u32 count = GameList::GetEntryCount();
|
||||
std::optional<u32> row;
|
||||
for (u32 i = 0; i < count; i++)
|
||||
{
|
||||
if (GameList::GetEntryByIndex(i)->path == path)
|
||||
{
|
||||
row = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!row.has_value())
|
||||
{
|
||||
// Game removed?
|
||||
return;
|
||||
}
|
||||
|
||||
const QModelIndex mi(index(static_cast<int>(row.value()), Column_Cover));
|
||||
emit dataChanged(mi, mi, {Qt::DecorationRole});
|
||||
}
|
||||
|
||||
int GameListModel::getCoverArtWidth() const
|
||||
{
|
||||
return std::max(static_cast<int>(static_cast<float>(COVER_ART_WIDTH) * m_cover_scale), 1);
|
||||
}
|
||||
|
||||
int GameListModel::getCoverArtHeight() const
|
||||
{
|
||||
return std::max(static_cast<int>(static_cast<float>(COVER_ART_HEIGHT) * m_cover_scale), 1);
|
||||
}
|
||||
|
||||
int GameListModel::getCoverArtSpacing() const
|
||||
{
|
||||
return std::max(static_cast<int>(static_cast<float>(COVER_ART_SPACING) * m_cover_scale), 1);
|
||||
}
|
||||
|
||||
int GameListModel::rowCount(const QModelIndex& parent) const
|
||||
{
|
||||
if (parent.isValid())
|
||||
return 0;
|
||||
|
||||
return static_cast<int>(GameList::GetEntryCount());
|
||||
}
|
||||
|
||||
int GameListModel::columnCount(const QModelIndex& parent) const
|
||||
{
|
||||
if (parent.isValid())
|
||||
return 0;
|
||||
|
||||
return Column_Count;
|
||||
}
|
||||
|
||||
QString GameListModel::formatTimespan(time_t timespan)
|
||||
{
|
||||
// avoid an extra string conversion
|
||||
const u32 hours = static_cast<u32>(timespan / 3600);
|
||||
const u32 minutes = static_cast<u32>((timespan % 3600) / 60);
|
||||
if (hours > 0)
|
||||
return qApp->translate("GameList", "%n hours", "", hours);
|
||||
else
|
||||
return qApp->translate("GameList", "%n minutes", "", minutes);
|
||||
}
|
||||
|
||||
QVariant GameListModel::data(const QModelIndex& index, int role) const
|
||||
{
|
||||
if (!index.isValid())
|
||||
return {};
|
||||
|
||||
const int row = index.row();
|
||||
if (row < 0 || row >= static_cast<int>(GameList::GetEntryCount()))
|
||||
return {};
|
||||
|
||||
const auto lock = GameList::GetLock();
|
||||
const GameList::Entry* ge = GameList::GetEntryByIndex(row);
|
||||
if (!ge)
|
||||
return {};
|
||||
|
||||
switch (role)
|
||||
{
|
||||
case Qt::DisplayRole:
|
||||
{
|
||||
switch (index.column())
|
||||
{
|
||||
case Column_Serial:
|
||||
return QString::fromStdString(ge->serial);
|
||||
|
||||
case Column_Title:
|
||||
return QString::fromStdString(ge->GetTitle(m_prefer_english_titles));
|
||||
|
||||
case Column_FileTitle:
|
||||
return QtUtils::StringViewToQString(Path::GetFileTitle(ge->path));
|
||||
|
||||
case Column_CRC:
|
||||
return QString::fromStdString(fmt::format("{:08X}", ge->crc));
|
||||
|
||||
case Column_TimePlayed:
|
||||
{
|
||||
if (ge->total_played_time == 0)
|
||||
return {};
|
||||
else
|
||||
return formatTimespan(ge->total_played_time);
|
||||
}
|
||||
|
||||
case Column_LastPlayed:
|
||||
return QString::fromStdString(GameList::FormatTimestamp(ge->last_played_time));
|
||||
|
||||
case Column_Size:
|
||||
return QString("%1 MB").arg(static_cast<double>(ge->total_size) / 1048576.0, 0, 'f', 2);
|
||||
|
||||
case Column_Cover:
|
||||
{
|
||||
if (m_show_titles_for_covers)
|
||||
return QString::fromStdString(ge->GetTitle(m_prefer_english_titles));
|
||||
else
|
||||
return {};
|
||||
}
|
||||
|
||||
default:
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
case Qt::InitialSortOrderRole:
|
||||
{
|
||||
switch (index.column())
|
||||
{
|
||||
case Column_Type:
|
||||
return static_cast<int>(ge->type);
|
||||
|
||||
case Column_Serial:
|
||||
return QString::fromStdString(ge->serial);
|
||||
|
||||
case Column_Title:
|
||||
case Column_Cover:
|
||||
return QString::fromStdString(ge->GetTitleSort(m_prefer_english_titles));
|
||||
|
||||
case Column_FileTitle:
|
||||
return QtUtils::StringViewToQString(Path::GetFileTitle(ge->path));
|
||||
|
||||
case Column_CRC:
|
||||
return static_cast<int>(ge->crc);
|
||||
|
||||
case Column_TimePlayed:
|
||||
return static_cast<qlonglong>(ge->total_played_time);
|
||||
|
||||
case Column_LastPlayed:
|
||||
return static_cast<qlonglong>(ge->last_played_time);
|
||||
|
||||
case Column_Region:
|
||||
return static_cast<int>(ge->region);
|
||||
|
||||
case Column_Compatibility:
|
||||
return static_cast<int>(ge->compatibility_rating);
|
||||
|
||||
case Column_Size:
|
||||
return static_cast<qulonglong>(ge->total_size);
|
||||
|
||||
default:
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
case Qt::DecorationRole:
|
||||
{
|
||||
switch (index.column())
|
||||
{
|
||||
case Column_Type:
|
||||
{
|
||||
return m_type_pixmaps[static_cast<u32>(ge->type)];
|
||||
}
|
||||
|
||||
case Column_Region:
|
||||
{
|
||||
return m_region_pixmaps[static_cast<u32>(ge->region)];
|
||||
}
|
||||
|
||||
case Column_Compatibility:
|
||||
{
|
||||
return m_compatibility_pixmaps[static_cast<u32>(
|
||||
(static_cast<u32>(ge->compatibility_rating) >= GameList::CompatibilityRatingCount) ?
|
||||
GameList::CompatibilityRating::Unknown :
|
||||
ge->compatibility_rating)];
|
||||
}
|
||||
|
||||
case Column_Cover:
|
||||
{
|
||||
QPixmap* pm = m_cover_pixmap_cache.Lookup(ge->path);
|
||||
if (pm)
|
||||
return *pm;
|
||||
|
||||
// We insert the placeholder into the cache, so that we don't repeatedly
|
||||
// queue loading jobs for this game.
|
||||
const_cast<GameListModel*>(this)->loadOrGenerateCover(ge);
|
||||
return *m_cover_pixmap_cache.Insert(ge->path, m_loading_pixmap);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
return {};
|
||||
}
|
||||
|
||||
default:
|
||||
return {};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
QVariant GameListModel::headerData(int section, Qt::Orientation orientation, int role) const
|
||||
{
|
||||
if (orientation != Qt::Horizontal || role != Qt::DisplayRole || section < 0 || section >= Column_Count)
|
||||
return {};
|
||||
|
||||
return m_column_display_names[section];
|
||||
}
|
||||
|
||||
void GameListModel::refresh()
|
||||
{
|
||||
beginResetModel();
|
||||
loadSettings();
|
||||
endResetModel();
|
||||
}
|
||||
|
||||
bool GameListModel::titlesLessThan(int left_row, int right_row) const
|
||||
{
|
||||
if (left_row < 0 || left_row >= static_cast<int>(GameList::GetEntryCount()) || right_row < 0 ||
|
||||
right_row >= static_cast<int>(GameList::GetEntryCount()))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const GameList::Entry* left = GameList::GetEntryByIndex(left_row);
|
||||
const GameList::Entry* right = GameList::GetEntryByIndex(right_row);
|
||||
return QtHost::LocaleSensitiveCompare(QString::fromStdString(left->GetTitleSort(m_prefer_english_titles)),
|
||||
QString::fromStdString(right->GetTitleSort(m_prefer_english_titles))) < 0;
|
||||
}
|
||||
|
||||
bool GameListModel::lessThan(const QModelIndex& left_index, const QModelIndex& right_index, int column) const
|
||||
{
|
||||
if (!left_index.isValid() || !right_index.isValid())
|
||||
return false;
|
||||
|
||||
const int left_row = left_index.row();
|
||||
const int right_row = right_index.row();
|
||||
if (left_row < 0 || left_row >= static_cast<int>(GameList::GetEntryCount()) || right_row < 0 ||
|
||||
right_row >= static_cast<int>(GameList::GetEntryCount()))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto lock = GameList::GetLock();
|
||||
const GameList::Entry* left = GameList::GetEntryByIndex(left_row);
|
||||
const GameList::Entry* right = GameList::GetEntryByIndex(right_row);
|
||||
if (!left || !right)
|
||||
return false;
|
||||
|
||||
switch (column)
|
||||
{
|
||||
case Column_Type:
|
||||
{
|
||||
if (left->type == right->type)
|
||||
return titlesLessThan(left_row, right_row);
|
||||
|
||||
return static_cast<int>(left->type) < static_cast<int>(right->type);
|
||||
}
|
||||
|
||||
case Column_Serial:
|
||||
{
|
||||
if (left->serial == right->serial)
|
||||
return titlesLessThan(left_row, right_row);
|
||||
return (StringUtil::Strcasecmp(left->serial.c_str(), right->serial.c_str()) < 0);
|
||||
}
|
||||
|
||||
case Column_Title:
|
||||
{
|
||||
return titlesLessThan(left_row, right_row);
|
||||
}
|
||||
|
||||
case Column_FileTitle:
|
||||
{
|
||||
const std::string_view file_title_left(Path::GetFileTitle(left->path));
|
||||
const std::string_view file_title_right(Path::GetFileTitle(right->path));
|
||||
if (file_title_left == file_title_right)
|
||||
return titlesLessThan(left_row, right_row);
|
||||
|
||||
const std::size_t smallest = std::min(file_title_left.size(), file_title_right.size());
|
||||
return (StringUtil::Strncasecmp(file_title_left.data(), file_title_right.data(), smallest) < 0);
|
||||
}
|
||||
|
||||
case Column_Region:
|
||||
{
|
||||
if (left->region == right->region)
|
||||
return titlesLessThan(left_row, right_row);
|
||||
return (static_cast<int>(left->region) < static_cast<int>(right->region));
|
||||
}
|
||||
|
||||
case Column_Compatibility:
|
||||
{
|
||||
if (left->compatibility_rating == right->compatibility_rating)
|
||||
return titlesLessThan(left_row, right_row);
|
||||
|
||||
return (static_cast<int>(left->compatibility_rating) < static_cast<int>(right->compatibility_rating));
|
||||
}
|
||||
|
||||
case Column_Size:
|
||||
{
|
||||
if (left->total_size == right->total_size)
|
||||
return titlesLessThan(left_row, right_row);
|
||||
|
||||
return (left->total_size < right->total_size);
|
||||
}
|
||||
|
||||
case Column_CRC:
|
||||
{
|
||||
if (left->crc == right->crc)
|
||||
return titlesLessThan(left_row, right_row);
|
||||
|
||||
return (left->crc < right->crc);
|
||||
}
|
||||
|
||||
case Column_TimePlayed:
|
||||
{
|
||||
if (left->total_played_time == right->total_played_time)
|
||||
return titlesLessThan(left_row, right_row);
|
||||
|
||||
return (left->total_played_time < right->total_played_time);
|
||||
}
|
||||
|
||||
case Column_LastPlayed:
|
||||
{
|
||||
if (left->last_played_time == right->last_played_time)
|
||||
return titlesLessThan(left_row, right_row);
|
||||
|
||||
return (left->last_played_time < right->last_played_time);
|
||||
}
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void GameListModel::loadSettings()
|
||||
{
|
||||
m_prefer_english_titles = Host::GetBaseBoolSettingValue("UI", "PreferEnglishGameList", false);
|
||||
}
|
||||
|
||||
QIcon GameListModel::getIconForType(GameList::EntryType type)
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case GameList::EntryType::PS2Disc:
|
||||
case GameList::EntryType::PS1Disc:
|
||||
return QIcon::fromTheme("disc-2-line");
|
||||
|
||||
case GameList::EntryType::ELF:
|
||||
default:
|
||||
return QIcon::fromTheme("file-settings-line");
|
||||
}
|
||||
}
|
||||
|
||||
QIcon GameListModel::getIconForRegion(GameList::Region region)
|
||||
{
|
||||
return QIcon(
|
||||
QStringLiteral("%1/icons/flags/%2.svg").arg(QtHost::GetResourcesBasePath()).arg(GameList::RegionToString(region, false)));
|
||||
}
|
||||
|
||||
void GameListModel::loadThemeSpecificImages()
|
||||
{
|
||||
for (u32 type = 0; type < static_cast<u32>(GameList::EntryType::Count); type++)
|
||||
m_type_pixmaps[type] = getIconForType(static_cast<GameList::EntryType>(type)).pixmap(QSize(24, 24), m_dpr);
|
||||
|
||||
for (u32 i = 0; i < static_cast<u32>(GameList::Region::Count); i++)
|
||||
m_region_pixmaps[i] = getIconForRegion(static_cast<GameList::Region>(i)).pixmap(QSize(36, 26), m_dpr);
|
||||
}
|
||||
|
||||
void GameListModel::loadCommonImages()
|
||||
{
|
||||
loadThemeSpecificImages();
|
||||
|
||||
const QString base_path(QtHost::GetResourcesBasePath());
|
||||
for (u32 i = 1; i < GameList::CompatibilityRatingCount; i++)
|
||||
m_compatibility_pixmaps[i] = QIcon((QStringLiteral("%1/icons/star-%2.svg").arg(base_path).arg(i - 1))).pixmap(QSize(88, 16), m_dpr);
|
||||
|
||||
m_placeholder_pixmap.load(QStringLiteral("%1/cover-placeholder.png").arg(base_path));
|
||||
}
|
||||
|
||||
void GameListModel::setColumnDisplayNames()
|
||||
{
|
||||
m_column_display_names[Column_Type] = tr("Type");
|
||||
m_column_display_names[Column_Serial] = tr("Code");
|
||||
m_column_display_names[Column_Title] = tr("Title");
|
||||
m_column_display_names[Column_FileTitle] = tr("File Title");
|
||||
m_column_display_names[Column_CRC] = tr("CRC");
|
||||
m_column_display_names[Column_TimePlayed] = tr("Time Played");
|
||||
m_column_display_names[Column_LastPlayed] = tr("Last Played");
|
||||
m_column_display_names[Column_Size] = tr("Size");
|
||||
m_column_display_names[Column_Region] = tr("Region");
|
||||
m_column_display_names[Column_Compatibility] = tr("Compatibility");
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user