diff --git a/pcsx2-qt/CMakeLists.txt b/pcsx2-qt/CMakeLists.txt index 1bdff961fc..b92e3c6b4a 100644 --- a/pcsx2-qt/CMakeLists.txt +++ b/pcsx2-qt/CMakeLists.txt @@ -168,8 +168,7 @@ target_sources(pcsx2-qt PRIVATE Debugger/DisassemblyWidget.cpp Debugger/DisassemblyWidget.h Debugger/DisassemblyWidget.ui - Debugger/DockManager.cpp - Debugger/DockManager.h + Debugger/JsonValueWrapper.h Debugger/RegisterWidget.cpp Debugger/RegisterWidget.h Debugger/RegisterWidget.ui @@ -189,6 +188,22 @@ target_sources(pcsx2-qt PRIVATE Debugger/Breakpoints/BreakpointWidget.cpp Debugger/Breakpoints/BreakpointWidget.h Debugger/Breakpoints/BreakpointWidget.ui + Debugger/Docking/DockLayout.cpp + Debugger/Docking/DockLayout.h + Debugger/Docking/DockManager.cpp + Debugger/Docking/DockManager.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/LayoutEditorDialog.cpp + Debugger/Docking/LayoutEditorDialog.h + Debugger/Docking/LayoutEditorDialog.ui + Debugger/Docking/NoLayoutsWidget.cpp + Debugger/Docking/NoLayoutsWidget.h + Debugger/Docking/NoLayoutsWidget.ui Debugger/Memory/MemorySearchWidget.cpp Debugger/Memory/MemorySearchWidget.h Debugger/Memory/MemorySearchWidget.ui diff --git a/pcsx2-qt/Debugger/DebuggerWidget.cpp b/pcsx2-qt/Debugger/DebuggerWidget.cpp index c4d2148ba5..aead513086 100644 --- a/pcsx2-qt/Debugger/DebuggerWidget.cpp +++ b/pcsx2-qt/Debugger/DebuggerWidget.cpp @@ -3,20 +3,67 @@ #include "DebuggerWidget.h" -#include "common/Assertions.h" +#include "JsonValueWrapper.h" -DebuggerWidget::DebuggerWidget(DebugInterface* cpu, QWidget* parent) - : QWidget(parent) - , m_cpu(cpu) -{ -} +#include "DebugTools/DebugInterface.h" + +#include "common/Assertions.h" DebugInterface& DebuggerWidget::cpu() const { - pxAssertRel(m_cpu, "DebuggerWidget::cpu() called on object that doesn't have a CPU type set."); + if (m_cpu_override.has_value()) + return DebugInterface::get(*m_cpu_override); + + pxAssertRel(m_cpu, "DebuggerWidget::cpu called on object with null cpu."); return *m_cpu; } +bool DebuggerWidget::setCpu(DebugInterface& new_cpu) +{ + BreakPointCpu before = cpu().getCpuType(); + m_cpu = &new_cpu; + BreakPointCpu after = cpu().getCpuType(); + return before == after; +} + +std::optional DebuggerWidget::cpuOverride() const +{ + return m_cpu_override; +} + +bool DebuggerWidget::setCpuOverride(std::optional new_cpu) +{ + BreakPointCpu before = cpu().getCpuType(); + m_cpu_override = new_cpu; + BreakPointCpu after = cpu().getCpuType(); + return before == after; +} + +void DebuggerWidget::toJson(JsonValueWrapper& json) +{ + if (m_cpu_override.has_value()) + { + const char* cpu_name = DebugInterface::cpuName(*m_cpu_override); + + rapidjson::Value target; + target.SetString(cpu_name, strlen(cpu_name)); + json.value().AddMember("target", target, json.allocator()); + } +} + +bool DebuggerWidget::fromJson(JsonValueWrapper& json) +{ + auto target = json.value().FindMember("target"); + if (target != json.value().MemberEnd() && target->value.IsString()) + { + for (BreakPointCpu cpu : DEBUG_CPUS) + if (strcmp(DebugInterface::cpuName(cpu), target->value.GetString()) == 0) + m_cpu_override = cpu; + } + + return true; +} + void DebuggerWidget::applyMonospaceFont() { // Easiest way to handle cross platform monospace fonts @@ -29,3 +76,9 @@ void DebuggerWidget::applyMonospaceFont() setStyleSheet(QStringLiteral("font: 10pt 'Monospace'")); #endif } + +DebuggerWidget::DebuggerWidget(DebugInterface* cpu, QWidget* parent) + : QWidget(parent) + , m_cpu(cpu) +{ +} diff --git a/pcsx2-qt/Debugger/DebuggerWidget.h b/pcsx2-qt/Debugger/DebuggerWidget.h index 53fec8c4b4..c2d9636dd2 100644 --- a/pcsx2-qt/Debugger/DebuggerWidget.h +++ b/pcsx2-qt/Debugger/DebuggerWidget.h @@ -12,17 +12,39 @@ inline void not_yet_implemented() abort(); } +class JsonValueWrapper; + +// The base class for the contents of the dock widgets in the debugger. class DebuggerWidget : public QWidget { Q_OBJECT -protected: - DebuggerWidget(DebugInterface* cpu, QWidget* parent = nullptr); - +public: + // Get the effective debug interface associated with this particular widget + // if it's set, otherwise return the one associated with the layout that + // contains this widget. 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 widget. + std::optional 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 new_cpu); + + virtual void toJson(JsonValueWrapper& json); + virtual bool fromJson(JsonValueWrapper& json); + void applyMonospaceFont(); +protected: + DebuggerWidget(DebugInterface* cpu, QWidget* parent = nullptr); + private: DebugInterface* m_cpu; + std::optional m_cpu_override; }; diff --git a/pcsx2-qt/Debugger/DebuggerWindow.cpp b/pcsx2-qt/Debugger/DebuggerWindow.cpp index 47be09fc85..7ceb192ac5 100644 --- a/pcsx2-qt/Debugger/DebuggerWindow.cpp +++ b/pcsx2-qt/Debugger/DebuggerWindow.cpp @@ -3,6 +3,8 @@ #include "DebuggerWindow.h" +#include "Debugger/Docking/DockManager.h" + #include "DebugTools/DebugInterface.h" #include "DebugTools/Breakpoints.h" #include "DebugTools/SymbolImporter.h" @@ -11,12 +13,20 @@ #include "MainWindow.h" #include "AnalysisOptionsDialog.h" +#include + +DebuggerWindow* g_debugger_window = nullptr; + DebuggerWindow::DebuggerWindow(QWidget* parent) : KDDockWidgets::QtWidgets::MainWindow(QStringLiteral("DebuggerWindow"), {}, parent) - , m_dock_manager(this) + , m_dock_manager(new DockManager(this)) { m_ui.setupUi(this); + g_debugger_window = this; + + m_dock_manager->loadLayouts(); + 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); @@ -24,27 +34,71 @@ DebuggerWindow::DebuggerWindow(QWidget* parent) connect(m_ui.actionAnalyse, &QAction::triggered, this, &DebuggerWindow::onAnalyse); connect(m_ui.actionOnTop, &QAction::triggered, [this] { this->setWindowFlags(this->windowFlags() ^ Qt::WindowStaysOnTopHint); this->show(); }); + connect(m_ui.menuWindows, &QMenu::aboutToShow, this, [this]() { + m_dock_manager->createWindowsMenu(m_ui.menuWindows); + }); + + connect(m_ui.actionResetAllLayouts, &QAction::triggered, [this]() { + QMessageBox::StandardButton result = QMessageBox::question( + g_debugger_window, tr("Confirmation"), tr("Are you sure you want to reset all layouts?")); + + if (result == QMessageBox::Yes) + m_dock_manager->resetAllLayouts(); + }); + + connect(m_ui.actionResetDefaultLayouts, &QAction::triggered, [this]() { + QMessageBox::StandardButton result = QMessageBox::question( + g_debugger_window, tr("Confirmation"), tr("Are you sure you want to reset the default layouts?")); + + if (result == QMessageBox::Yes) + m_dock_manager->resetDefaultLayouts(); + }); + connect(g_emu_thread, &EmuThread::onVMPaused, this, &DebuggerWindow::onVMStateChanged); connect(g_emu_thread, &EmuThread::onVMResumed, this, &DebuggerWindow::onVMStateChanged); onVMStateChanged(); // If we missed a state change while we weren't loaded - // We can't do this in the designer, but we want to right align the actionOnTop action in the toolbar - //QWidget* spacer = new QWidget(this); - //spacer->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); - //m_ui.toolBar->insertWidget(m_ui.actionAnalyse, spacer); + m_dock_manager->switchToLayout(0); - //m_ui.cpuTabs->addTab(m_cpuWidget_r5900, "R5900"); - //m_ui.cpuTabs->addTab(m_cpuWidget_r3000, "R3000"); + QMenuBar* menu_bar = menuBar(); - m_dock_manager.switchToLayout(0); + setMenuWidget(m_dock_manager->createLayoutSwitcher(menu_bar)); - //QTabBar* tabs = new QTabBar(); - //tabs->addTab("Test"); - //m_ui.menuBar->layout()->addWidget(tabs); + Host::RunOnCPUThread([]() { + R5900SymbolImporter.OnDebuggerOpened(); + }); } -DebuggerWindow::~DebuggerWindow() = default; +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(); +} + +DockManager& DebuggerWindow::dockManager() +{ + return *m_dock_manager; +} // There is no straightforward way to set the tab text to bold in Qt // Sorry colour blind people, but this is the best we can do for now @@ -131,18 +185,16 @@ void DebuggerWindow::onAnalyse() dialog->show(); } -void DebuggerWindow::showEvent(QShowEvent* event) +void DebuggerWindow::closeEvent(QCloseEvent* event) { - Host::RunOnCPUThread([]() { - R5900SymbolImporter.OnDebuggerOpened(); - }); - QMainWindow::showEvent(event); -} + dockManager().saveCurrentLayout(); -void DebuggerWindow::hideEvent(QHideEvent* event) -{ Host::RunOnCPUThread([]() { R5900SymbolImporter.OnDebuggerClosed(); }); - QMainWindow::hideEvent(event); + + KDDockWidgets::QtWidgets::MainWindow::closeEvent(event); + + g_debugger_window = nullptr; + deleteLater(); } diff --git a/pcsx2-qt/Debugger/DebuggerWindow.h b/pcsx2-qt/Debugger/DebuggerWindow.h index 65df2dd71a..bcfa935aec 100644 --- a/pcsx2-qt/Debugger/DebuggerWindow.h +++ b/pcsx2-qt/Debugger/DebuggerWindow.h @@ -5,17 +5,24 @@ #include "ui_DebuggerWindow.h" -#include "DockManager.h" +#include "DebugTools/DebugInterface.h" #include +class DockManager; + class DebuggerWindow : public KDDockWidgets::QtWidgets::MainWindow { Q_OBJECT public: DebuggerWindow(QWidget* parent); - ~DebuggerWindow(); + + static DebuggerWindow* getInstance(); + static DebuggerWindow* createInstance(); + static void destroyInstance(); + + DockManager& dockManager(); public slots: void onVMStateChanged(); @@ -26,8 +33,7 @@ public slots: void onAnalyse(); protected: - void showEvent(QShowEvent* event); - void hideEvent(QHideEvent* event); + void closeEvent(QCloseEvent* event); private: Ui::DebuggerWindow m_ui; @@ -36,7 +42,9 @@ private: QAction* m_actionStepOver; QAction* m_actionStepOut; - DockManager m_dock_manager; + DockManager* m_dock_manager; void setTabActiveStyle(BreakPointCpu toggledCPU); }; + +extern DebuggerWindow* g_debugger_window; diff --git a/pcsx2-qt/Debugger/DebuggerWindow.ui b/pcsx2-qt/Debugger/DebuggerWindow.ui index c0bd9b9487..fac077dc57 100644 --- a/pcsx2-qt/Debugger/DebuggerWindow.ui +++ b/pcsx2-qt/Debugger/DebuggerWindow.ui @@ -77,17 +77,19 @@ Windows - - - Layouts - - View + + + Layouts + + + + @@ -169,6 +171,21 @@ Analyze + + + Reset All Layouts + + + + + Reset Default Layouts + + + + + Reset Splitter Positions + + diff --git a/pcsx2-qt/Debugger/DockManager.cpp b/pcsx2-qt/Debugger/DockManager.cpp deleted file mode 100644 index 409c3cece8..0000000000 --- a/pcsx2-qt/Debugger/DockManager.cpp +++ /dev/null @@ -1,108 +0,0 @@ -// SPDX-FileCopyrightText: 2002-2024 PCSX2 Dev Team -// SPDX-License-Identifier: GPL-3.0+ - -#include "DockManager.h" - -#include "DebuggerWindow.h" -#include "DisassemblyWidget.h" -#include "RegisterWidget.h" -#include "StackWidget.h" -#include "ThreadWidget.h" -#include "Breakpoints/BreakpointWidget.h" -#include "Memory/MemorySearchWidget.h" -#include "Memory/MemoryViewWidget.h" -#include "Memory/SavedAddressesWidget.h" -#include "SymbolTree/SymbolTreeWidgets.h" - -#include - -#include -#include - -#define FOR_EACH_DEBUGGER_DOCK_WIDGET \ - /* Top right. */ \ - X(DisassemblyWidget, QT_TRANSLATE_NOOP("DockWidget", "Disassembly"), OnRight, Root) \ - /* Bottom. */ \ - X(MemoryViewWidget, QT_TRANSLATE_NOOP("DockWidget", "Memory"), OnBottom, DisassemblyWidget) \ - X(BreakpointWidget, QT_TRANSLATE_NOOP("DockWidget", "Breakpoints"), None, MemoryViewWidget) \ - X(ThreadWidget, QT_TRANSLATE_NOOP("DockWidget", "Threads"), None, MemoryViewWidget) \ - X(StackWidget, QT_TRANSLATE_NOOP("DockWidget", "Stack"), None, MemoryViewWidget) \ - X(SavedAddressesWidget, QT_TRANSLATE_NOOP("DockWidget", "Saved Addresses"), None, MemoryViewWidget) \ - X(GlobalVariableTreeWidget, QT_TRANSLATE_NOOP("DockWidget", "Globals"), None, MemoryViewWidget) \ - X(LocalVariableTreeWidget, QT_TRANSLATE_NOOP("DockWidget", "Locals"), None, MemoryViewWidget) \ - X(ParameterVariableTreeWidget, QT_TRANSLATE_NOOP("DockWidget", "Parameters"), None, MemoryViewWidget) \ - /* Top left. */ \ - X(RegisterWidget, QT_TRANSLATE_NOOP("DockWidget", "Registers"), OnLeft, DisassemblyWidget) \ - X(FunctionTreeWidget, QT_TRANSLATE_NOOP("DockWidget", "Functions"), None, RegisterWidget) \ - X(MemorySearchWidget, QT_TRANSLATE_NOOP("DockWidget", "Memory Search"), None, RegisterWidget) - -DockManager::DockManager(DebuggerWindow* window) - : m_window(window) -{ - createDefaultLayout("R5900", r5900Debug); - //createDefaultLayout("R3000", r3000Debug); - loadLayouts(); -} - -void DockManager::configure_docking_system() -{ - KDDockWidgets::Config::self().setFlags( - KDDockWidgets::Config::Flag_HideTitleBarWhenTabsVisible | - KDDockWidgets::Config::Flag_AlwaysShowTabs | - KDDockWidgets::Config::Flag_AllowReorderTabs | - KDDockWidgets::Config::Flag_TabsHaveCloseButton | - KDDockWidgets::Config::Flag_TitleBarIsFocusable); -} - -const std::vector& DockManager::layouts() -{ - return m_layouts; -} - -void DockManager::switchToLayout(size_t layout) -{ - //m_layouts.at(m_current_layout).dock_manager->setParent(nullptr); - //m_window->setCentralWidget(m_layouts.at(layout).dock_manager); - //m_current_layout = layout; -} - -size_t DockManager::cloneLayout(size_t existing_layout, std::string new_name) -{ - return 0; -} - -bool DockManager::deleteLayout(size_t layout) -{ - return false; -} - -void DockManager::loadLayouts() -{ -} - -void DockManager::saveLayouts() -{ -} - -size_t DockManager::createDefaultLayout(const char* name, DebugInterface& cpu) -{ - size_t index = m_layouts.size(); - - Layout& layout = m_layouts.emplace_back(); - layout.name = name; - layout.cpu = cpu.getCpuType(); - layout.user_defined = false; - - KDDockWidgets::QtWidgets::DockWidget* dock_Root = nullptr; -#define X(Type, title, Location, Parent) \ - KDDockWidgets::QtWidgets::DockWidget* dock_##Type = new KDDockWidgets::QtWidgets::DockWidget(title); \ - dock_##Type->setWidget(new Type(cpu)); \ - if (KDDockWidgets::Location_##Location != KDDockWidgets::Location_None) \ - m_window->addDockWidget(dock_##Type, KDDockWidgets::Location_##Location, dock_##Parent); \ - else \ - dock_##Parent->addDockWidgetAsTab(dock_##Type); - FOR_EACH_DEBUGGER_DOCK_WIDGET -#undef X - - return index; -} diff --git a/pcsx2-qt/Debugger/DockManager.h b/pcsx2-qt/Debugger/DockManager.h deleted file mode 100644 index 430908a8a1..0000000000 --- a/pcsx2-qt/Debugger/DockManager.h +++ /dev/null @@ -1,42 +0,0 @@ -// SPDX-FileCopyrightText: 2002-2024 PCSX2 Dev Team -// SPDX-License-Identifier: GPL-3.0+ - -#pragma once - -#include "DebugTools/DebugInterface.h" - -#include -#include - -class DebuggerWindow; - -class DockManager -{ -public: - struct Layout - { - std::string name; - BreakPointCpu cpu; - bool user_defined = false; - }; - - DockManager(DebuggerWindow* window); - - static void configure_docking_system(); - - const std::vector& layouts(); - void switchToLayout(size_t layout); - size_t cloneLayout(size_t existing_layout, std::string new_name); - bool deleteLayout(size_t layout); - - void loadLayouts(); - void saveLayouts(); - -protected: - size_t createDefaultLayout(const char* name, DebugInterface& cpu); - - KDDockWidgets::QtWidgets::MainWindow* m_window; - - std::vector m_layouts; - size_t m_current_layout = 0; -}; diff --git a/pcsx2-qt/Debugger/Docking/DockLayout.cpp b/pcsx2-qt/Debugger/Docking/DockLayout.cpp new file mode 100644 index 0000000000..2bf8471528 --- /dev/null +++ b/pcsx2-qt/Debugger/Docking/DockLayout.cpp @@ -0,0 +1,682 @@ +// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team +// SPDX-License-Identifier: GPL-3.0+ + +#include "DockLayout.h" + +#include "Debugger/DebuggerWidget.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 +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#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 = 1; + +// Increment this whenever there is a non-breaking change to the JSON format. +const u32 DEBUGGER_LAYOUT_FILE_VERSION_MINOR = 0; + +DockLayout::DockLayout( + std::string name, + BreakPointCpu cpu, + bool is_default, + const DockTables::DefaultDockLayout& default_layout, + DockLayout::Index index) + : m_name(name) + , m_cpu(cpu) + , m_is_default(is_default) + , m_base_layout(default_layout.name) +{ + DebugInterface& debug_interface = DebugInterface::get(cpu); + + for (size_t i = 0; i < default_layout.widgets.size(); i++) + { + auto iterator = DockTables::DEBUGGER_WIDGETS.find(default_layout.widgets[i].type); + pxAssertRel(iterator != DockTables::DEBUGGER_WIDGETS.end(), "Invalid default layout."); + const DockTables::DebuggerWidgetDescription& dock_description = iterator->second; + + DebuggerWidget* widget = dock_description.create_widget(debug_interface); + m_widgets.emplace(default_layout.widgets[i].type, widget); + } + + save(index); +} + +DockLayout::DockLayout( + std::string name, + BreakPointCpu cpu, + bool is_default, + DockLayout::Index index) + : m_name(name) + , m_cpu(cpu) + , m_is_default(is_default) +{ + save(index); +} + +DockLayout::DockLayout( + std::string 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_base_layout(layout_to_clone.m_base_layout) + , 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_WIDGETS.find(widget_to_clone->metaObject()->className()); + if (widget_description == DockTables::DEBUGGER_WIDGETS.end()) + continue; + + DebuggerWidget* new_widget = widget_description->second.create_widget(DebugInterface::get(cpu)); + 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 std::string& DockLayout::name() const +{ + return m_name; +} + +void DockLayout::setName(std::string 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))) + recreateDebuggerWidget(unique_name); + } +} + +void DockLayout::freeze() +{ + pxAssert(!m_is_frozen); + m_is_frozen = true; + + // 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(dock->view()); + view->setWidget(new QWidget()); + + delete dock; + } +} + +void DockLayout::thaw(DebuggerWindow* window) +{ + pxAssert(m_is_frozen); + m_is_frozen = false; + + KDDockWidgets::LayoutSaver saver(KDDockWidgets::RestoreOption_RelativeToMainWindow); + + if (m_geometry.isEmpty()) + { + // This is a newly created layout with no geometry information. + setupDefaultLayout(window); + return; + } + + // Restore the geometry of the dock widgets we just recreated. + 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(dock->view()); + view->setWidget(new QWidget()); + + delete dock; + } + + setupDefaultLayout(window); + return; + } + + // Check that all the dock widgets have been restored correctly. + std::vector orphaned_debugger_widgets; + 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_widgets.emplace_back(unique_name); + } + } + + // Delete any debugger widgets that haven't been restored correctly. + for (const QString& unique_name : orphaned_debugger_widgets) + { + auto widget_iterator = m_widgets.find(unique_name); + delete widget_iterator->second.get(); + m_widgets.erase(widget_iterator); + } + + retranslateDockWidgets(); +} + +KDDockWidgets::Core::DockWidget* DockLayout::createDockWidget(const QString& name) +{ + pxAssert(!m_is_frozen); + pxAssert(KDDockWidgets::LayoutSaver::restoreInProgress()); + + auto widget_iterator = m_widgets.find(name); + if (widget_iterator == m_widgets.end()) + return nullptr; + + DebuggerWidget* widget = widget_iterator->second; + pxAssert(widget); + + auto view = static_cast( + KDDockWidgets::Config::self().viewFactory()->createDockWidget(name)); + view->setWidget(widget); + + return view->asController(); +} + +void DockLayout::retranslateDockWidgets() +{ + for (KDDockWidgets::Core::DockWidget* widget : KDDockWidgets::DockRegistry::self()->dockwidgets()) + retranslateDockWidget(widget); +} + +void DockLayout::retranslateDockWidget(KDDockWidgets::Core::DockWidget* dock_widget) +{ + pxAssert(!m_is_frozen); + + auto widget_iterator = m_widgets.find(dock_widget->uniqueName()); + if (widget_iterator == m_widgets.end()) + return; + + DebuggerWidget* widget = widget_iterator->second.get(); + if (!widget) + return; + + auto description_iterator = DockTables::DEBUGGER_WIDGETS.find(widget->metaObject()->className()); + if (description_iterator == DockTables::DEBUGGER_WIDGETS.end()) + return; + + const DockTables::DebuggerWidgetDescription& description = description_iterator->second; + + QString translated_title = QCoreApplication::translate("DebuggerWidget", description.title); + std::optional cpu_override = widget->cpuOverride(); + + if (cpu_override.has_value()) + { + const char* cpu_name = DebugInterface::cpuName(*cpu_override); + dock_widget->setTitle(QString("%1 (%2)").arg(translated_title).arg(cpu_name)); + } + else + { + dock_widget->setTitle(std::move(translated_title)); + } +} + +void DockLayout::dockWidgetClosed(KDDockWidgets::Core::DockWidget* dock_widget) +{ + // 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; + + auto debugger_widget_iterator = m_widgets.find(dock_widget->uniqueName()); + if (debugger_widget_iterator == m_widgets.end()) + return; + + m_widgets.erase(debugger_widget_iterator); + dock_widget->deleteLater(); +} + +bool DockLayout::hasDebuggerWidget(QString unique_name) +{ + return m_widgets.find(unique_name) != m_widgets.end(); +} + +void DockLayout::toggleDebuggerWidget(QString unique_name, DebuggerWindow* window) +{ + pxAssert(!m_is_frozen); + + auto debugger_widget_iterator = m_widgets.find(unique_name); + auto [controller, view] = DockUtils::dockWidgetFromName(unique_name); + + if (debugger_widget_iterator == m_widgets.end()) + { + // Create the dock widget. + if (controller) + return; + + auto description_iterator = DockTables::DEBUGGER_WIDGETS.find(unique_name); + if (description_iterator == DockTables::DEBUGGER_WIDGETS.end()) + return; + + const DockTables::DebuggerWidgetDescription& description = description_iterator->second; + + DebuggerWidget* widget = description.create_widget(DebugInterface::get(m_cpu)); + m_widgets.emplace(unique_name, widget); + + auto view = static_cast( + KDDockWidgets::Config::self().viewFactory()->createDockWidget(unique_name)); + view->setWidget(widget); + + KDDockWidgets::Core::DockWidget* controller = view->asController(); + if (!controller) + { + delete view; + return; + } + + DockUtils::insertDockWidgetAtPreferredLocation(controller, description.preferred_location, window); + retranslateDockWidget(controller); + } + else + { + // Delete the dock widget. + if (!controller) + return; + + m_widgets.erase(debugger_widget_iterator); + delete controller; + } +} + +void DockLayout::recreateDebuggerWidget(QString unique_name) +{ + pxAssert(!m_is_frozen); + + auto [controller, view] = DockUtils::dockWidgetFromName(unique_name); + if (!controller || !view) + return; + + auto debugger_widget_iterator = m_widgets.find(unique_name); + if (debugger_widget_iterator == m_widgets.end()) + return; + + DebuggerWidget* old_debugger_widget = debugger_widget_iterator->second; + pxAssert(old_debugger_widget == view->widget()); + + auto description_iterator = DockTables::DEBUGGER_WIDGETS.find(old_debugger_widget->metaObject()->className()); + if (description_iterator == DockTables::DEBUGGER_WIDGETS.end()) + return; + + const DockTables::DebuggerWidgetDescription& description = description_iterator->second; + + DebuggerWidget* new_debugger_widget = description.create_widget(DebugInterface::get(m_cpu)); + new_debugger_widget->setCpuOverride(old_debugger_widget->cpuOverride()); + debugger_widget_iterator->second = new_debugger_widget; + + view->setWidget(new_debugger_widget); + + delete old_debugger_widget; +} + +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 (!m_is_frozen) + { + // 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); + const std::string& default_layouts_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("version_major", DEBUGGER_LAYOUT_FILE_VERSION_MAJOR, json.GetAllocator()); + json.AddMember("version_minor", DEBUGGER_LAYOUT_FILE_VERSION_MINOR, json.GetAllocator()); + rapidjson::Value version_hash; + version_hash.SetString(default_layouts_hash.c_str(), default_layouts_hash.size()); + json.AddMember("version_hash", version_hash, json.GetAllocator()); + + json.AddMember("name", rapidjson::Value().SetString(m_name.c_str(), m_name.size()), json.GetAllocator()); + json.AddMember("target", rapidjson::Value().SetString(cpu_name, strlen(cpu_name)), json.GetAllocator()); + json.AddMember("index", static_cast(layout_index), json.GetAllocator()); + json.AddMember("isDefault", m_is_default, 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()); + } + + rapidjson::Value 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()); + + 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()); + + JsonValueWrapper wrapper(object, json.GetAllocator()); + widget->toJson(wrapper); + + widgets.PushBack(object, json.GetAllocator()); + } + json.AddMember("widgets", widgets, json.GetAllocator()); + + if (!m_geometry.isEmpty() && !geometry.Parse(m_geometry).HasParseError()) + json.AddMember("geometry", geometry, json.GetAllocator()); + + rapidjson::StringBuffer string_buffer; + rapidjson::PrettyWriter writer(string_buffer); + json.Accept(writer); + + std::string safe_name = Path::SanitizeFileName(m_name); + + // 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_frozen); + + result = SUCCESS; + + std::optional 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("version_major"); + if (version_major == json.MemberEnd() || !version_major->value.IsInt()) + { + Console.Error("Debugger: Layout file '%s' has missing or invalid 'version_major' property.", path.c_str()); + result = INVALID_FORMAT; + return; + } + + if (version_major->value.GetInt() != DEBUGGER_LAYOUT_FILE_VERSION_MAJOR) + { + result = MAJOR_VERSION_MISMATCH; + return; + } + + auto version_minor = json.FindMember("version_minor"); + if (version_minor == json.MemberEnd() || !version_minor->value.IsInt()) + { + Console.Error("Debugger: Layout file '%s' has missing or invalid 'version_minor' property.", path.c_str()); + result = INVALID_FORMAT; + return; + } + + auto version_hash = json.FindMember("version_hash"); + if (version_hash == json.MemberEnd() || !version_hash->value.IsString()) + { + Console.Error("Debugger: Layout file '%s' has missing or invalid 'version_hash' property.", path.c_str()); + result = INVALID_FORMAT; + return; + } + + if (strcmp(version_hash->value.GetString(), DockTables::hashDefaultLayouts().c_str()) != 0) + 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").toStdString(); + + 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 base_layout = json.FindMember("baseLayout"); + if (base_layout != json.MemberEnd() && base_layout->value.IsString()) + m_base_layout = base_layout->value.GetString(); + + auto widgets = json.FindMember("widgets"); + if (widgets != json.MemberEnd() && widgets->value.IsArray()) + { + for (rapidjson::Value& object : widgets->value.GetArray()) + { + auto unique_name = object.FindMember("uniqueName"); + if (unique_name == object.MemberEnd() || !unique_name->value.IsString()) + continue; + + auto type = object.FindMember("type"); + if (type == object.MemberEnd() || !type->value.IsString()) + continue; + + auto description = DockTables::DEBUGGER_WIDGETS.find(type->value.GetString()); + if (description == DockTables::DEBUGGER_WIDGETS.end()) + continue; + + DebuggerWidget* widget = description->second.create_widget(DebugInterface::get(m_cpu)); + + 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 writer(string_buffer); + geometry->value.Accept(writer); + + m_geometry = QByteArray(string_buffer.GetString(), string_buffer.GetSize()); + } + + m_layout_file_path = path; +} + +void DockLayout::setupDefaultLayout(DebuggerWindow* window) +{ + pxAssert(!m_is_frozen); + + if (m_base_layout.empty()) + return; + + const DockTables::DefaultDockLayout* layout = nullptr; + for (const DockTables::DefaultDockLayout& default_layout : DockTables::DEFAULT_DOCK_LAYOUTS) + if (default_layout.name == m_base_layout) + layout = &default_layout; + + if (!layout) + return; + + std::vector groups(layout->groups.size(), nullptr); + + for (const DockTables::DefaultDockWidgetDescription& dock_description : layout->widgets) + { + const DockTables::DefaultDockGroupDescription& group = layout->groups[static_cast(dock_description.group)]; + + auto widget_iterator = m_widgets.find(dock_description.type); + if (widget_iterator == m_widgets.end()) + continue; + + const QString& unique_name = widget_iterator->first; + DebuggerWidget* widget = widget_iterator->second; + + auto view = static_cast( + KDDockWidgets::Config::self().viewFactory()->createDockWidget(unique_name)); + view->setWidget(widget); + + if (!groups[static_cast(dock_description.group)]) + { + KDDockWidgets::QtWidgets::DockWidget* parent = nullptr; + if (group.parent != DockTables::DefaultDockGroup::ROOT) + parent = groups[static_cast(group.parent)]; + + window->addDockWidget(view, group.location, parent); + + groups[static_cast(dock_description.group)] = view; + } + else + { + groups[static_cast(dock_description.group)]->addDockWidgetAsTab(view); + } + } + + for (KDDockWidgets::Core::Group* group : KDDockWidgets::DockRegistry::self()->groups()) + group->setCurrentTabIndex(0); + + retranslateDockWidgets(); +} diff --git a/pcsx2-qt/Debugger/Docking/DockLayout.h b/pcsx2-qt/Debugger/Docking/DockLayout.h new file mode 100644 index 0000000000..0c58bdc7a9 --- /dev/null +++ b/pcsx2-qt/Debugger/Docking/DockLayout.h @@ -0,0 +1,146 @@ +// 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 +#include + +#include + +class DebuggerWidget; +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( + std::string name, + BreakPointCpu cpu, + bool is_default, + const DockTables::DefaultDockLayout& default_layout, + DockLayout::Index index); + + // Create a new blank layout. + DockLayout( + std::string name, + BreakPointCpu cpu, + bool is_default, + DockLayout::Index index); + + // Clone an existing layout. + DockLayout( + std::string 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 std::string& name() const; + void setName(std::string 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(DebuggerWindow* window); + + KDDockWidgets::Core::DockWidget* createDockWidget(const QString& name); + void retranslateDockWidgets(); + void retranslateDockWidget(KDDockWidgets::Core::DockWidget* dock_widget); + void dockWidgetClosed(KDDockWidgets::Core::DockWidget* dock_widget); + + bool hasDebuggerWidget(QString unique_name); + void toggleDebuggerWidget(QString unique_name, DebuggerWindow* window); + void recreateDebuggerWidget(QString unique_name); + + void deleteFile(); + + bool save(DockLayout::Index layout_index); + +private: + void load( + const std::string& path, + DockLayout::LoadResult& result, + DockLayout::Index& index_last_session); + + void setupDefaultLayout(DebuggerWindow* window); + + // The name displayed in the user interface. Also used to determine the + // file name for the layout file. + std::string 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; + + // 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; + + // 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> 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 false, + // otherwise it will be true. + bool m_is_frozen = true; +}; diff --git a/pcsx2-qt/Debugger/Docking/DockManager.cpp b/pcsx2-qt/Debugger/Docking/DockManager.cpp new file mode 100644 index 0000000000..86dd58603d --- /dev/null +++ b/pcsx2-qt/Debugger/Docking/DockManager.cpp @@ -0,0 +1,602 @@ +// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team +// SPDX-License-Identifier: GPL-3.0+ + +#include "DockManager.h" + +#include "Debugger/DebuggerWindow.h" +#include "Debugger/Docking/DockTables.h" +#include "Debugger/Docking/DockViews.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 +#include +#include +#include + +#include +#include +#include +#include + +#include "fmt/format.h" + +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() +{ + static bool done = false; + if (done) + return; + + KDDockWidgets::Config::self().setFlags( + KDDockWidgets::Config::Flag_HideTitleBarWhenTabsVisible | + KDDockWidgets::Config::Flag_AlwaysShowTabs | + KDDockWidgets::Config::Flag_AllowReorderTabs | + KDDockWidgets::Config::Flag_TitleBarIsFocusable); + + KDDockWidgets::Config::self().setDockWidgetFactoryFunc(&DockManager::dockWidgetFactory); + KDDockWidgets::Config::self().setViewFactory(new DockViewFactory()); + KDDockWidgets::Config::self().setDragAboutToStartFunc(&DockManager::dragAboutToStart); + KDDockWidgets::Config::self().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()) + { + 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) +{ + if (layout_index == m_current_layout) + return; + + if (m_current_layout != DockLayout::INVALID_INDEX) + { + DockLayout& layout = m_layouts.at(m_current_layout); + layout.freeze(); + layout.save(m_current_layout); + } + + m_current_layout = layout_index; + + if (m_current_layout != DockLayout::INVALID_INDEX) + { + DockLayout& layout = m_layouts.at(m_current_layout); + layout.thaw(g_debugger_window); + } +} + +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; + bool order_changed = false; + std::vector 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 std::string& name = layout.name(); + std::string 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 = fmt::format("{} #{}", name, 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); + } + + if (index_last_session != index) + order_changed = true; + + indices_last_session.emplace_back(index_last_session); + } + + // Make sure the layouts remain in the same order they were in previously. + std::vector layout_pointers; + for (DockLayout& layout : m_layouts) + layout_pointers.emplace_back(&layout); + + std::sort(layout_pointers.begin(), layout_pointers.end(), + [this, &indices_last_session](const DockLayout* lhs, const DockLayout* rhs) { + size_t lhs_index = lhs - m_layouts.data(); + size_t rhs_index = rhs - m_layouts.data(); + DockLayout::Index lhs_index_last_session = indices_last_session.at(lhs_index); + DockLayout::Index rhs_index_last_session = indices_last_session.at(rhs_index); + return lhs_index_last_session < rhs_index_last_session; + }); + + std::vector sorted_layouts; + for (size_t i = 0; i < layout_pointers.size(); i++) + sorted_layouts.emplace_back(std::move(*layout_pointers[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) + createLayout(tr(layout.name.c_str()).toStdString(), layout.cpu, true, layout); + + switchToLayout(0); + updateLayoutSwitcher(); + saveLayouts(); +} + +void DockManager::resetDefaultLayouts() +{ + switchToLayout(DockLayout::INVALID_INDEX); + + std::vector old_layouts = std::move(m_layouts); + m_layouts = std::vector(); + + for (const DockTables::DefaultDockLayout& layout : DockTables::DEFAULT_DOCK_LAYOUTS) + createLayout(tr(layout.name.c_str()).toStdString(), layout.cpu, true, layout); + + for (DockLayout& layout : old_layouts) + if (!layout.isDefault()) + m_layouts.emplace_back(std::move(layout)); + else + layout.deleteFile(); + + switchToLayout(0); + updateLayoutSwitcher(); + saveLayouts(); +} + +void DockManager::createWindowsMenu(QMenu* menu) +{ + menu->clear(); + + if (m_current_layout == DockLayout::INVALID_INDEX) + return; + + DockLayout& layout = m_layouts.at(m_current_layout); + + for (const auto& [type, desc] : DockTables::DEBUGGER_WIDGETS) + { + QAction* action = new QAction(menu); + action->setText(QCoreApplication::translate("DebuggerWidget", desc.title)); + action->setCheckable(true); + action->setChecked(layout.hasDebuggerWidget(type)); + connect(action, &QAction::triggered, this, [&layout, type]() { + layout.toggleDebuggerWidget(type, g_debugger_window); + }); + menu->addAction(action); + } +} + +QWidget* DockManager::createLayoutSwitcher(QWidget* menu_bar) +{ + QWidget* container = new QWidget; + + QHBoxLayout* layout = new QHBoxLayout; + layout->setContentsMargins(0, 2, 2, 0); + container->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, 4, 0, 4); + menu_wrapper->setLayout(menu_layout); + + menu_layout->addWidget(menu_bar); + + m_switcher = new QTabBar; + m_switcher->setContentsMargins(0, 0, 0, 0); + m_switcher->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Preferred); + m_switcher->setContextMenuPolicy(Qt::CustomContextMenu); + m_switcher->setMovable(true); + layout->addWidget(m_switcher); + + updateLayoutSwitcher(); + + connect(m_switcher, &QTabBar::tabMoved, this, &DockManager::layoutSwitcherTabMoved); + connect(m_switcher, &QTabBar::customContextMenuRequested, this, &DockManager::layoutSwitcherContextMenu); + + QWidget* spacer = new QWidget; + spacer->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); + layout->addWidget(spacer); + + QPushButton* lock_layout_toggle = new QPushButton; + connect(lock_layout_toggle, &QPushButton::toggled, this, [this, lock_layout_toggle](bool checked) { + setLayoutLocked(checked); + if (m_layout_locked) + lock_layout_toggle->setText(tr("Layout Locked")); + else + lock_layout_toggle->setText(tr("Layout Unlocked")); + }); + lock_layout_toggle->setCheckable(true); + lock_layout_toggle->setChecked(m_layout_locked); + lock_layout_toggle->setFlat(true); + layout->addWidget(lock_layout_toggle); + + return container; +} + +void DockManager::updateLayoutSwitcher() +{ + if (!m_switcher) + return; + + disconnect(m_tab_connection); + + for (int i = m_switcher->count(); i > 0; i--) + m_switcher->removeTab(i - 1); + + for (DockLayout& layout : m_layouts) + { + const char* cpu_name = DebugInterface::cpuName(layout.cpu()); + QString tab_name = QString("%1 (%2)").arg(layout.name().c_str()).arg(cpu_name); + m_switcher->addTab(tab_name); + } + + m_plus_tab_index = m_switcher->addTab("+"); + m_current_tab_index = m_current_layout; + + if (m_current_layout != DockLayout::INVALID_INDEX) + m_switcher->setCurrentIndex(m_current_layout); + + // If we don't have any layouts, the currently selected tab will never be + // changed, so we respond to all clicks instead. + if (!m_layouts.empty()) + m_tab_connection = connect(m_switcher, &QTabBar::currentChanged, this, &DockManager::layoutSwitcherTabChanged); + else + m_tab_connection = connect(m_switcher, &QTabBar::tabBarClicked, this, &DockManager::layoutSwitcherTabChanged); +} + +void DockManager::layoutSwitcherTabChanged(int index) +{ + if (index == m_plus_tab_index) + { + if (m_current_tab_index >= 0 && m_current_tab_index < m_plus_tab_index) + m_switcher->setCurrentIndex(m_current_tab_index); + + auto name_validator = [this](const std::string& name) { + return !hasNameConflict(name, DockLayout::INVALID_INDEX); + }; + + bool can_clone_current_layout = m_current_layout != DockLayout::INVALID_INDEX; + 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->initial_state(); + 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); + 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) + return; + + 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; + } + } + + switchToLayout(new_layout); + updateLayoutSwitcher(); + } + } + else + { + DockLayout::Index layout_index = static_cast(index); + if (layout_index < 0 || layout_index >= m_layouts.size()) + return; + + switchToLayout(layout_index); + m_current_tab_index = index; + } +} + +void DockManager::layoutSwitcherTabMoved(int from, int to) +{ + DockLayout::Index from_index = static_cast(from); + DockLayout::Index to_index = static_cast(to); + + 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; +} + +void DockManager::layoutSwitcherContextMenu(QPoint pos) +{ + int tab_index = m_switcher->tabAt(pos); + if (tab_index < 0 || tab_index >= m_plus_tab_index) + return; + + QMenu* menu = new QMenu(tr("Layout Switcher Context Menu"), m_switcher); + + QAction* edit_action = new QAction(tr("Edit Layout"), menu); + connect(edit_action, &QAction::triggered, [this, tab_index]() { + DockLayout::Index layout_index = static_cast(tab_index); + if (layout_index >= m_layouts.size()) + return; + + DockLayout& layout = m_layouts[layout_index]; + + auto name_validator = [this, layout_index](const std::string& name) { + return !hasNameConflict(name, layout_index); + }; + + LayoutEditorDialog* dialog = new LayoutEditorDialog( + layout.name(), layout.cpu(), name_validator, g_debugger_window); + + if (dialog->exec() == QDialog::Accepted && name_validator(dialog->name())) + { + layout.setName(dialog->name()); + layout.setCpu(dialog->cpu()); + + layout.save(layout_index); + + updateLayoutSwitcher(); + } + }); + menu->addAction(edit_action); + + QAction* delete_action = new QAction(tr("Delete Layout"), menu); + connect(delete_action, &QAction::triggered, [this, tab_index]() { + DockLayout::Index layout_index = static_cast(tab_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().c_str()); + QMessageBox::StandardButton result = QMessageBox::question(g_debugger_window, tr("Confirmation"), text); + + if (result == QMessageBox::Yes) + { + deleteLayout(layout_index); + updateLayoutSwitcher(); + } + }); + menu->addAction(delete_action); + + menu->popup(m_switcher->mapToGlobal(pos)); +} + +bool DockManager::hasNameConflict(const std::string& name, DockLayout::Index layout_index) +{ + std::string safe_name = Path::SanitizeFileName(name); + for (DockLayout::Index i = 0; i < m_layouts.size(); i++) + if (i != layout_index && StringUtil::compareNoCase(m_layouts[i].name(), safe_name)) + return true; + + return false; +} + +void DockManager::retranslateDockWidget(KDDockWidgets::Core::DockWidget* dock_widget) +{ + if (m_current_layout == DockLayout::INVALID_INDEX) + return; + + m_layouts.at(m_current_layout).retranslateDockWidget(dock_widget); +} + +void DockManager::dockWidgetClosed(KDDockWidgets::Core::DockWidget* dock_widget) +{ + if (m_current_layout == DockLayout::INVALID_INDEX) + return; + + m_layouts.at(m_current_layout).dockWidgetClosed(dock_widget); +} + +void DockManager::recreateDebuggerWidget(QString unique_name) +{ + if (m_current_layout == DockLayout::INVALID_INDEX) + return; + + m_layouts.at(m_current_layout).recreateDebuggerWidget(unique_name); +} + +bool DockManager::isLayoutLocked() +{ + return m_layout_locked; +} + +void DockManager::setLayoutLocked(bool locked) +{ + m_layout_locked = locked; + + for (KDDockWidgets::Core::Group* group : KDDockWidgets::DockRegistry::self()->groups()) + { + auto stack = static_cast(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)); + } +} + +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; +} diff --git a/pcsx2-qt/Debugger/Docking/DockManager.h b/pcsx2-qt/Debugger/Docking/DockManager.h new file mode 100644 index 0000000000..d9b4a10711 --- /dev/null +++ b/pcsx2-qt/Debugger/Docking/DockManager.h @@ -0,0 +1,94 @@ +// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team +// SPDX-License-Identifier: GPL-3.0+ + +#pragma once + +#include "Debugger/Docking/DockLayout.h" + +#include +#include +#include +#include +#include + +#include +#include + +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 + 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)..., layout_index); + + return layout_index; + } + + bool deleteLayout(DockLayout::Index layout_index); + + void switchToLayout(DockLayout::Index layout_index); + + void loadLayouts(); + bool saveLayouts(); + bool saveCurrentLayout(); + + void resetAllLayouts(); + void resetDefaultLayouts(); + + void createWindowsMenu(QMenu* menu); + + QWidget* createLayoutSwitcher(QWidget* menu_bar); + void updateLayoutSwitcher(); + void layoutSwitcherTabChanged(int index); + void layoutSwitcherTabMoved(int from, int to); + void layoutSwitcherContextMenu(QPoint pos); + + bool hasNameConflict(const std::string& name, DockLayout::Index layout_index); + + void retranslateDockWidget(KDDockWidgets::Core::DockWidget* dock_widget); + void dockWidgetClosed(KDDockWidgets::Core::DockWidget* dock_widget); + + void recreateDebuggerWidget(QString unique_name); + + bool isLayoutLocked(); + void setLayoutLocked(bool locked); + +private: + static KDDockWidgets::Core::DockWidget* dockWidgetFactory(const QString& name); + static bool dragAboutToStart(KDDockWidgets::Core::Draggable* draggable); + + std::vector m_layouts; + DockLayout::Index m_current_layout = DockLayout::INVALID_INDEX; + + QTabBar* m_switcher = nullptr; + int m_plus_tab_index = -1; + int m_current_tab_index = -1; + + QMetaObject::Connection m_tab_connection; + + bool m_layout_locked = true; +}; diff --git a/pcsx2-qt/Debugger/Docking/DockTables.cpp b/pcsx2-qt/Debugger/Docking/DockTables.cpp new file mode 100644 index 0000000000..5d625b3c21 --- /dev/null +++ b/pcsx2-qt/Debugger/Docking/DockTables.cpp @@ -0,0 +1,175 @@ +// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team +// SPDX-License-Identifier: GPL-3.0+ + +#include "DockTables.h" + +#include "Debugger/DisassemblyWidget.h" +#include "Debugger/RegisterWidget.h" +#include "Debugger/StackWidget.h" +#include "Debugger/ThreadWidget.h" +#include "Debugger/Breakpoints/BreakpointWidget.h" +#include "Debugger/Memory/MemorySearchWidget.h" +#include "Debugger/Memory/MemoryViewWidget.h" +#include "Debugger/Memory/SavedAddressesWidget.h" +#include "Debugger/SymbolTree/SymbolTreeWidgets.h" + +#include "common/MD5Digest.h" + +#include "fmt/format.h" + +using namespace DockUtils; + +#define DEBUGGER_WIDGET(type, title, preferred_location) \ + { \ + #type, \ + { \ + [](DebugInterface& cpu) -> DebuggerWidget* { return new type(cpu); }, \ + title, \ + preferred_location \ + } \ + } + +const std::map DockTables::DEBUGGER_WIDGETS = { + DEBUGGER_WIDGET(BreakpointWidget, QT_TRANSLATE_NOOP("DebuggerWidget", "Breakpoints"), BOTTOM_MIDDLE), + DEBUGGER_WIDGET(DisassemblyWidget, QT_TRANSLATE_NOOP("DebuggerWidget", "Disassembly"), TOP_RIGHT), + DEBUGGER_WIDGET(FunctionTreeWidget, QT_TRANSLATE_NOOP("DebuggerWidget", "Functions"), TOP_LEFT), + DEBUGGER_WIDGET(GlobalVariableTreeWidget, QT_TRANSLATE_NOOP("DebuggerWidget", "Globals"), BOTTOM_MIDDLE), + DEBUGGER_WIDGET(LocalVariableTreeWidget, QT_TRANSLATE_NOOP("DebuggerWidget", "Locals"), BOTTOM_MIDDLE), + DEBUGGER_WIDGET(MemorySearchWidget, QT_TRANSLATE_NOOP("DebuggerWidget", "Memory Search"), TOP_LEFT), + DEBUGGER_WIDGET(MemoryViewWidget, QT_TRANSLATE_NOOP("DebuggerWidget", "Memory"), BOTTOM_MIDDLE), + DEBUGGER_WIDGET(ParameterVariableTreeWidget, QT_TRANSLATE_NOOP("DebuggerWidget", "Parameters"), BOTTOM_MIDDLE), + DEBUGGER_WIDGET(RegisterWidget, QT_TRANSLATE_NOOP("DebuggerWidget", "Registers"), TOP_LEFT), + DEBUGGER_WIDGET(SavedAddressesWidget, QT_TRANSLATE_NOOP("DebuggerWidget", "Saved Addresses"), BOTTOM_MIDDLE), + DEBUGGER_WIDGET(StackWidget, QT_TRANSLATE_NOOP("DebuggerWidget", "Stack"), BOTTOM_MIDDLE), + DEBUGGER_WIDGET(ThreadWidget, QT_TRANSLATE_NOOP("DebuggerWidget", "Threads"), BOTTOM_MIDDLE), +}; + +#undef DEBUGGER_WIDGET + +const std::vector DockTables::DEFAULT_DOCK_LAYOUTS = { + { + QT_TRANSLATE_NOOP("DebuggerLayout", "R5900"), + BREAKPOINT_EE, + { + /* [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}, + }, + { + /* DefaultDockGroup::TOP_RIGHT */ + {"DisassemblyWidget", DefaultDockGroup::TOP_RIGHT}, + /* DefaultDockGroup::BOTTOM */ + {"MemoryViewWidget", DefaultDockGroup::BOTTOM}, + {"BreakpointWidget", DefaultDockGroup::BOTTOM}, + {"ThreadWidget", DefaultDockGroup::BOTTOM}, + {"StackWidget", DefaultDockGroup::BOTTOM}, + {"SavedAddressesWidget", DefaultDockGroup::BOTTOM}, + {"GlobalVariableTreeWidget", DefaultDockGroup::BOTTOM}, + {"LocalVariableTreeWidget", DefaultDockGroup::BOTTOM}, + {"ParameterVariableTreeWidget", DefaultDockGroup::BOTTOM}, + /* DefaultDockGroup::TOP_LEFT */ + {"RegisterWidget", DefaultDockGroup::TOP_LEFT}, + {"FunctionTreeWidget", DefaultDockGroup::TOP_LEFT}, + {"MemorySearchWidget", DefaultDockGroup::TOP_LEFT}, + }, + }, + { + QT_TRANSLATE_NOOP("DebuggerLayout", "R3000"), + BREAKPOINT_IOP, + { + /* [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}, + }, + { + /* DefaultDockGroup::TOP_RIGHT */ + {"DisassemblyWidget", DefaultDockGroup::TOP_RIGHT}, + /* DefaultDockGroup::BOTTOM */ + {"MemoryViewWidget", DefaultDockGroup::BOTTOM}, + {"BreakpointWidget", DefaultDockGroup::BOTTOM}, + {"ThreadWidget", DefaultDockGroup::BOTTOM}, + {"StackWidget", DefaultDockGroup::BOTTOM}, + {"SavedAddressesWidget", DefaultDockGroup::BOTTOM}, + {"GlobalVariableTreeWidget", DefaultDockGroup::BOTTOM}, + {"LocalVariableTreeWidget", DefaultDockGroup::BOTTOM}, + {"ParameterVariableTreeWidget", DefaultDockGroup::BOTTOM}, + /* DefaultDockGroup::TOP_LEFT */ + {"RegisterWidget", DefaultDockGroup::TOP_LEFT}, + {"FunctionTreeWidget", DefaultDockGroup::TOP_LEFT}, + {"MemorySearchWidget", DefaultDockGroup::TOP_LEFT}, + }, + }, +}; + +const std::string& DockTables::hashDefaultLayouts() +{ + static std::string hash; + if (!hash.empty()) + return hash; + + MD5Digest md5; + + u32 hash_version = 1; + md5.Update(&hash_version, sizeof(hash_version)); + + size_t layout_count = DEFAULT_DOCK_LAYOUTS.size(); + md5.Update(&layout_count, sizeof(layout_count)); + + for (const DefaultDockLayout& layout : DEFAULT_DOCK_LAYOUTS) + hashDefaultLayout(layout, md5); + + u8 digest[16]; + md5.Final(digest); + hash = fmt::format( + "{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}", + digest[0], digest[1], digest[2], digest[3], digest[4], digest[5], digest[6], digest[7], + digest[8], digest[9], digest[10], digest[11], digest[12], digest[13], digest[14], digest[15]); + + return hash; +} + +void DockTables::hashDefaultLayout(const DefaultDockLayout& layout, MD5Digest& md5) +{ + size_t layout_name_size = layout.name.size(); + md5.Update(&layout_name_size, sizeof(layout_name_size)); + md5.Update(layout.name.c_str(), layout.name.size()); + + const char* cpu_name = DebugInterface::cpuName(layout.cpu); + size_t cpu_name_size = strlen(cpu_name); + md5.Update(&cpu_name_size, sizeof(cpu_name_size)); + md5.Update(cpu_name, cpu_name_size); + + size_t group_count = layout.groups.size(); + md5.Update(&group_count, sizeof(group_count)); + + for (const DefaultDockGroupDescription& group : layout.groups) + hashDefaultGroup(group, md5); + + size_t widget_count = layout.widgets.size(); + md5.Update(&widget_count, sizeof(widget_count)); + + for (const DefaultDockWidgetDescription& widget : layout.widgets) + hashDefaultDockWidget(widget, md5); +} + +void DockTables::hashDefaultGroup(const DefaultDockGroupDescription& group, MD5Digest& md5) +{ + const char* location = DockUtils::locationToString(group.location); + size_t location_size = strlen(location); + md5.Update(&location_size, sizeof(location_size)); + md5.Update(location, location_size); + + u32 parent = static_cast(group.parent); + md5.Update(&parent, sizeof(parent)); +} + +void DockTables::hashDefaultDockWidget(const DefaultDockWidgetDescription& widget, MD5Digest& md5) +{ + std::string type = widget.type.toStdString(); + size_t type_size = type.size(); + md5.Update(&type_size, sizeof(type_size)); + md5.Update(type.c_str(), type_size); + + u32 group = static_cast(widget.group); + md5.Update(&group, sizeof(group)); +} diff --git a/pcsx2-qt/Debugger/Docking/DockTables.h b/pcsx2-qt/Debugger/Docking/DockTables.h new file mode 100644 index 0000000000..55a31bb33c --- /dev/null +++ b/pcsx2-qt/Debugger/Docking/DockTables.h @@ -0,0 +1,72 @@ +// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team +// SPDX-License-Identifier: GPL-3.0+ + +#pragma once + +#include "DockUtils.h" + +#include "DebugTools/DebugInterface.h" + +#include + +class MD5Digest; + +class DebugInterface; +class DebuggerWidget; + +namespace DockTables +{ + struct DebuggerWidgetDescription + { + DebuggerWidget* (*create_widget)(DebugInterface& cpu); + + // The untranslated string displayed as the dock widget tab text. + const char* title; + + // 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 DEBUGGER_WIDGETS; + + enum class DefaultDockGroup + { + ROOT = -1, + TOP_RIGHT = 0, + BOTTOM = 1, + TOP_LEFT = 2 + }; + + struct DefaultDockGroupDescription + { + KDDockWidgets::Location location; + DefaultDockGroup parent; + }; + + extern const std::vector DEFAULT_DOCK_GROUPS; + + struct DefaultDockWidgetDescription + { + QString type; + DefaultDockGroup group; + }; + + struct DefaultDockLayout + { + std::string name; + BreakPointCpu cpu; + std::vector groups; + std::vector widgets; + }; + + extern const std::vector DEFAULT_DOCK_LAYOUTS; + + // This is used to determine if the user has updated and we need to recreate + // the default layouts. + const std::string& hashDefaultLayouts(); + + void hashDefaultLayout(const DefaultDockLayout& layout, MD5Digest& md5); + void hashDefaultGroup(const DefaultDockGroupDescription& group, MD5Digest& md5); + void hashDefaultDockWidget(const DefaultDockWidgetDescription& widget, MD5Digest& md5); +} // namespace DockTables diff --git a/pcsx2-qt/Debugger/Docking/DockUtils.cpp b/pcsx2-qt/Debugger/Docking/DockUtils.cpp new file mode 100644 index 0000000000..be4dbc3152 --- /dev/null +++ b/pcsx2-qt/Debugger/Docking/DockUtils.cpp @@ -0,0 +1,116 @@ +// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team +// SPDX-License-Identifier: GPL-3.0+ + +#include "DockUtils.h" + +#include +#include +#include +#include + +DockUtils::DockWidgetPair DockUtils::dockWidgetFromName(QString unique_name) +{ + KDDockWidgets::Vector names{unique_name}; + KDDockWidgets::Vector dock_widgets = + KDDockWidgets::DockRegistry::self()->dockWidgets(names); + if (dock_widgets.size() != 1 || !dock_widgets[0]) + return {}; + + return {dock_widgets[0], static_cast(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(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(dock_widget->view()); + window->addDockWidget(dock_view, KDDockWidgets::Location_OnTop); + } +} + +const char* DockUtils::locationToString(KDDockWidgets::Location location) +{ + switch (location) + { + case KDDockWidgets::Location_None: + return "none"; + case KDDockWidgets::Location_OnLeft: + return "left"; + case KDDockWidgets::Location_OnTop: + return "top"; + case KDDockWidgets::Location_OnRight: + return "right"; + case KDDockWidgets::Location_OnBottom: + return "bottom"; + } + + return ""; +} diff --git a/pcsx2-qt/Debugger/Docking/DockUtils.h b/pcsx2-qt/Debugger/Docking/DockUtils.h new file mode 100644 index 0000000000..c3ea0f2411 --- /dev/null +++ b/pcsx2-qt/Debugger/Docking/DockUtils.h @@ -0,0 +1,39 @@ +// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team +// SPDX-License-Identifier: GPL-3.0+ + +#pragma once + +#include +#include +#include + +namespace DockUtils +{ + struct DockWidgetPair + { + KDDockWidgets::Core::DockWidget* controller = nullptr; + KDDockWidgets::QtWidgets::DockWidget* view = nullptr; + }; + + DockWidgetPair dockWidgetFromName(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); + + const char* locationToString(KDDockWidgets::Location location); +} // namespace DockUtils diff --git a/pcsx2-qt/Debugger/Docking/DockViews.cpp b/pcsx2-qt/Debugger/Docking/DockViews.cpp new file mode 100644 index 0000000000..3a57c16ec1 --- /dev/null +++ b/pcsx2-qt/Debugger/Docking/DockViews.cpp @@ -0,0 +1,224 @@ +// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team +// SPDX-License-Identifier: GPL-3.0+ + +#include "DockViews.h" + +#include "Debugger/DebuggerWidget.h" +#include "Debugger/DebuggerWindow.h" +#include "Debugger/Docking/DockManager.h" + +#include "DebugTools/DebugInterface.h" + +#include +#include + +#include + +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)); +} + +// ***************************************************************************** + +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) +{ + auto view = static_cast(sender()); + + KDDockWidgets::Core::DockWidget* controller = view->asController(); + if (!controller) + return; + + if (!open && g_debugger_window) + g_debugger_window->dockManager().dockWidgetClosed(controller); +} + +// ***************************************************************************** + +DockTitleBar::DockTitleBar(KDDockWidgets::Core::TitleBar* controller, KDDockWidgets::Core::View* parent) + : KDDockWidgets::QtWidgets::TitleBar(controller, parent) +{ +} + +void DockTitleBar::mouseDoubleClickEvent(QMouseEvent* ev) +{ + if (g_debugger_window && !g_debugger_window->dockManager().isLayoutLocked()) + KDDockWidgets::QtWidgets::TitleBar::mouseDoubleClickEvent(ev); + else + ev->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::contextMenu); +} + +void DockTabBar::contextMenu(QPoint pos) +{ + auto tab_bar = qobject_cast(sender()); + int tab_index = tab_bar->tabAt(pos); + + // Filter out the placeholder widget displayed when there are no layouts. + if (!hasDebuggerWidget(tab_index)) + return; + + QMenu* menu = new QMenu(tr("Dock Widget Menu"), tab_bar); + + QMenu* set_target_menu = menu->addMenu(tr("Set Target")); + + for (BreakPointCpu cpu : DEBUG_CPUS) + { + const char* cpu_name = DebugInterface::cpuName(cpu); + const char* long_cpu_name = DebugInterface::longCpuName(cpu); + QString text = QString("%1 (%2)").arg(cpu_name).arg(long_cpu_name); + QAction* action = new QAction(text, menu); + connect(action, &QAction::triggered, this, [tab_bar, tab_index, cpu]() { + KDDockWidgets::Core::TabBar* tab_bar_controller = tab_bar->asController(); + if (!tab_bar_controller) + return; + + KDDockWidgets::Core::DockWidget* dock_controller = tab_bar_controller->dockWidgetAt(tab_index); + if (!dock_controller) + return; + + KDDockWidgets::QtWidgets::DockWidget* dock_view = + static_cast(dock_controller->view()); + + DebuggerWidget* widget = qobject_cast(dock_view->widget()); + if (!widget) + return; + + if (!g_debugger_window) + return; + + if (!widget->setCpuOverride(cpu)) + g_debugger_window->dockManager().recreateDebuggerWidget(dock_view->uniqueName()); + + g_debugger_window->dockManager().retranslateDockWidget(dock_controller); + }); + set_target_menu->addAction(action); + } + + set_target_menu->addSeparator(); + + QAction* inherit_action = new QAction(tr("Inherit From Layout"), menu); + connect(inherit_action, &QAction::triggered, this, [tab_bar, tab_index]() { + KDDockWidgets::Core::TabBar* tab_bar_controller = tab_bar->asController(); + if (!tab_bar_controller) + return; + + KDDockWidgets::Core::DockWidget* dock_controller = tab_bar_controller->dockWidgetAt(tab_index); + if (!dock_controller) + return; + + KDDockWidgets::QtWidgets::DockWidget* dock_view = + static_cast(dock_controller->view()); + + DebuggerWidget* widget = qobject_cast(dock_view->widget()); + if (!widget) + return; + + if (!g_debugger_window) + return; + + if (!widget->setCpuOverride(std::nullopt)) + g_debugger_window->dockManager().recreateDebuggerWidget(dock_view->uniqueName()); + + g_debugger_window->dockManager().retranslateDockWidget(dock_controller); + }); + set_target_menu->addAction(inherit_action); + + menu->popup(tab_bar->mapToGlobal(pos)); +} + +bool DockTabBar::hasDebuggerWidget(int tab_index) +{ + KDDockWidgets::Core::TabBar* tab_bar_controller = asController(); + if (!tab_bar_controller) + return false; + + KDDockWidgets::Core::DockWidget* dock_controller = tab_bar_controller->dockWidgetAt(tab_index); + if (!dock_controller) + return false; + + auto dock_view = static_cast(dock_controller->view()); + + DebuggerWidget* widget = qobject_cast(dock_view->widget()); + if (!widget) + return false; + + return true; +} + +void DockTabBar::mouseDoubleClickEvent(QMouseEvent* ev) +{ + if (g_debugger_window && !g_debugger_window->dockManager().isLayoutLocked()) + KDDockWidgets::QtWidgets::TabBar::mouseDoubleClickEvent(ev); + else + ev->ignore(); +} diff --git a/pcsx2-qt/Debugger/Docking/DockViews.h b/pcsx2-qt/Debugger/Docking/DockViews.h new file mode 100644 index 0000000000..12b26edfbd --- /dev/null +++ b/pcsx2-qt/Debugger/Docking/DockViews.h @@ -0,0 +1,89 @@ +// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team +// SPDX-License-Identifier: GPL-3.0+ + +#pragma once + +#include +#include +#include +#include +#include + +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; +}; + +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* ev) 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* ev) override; +}; + +class DockTabBar : public KDDockWidgets::QtWidgets::TabBar +{ + Q_OBJECT + +public: + DockTabBar(KDDockWidgets::Core::TabBar* controller, QWidget* parent = nullptr); + +protected: + void contextMenu(QPoint pos); + bool hasDebuggerWidget(int tab_index); + + void mouseDoubleClickEvent(QMouseEvent* ev) override; +}; diff --git a/pcsx2-qt/Debugger/Docking/LayoutEditorDialog.cpp b/pcsx2-qt/Debugger/Docking/LayoutEditorDialog.cpp new file mode 100644 index 0000000000..cca7a8ab7a --- /dev/null +++ b/pcsx2-qt/Debugger/Docking/LayoutEditorDialog.cpp @@ -0,0 +1,103 @@ +// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team +// SPDX-License-Identifier: GPL-3.0+ + +#include "LayoutEditorDialog.h" + +#include "Debugger/Docking/DockTables.h" + +#include + +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 std::string& 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(QString::fromStdString(name)); + + setupInputWidgets(cpu, {}); + + m_ui.initialStateLabel->hide(); + m_ui.initialStateEditor->hide(); + + onNameChanged(); +} + +std::string LayoutEditorDialog::name() +{ + return m_ui.nameEditor->text().toStdString(); +} + +BreakPointCpu LayoutEditorDialog::cpu() +{ + return static_cast(m_ui.cpuEditor->currentData().toInt()); +} + +LayoutEditorDialog::InitialState LayoutEditorDialog::initial_state() +{ + return m_ui.initialStateEditor->currentData().value(); +} + +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* cpu_name = DebugInterface::cpuName(cpu); + const char* long_cpu_name = DebugInterface::longCpuName(cpu); + QString text = QString("%1 (%2)").arg(cpu_name).arg(long_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_name_validator(m_ui.nameEditor->text().toStdString())) + { + 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); +} diff --git a/pcsx2-qt/Debugger/Docking/LayoutEditorDialog.h b/pcsx2-qt/Debugger/Docking/LayoutEditorDialog.h new file mode 100644 index 0000000000..b684dd5195 --- /dev/null +++ b/pcsx2-qt/Debugger/Docking/LayoutEditorDialog.h @@ -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 + +class LayoutEditorDialog : public QDialog +{ + Q_OBJECT + +public: + using NameValidator = std::function; + + enum CreationMode + { + DEFAULT_LAYOUT, + BLANK_LAYOUT, + CLONE_LAYOUT, + }; + + // Bundles together a creation mode and inital state. + using InitialState = std::pair; + + // Create a "New Layout" dialog. + LayoutEditorDialog(NameValidator name_validator, bool can_clone_current_layout, QWidget* parent = nullptr); + + // Create a "Edit Layout" dialog. + LayoutEditorDialog(const std::string& name, BreakPointCpu cpu, NameValidator name_validator, QWidget* parent = nullptr); + + std::string name(); + BreakPointCpu cpu(); + InitialState initial_state(); + +private: + void setupInputWidgets(BreakPointCpu cpu, bool can_clone_current_layout); + void onNameChanged(); + + Ui::LayoutEditorDialog m_ui; + NameValidator m_name_validator; +}; diff --git a/pcsx2-qt/Debugger/Docking/LayoutEditorDialog.ui b/pcsx2-qt/Debugger/Docking/LayoutEditorDialog.ui new file mode 100644 index 0000000000..cb24336513 --- /dev/null +++ b/pcsx2-qt/Debugger/Docking/LayoutEditorDialog.ui @@ -0,0 +1,115 @@ + + + LayoutEditorDialog + + + + 0 + 0 + 400 + 150 + + + + + + + + + + + + Name + + + + + + + Target + + + + + + + Initial State + + + + + + + + + + + + + + + + + + + + color: red + + + + + + true + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + + + buttonBox + accepted() + LayoutEditorDialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + LayoutEditorDialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/pcsx2-qt/Debugger/Docking/NoLayoutsWidget.cpp b/pcsx2-qt/Debugger/Docking/NoLayoutsWidget.cpp new file mode 100644 index 0000000000..adb47c0e2b --- /dev/null +++ b/pcsx2-qt/Debugger/Docking/NoLayoutsWidget.cpp @@ -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; +} diff --git a/pcsx2-qt/Debugger/Docking/NoLayoutsWidget.h b/pcsx2-qt/Debugger/Docking/NoLayoutsWidget.h new file mode 100644 index 0000000000..4f7637700e --- /dev/null +++ b/pcsx2-qt/Debugger/Docking/NoLayoutsWidget.h @@ -0,0 +1,21 @@ +// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team +// SPDX-License-Identifier: GPL-3.0+ + +#pragma once + +#include "ui_NoLayoutsWidget.h" + +#include + +class NoLayoutsWidget : public QWidget +{ + Q_OBJECT + +public: + NoLayoutsWidget(QWidget* parent = nullptr); + + QPushButton* createDefaultLayoutsButton(); + +private: + Ui::NoLayoutsWidget m_ui; +}; diff --git a/pcsx2-qt/Debugger/Docking/NoLayoutsWidget.ui b/pcsx2-qt/Debugger/Docking/NoLayoutsWidget.ui new file mode 100644 index 0000000000..f727a71b46 --- /dev/null +++ b/pcsx2-qt/Debugger/Docking/NoLayoutsWidget.ui @@ -0,0 +1,97 @@ + + + NoLayoutsWidget + + + + 0 + 0 + 400 + 300 + + + + Form + + + false + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + There are no layouts. + + + Qt::AlignCenter + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Create Default Layouts + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + diff --git a/pcsx2-qt/Debugger/JsonValueWrapper.h b/pcsx2-qt/Debugger/JsonValueWrapper.h new file mode 100644 index 0000000000..28f8cd0ec4 --- /dev/null +++ b/pcsx2-qt/Debugger/JsonValueWrapper.h @@ -0,0 +1,34 @@ +// 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& allocator) + : m_value(value) + , m_allocator(allocator) + { + } + + rapidjson::Value& value() + { + return m_value; + } + + rapidjson::MemoryPoolAllocator& allocator() + { + return m_allocator; + } + +private: + rapidjson::Value& m_value; + rapidjson::MemoryPoolAllocator& m_allocator; +}; diff --git a/pcsx2-qt/Debugger/Memory/MemoryViewWidget.cpp b/pcsx2-qt/Debugger/Memory/MemoryViewWidget.cpp index bbb25a1ec8..46f3f048df 100644 --- a/pcsx2-qt/Debugger/Memory/MemoryViewWidget.cpp +++ b/pcsx2-qt/Debugger/Memory/MemoryViewWidget.cpp @@ -41,7 +41,7 @@ void MemoryViewTable::UpdateSelectedAddress(u32 selected, bool page) } } -void MemoryViewTable::DrawTable(QPainter& painter, const QPalette& palette, s32 height) +void MemoryViewTable::DrawTable(QPainter& painter, const QPalette& palette, s32 height, DebugInterface& cpu) { rowHeight = painter.fontMetrics().height() + 2; const s32 charWidth = painter.fontMetrics().averageCharWidth(); @@ -106,7 +106,7 @@ void MemoryViewTable::DrawTable(QPainter& painter, const QPalette& palette, s32 { case MemoryViewType::BYTE: { - const u8 val = static_cast(m_cpu->read8(thisSegmentsStart, valid)); + const u8 val = static_cast(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) : "??"); @@ -114,7 +114,7 @@ void MemoryViewTable::DrawTable(QPainter& painter, const QPalette& palette, s32 } case MemoryViewType::BYTEHW: { - const u16 val = convertEndian(static_cast(m_cpu->read16(thisSegmentsStart, valid))); + const u16 val = convertEndian(static_cast(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) : "????"); @@ -122,7 +122,7 @@ void MemoryViewTable::DrawTable(QPainter& painter, const QPalette& palette, s32 } case MemoryViewType::WORD: { - const u32 val = convertEndian(m_cpu->read32(thisSegmentsStart, valid)); + const u32 val = convertEndian(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) : "????????"); @@ -130,7 +130,7 @@ void MemoryViewTable::DrawTable(QPainter& painter, const QPalette& palette, s32 } case MemoryViewType::DWORD: { - const u64 val = convertEndian(m_cpu->read64(thisSegmentsStart, valid)); + const u64 val = convertEndian(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) : "????????????????"); @@ -153,7 +153,7 @@ void MemoryViewTable::DrawTable(QPainter& painter, const QPalette& palette, s32 painter.setPen(palette.text().color()); bool valid; - const u8 value = m_cpu->read8(currentRowAddress + j, valid); + const u8 value = cpu.read8(currentRowAddress + j, valid); if (valid) { QChar curChar = QChar::fromLatin1(value); @@ -216,54 +216,54 @@ void MemoryViewTable::SelectAt(QPoint pos) } } -u128 MemoryViewTable::GetSelectedSegment() +u128 MemoryViewTable::GetSelectedSegment(DebugInterface& cpu) { u128 val; switch (displayType) { case MemoryViewType::BYTE: - val.lo = m_cpu->read8(selectedAddress); + val.lo = cpu.read8(selectedAddress); break; case MemoryViewType::BYTEHW: - val.lo = convertEndian(static_cast(m_cpu->read16(selectedAddress & ~1))); + val.lo = convertEndian(static_cast(cpu.read16(selectedAddress & ~1))); break; case MemoryViewType::WORD: - val.lo = convertEndian(m_cpu->read32(selectedAddress & ~3)); + val.lo = convertEndian(cpu.read32(selectedAddress & ~3)); break; case MemoryViewType::DWORD: - val._u64[0] = convertEndian(m_cpu->read64(selectedAddress & ~7)); + val._u64[0] = convertEndian(cpu.read64(selectedAddress & ~7)); break; } return val; } -void MemoryViewTable::InsertIntoSelectedHexView(u8 value) +void MemoryViewTable::InsertIntoSelectedHexView(u8 value, DebugInterface& cpu) { const u8 mask = selectedNibbleHI ? 0x0f : 0xf0; - u8 curVal = m_cpu->read8(selectedAddress) & mask; + u8 curVal = cpu.read8(selectedAddress) & mask; u8 newVal = value << (selectedNibbleHI ? 4 : 0); curVal |= newVal; - Host::RunOnCPUThread([this, address = selectedAddress, cpu = m_cpu, val = curVal] { - cpu->write8(address, val); + Host::RunOnCPUThread([this, address = selectedAddress, &cpu, val = curVal] { + cpu.write8(address, val); QtHost::RunOnUIThread([this] { parent->update(); }); }); } -void MemoryViewTable::InsertAtCurrentSelection(const QString& text) +void MemoryViewTable::InsertAtCurrentSelection(const QString& text, DebugInterface& cpu) { - if (!m_cpu->isValidAddress(selectedAddress)) + 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 = m_cpu, inBytes = input] { + Host::RunOnCPUThread([this, address = selectedAddress, &cpu, inBytes = input] { u32 currAddr = address; for (int i = 0; i < inBytes.size(); i++) { - cpu->write8(currAddr, inBytes[i]); + cpu.write8(currAddr, inBytes[i]); currAddr = nextAddress(currAddr); QtHost::RunOnUIThread([this] { parent->update(); }); } @@ -343,9 +343,9 @@ void MemoryViewTable::BackwardSelection() // We need both key and keychar because `key` is easy to use, but is case insensitive -bool MemoryViewTable::KeyPress(int key, QChar keychar) +bool MemoryViewTable::KeyPress(int key, QChar keychar, DebugInterface& cpu) { - if (!m_cpu->isValidAddress(selectedAddress)) + if (!cpu.isValidAddress(selectedAddress)) return false; bool pressHandled = false; @@ -356,8 +356,8 @@ bool MemoryViewTable::KeyPress(int key, QChar keychar) { if (keyCharIsText || (!keychar.isNonCharacter() && keychar.category() != QChar::Other_Control)) { - Host::RunOnCPUThread([this, address = selectedAddress, cpu = m_cpu, val = keychar.toLatin1()] { - cpu->write8(address, val); + Host::RunOnCPUThread([this, address = selectedAddress, &cpu, val = keychar.toLatin1()] { + cpu.write8(address, val); QtHost::RunOnUIThread([this] { UpdateSelectedAddress(selectedAddress + 1); parent->update(); }); }); pressHandled = true; @@ -367,8 +367,8 @@ bool MemoryViewTable::KeyPress(int key, QChar keychar) { case Qt::Key::Key_Backspace: case Qt::Key::Key_Escape: - Host::RunOnCPUThread([this, address = selectedAddress, cpu = m_cpu] { - cpu->write8(address, 0); + Host::RunOnCPUThread([this, address = selectedAddress, &cpu] { + cpu.write8(address, 0); QtHost::RunOnUIThread([this] {BackwardSelection(); parent->update(); }); }); pressHandled = true; @@ -395,7 +395,7 @@ bool MemoryViewTable::KeyPress(int key, QChar keychar) const u8 keyPressed = static_cast(QString(QChar(key)).toInt(&pressHandled, 16)); if (pressHandled) { - InsertIntoSelectedHexView(keyPressed); + InsertIntoSelectedHexView(keyPressed, cpu); ForwardSelection(); } } @@ -404,7 +404,7 @@ bool MemoryViewTable::KeyPress(int key, QChar keychar) { case Qt::Key::Key_Backspace: case Qt::Key::Key_Escape: - InsertIntoSelectedHexView(0); + InsertIntoSelectedHexView(0, cpu); BackwardSelection(); pressHandled = true; break; @@ -459,7 +459,6 @@ MemoryViewWidget::MemoryViewWidget(DebugInterface& cpu, QWidget* parent) this->setFocusPolicy(Qt::FocusPolicy::ClickFocus); connect(this, &MemoryViewWidget::customContextMenuRequested, this, &MemoryViewWidget::customMenuRequested); - m_table.SetCpu(&cpu); m_table.UpdateStartAddress(0x480000); applyMonospaceFont(); @@ -476,7 +475,7 @@ void MemoryViewWidget::paintEvent(QPaintEvent* event) if (!cpu().isAlive()) return; - m_table.DrawTable(painter, this->palette(), this->height()); + m_table.DrawTable(painter, this->palette(), this->height(), cpu()); } void MemoryViewWidget::mousePressEvent(QMouseEvent* event) @@ -580,7 +579,7 @@ void MemoryViewWidget::contextCopyByte() void MemoryViewWidget::contextCopySegment() { - QApplication::clipboard()->setText(QString::number(m_table.GetSelectedSegment().lo, 16).toUpper()); + QApplication::clipboard()->setText(QString::number(m_table.GetSelectedSegment(cpu()).lo, 16).toUpper()); } void MemoryViewWidget::contextCopyCharacter() @@ -590,7 +589,7 @@ void MemoryViewWidget::contextCopyCharacter() void MemoryViewWidget::contextPaste() { - m_table.InsertAtCurrentSelection(QApplication::clipboard()->text()); + m_table.InsertAtCurrentSelection(QApplication::clipboard()->text(), cpu()); } void MemoryViewWidget::contextGoToAddress() @@ -632,7 +631,7 @@ void MemoryViewWidget::wheelEvent(QWheelEvent* event) void MemoryViewWidget::keyPressEvent(QKeyEvent* event) { - if (!m_table.KeyPress(event->key(), event->text().size() ? event->text()[0] : '\0')) + if (!m_table.KeyPress(event->key(), event->text().size() ? event->text()[0] : '\0', cpu())) { switch (event->key()) { diff --git a/pcsx2-qt/Debugger/Memory/MemoryViewWidget.h b/pcsx2-qt/Debugger/Memory/MemoryViewWidget.h index e410e799a2..ed5831e5d7 100644 --- a/pcsx2-qt/Debugger/Memory/MemoryViewWidget.h +++ b/pcsx2-qt/Debugger/Memory/MemoryViewWidget.h @@ -29,7 +29,6 @@ enum class MemoryViewType class MemoryViewTable { QWidget* parent; - DebugInterface* m_cpu; MemoryViewType displayType = MemoryViewType::BYTE; bool littleEndian = true; u32 rowCount; @@ -46,7 +45,7 @@ class MemoryViewTable bool selectedNibbleHI = false; - void InsertIntoSelectedHexView(u8 value); + void InsertIntoSelectedHexView(u8 value, DebugInterface& cpu); template T convertEndian(T in) @@ -66,24 +65,23 @@ class MemoryViewTable public: MemoryViewTable(QWidget* parent) - : parent(parent){}; + : parent(parent) + { + } + u32 startAddress; u32 selectedAddress; - void SetCpu(DebugInterface* cpu) - { - m_cpu = cpu; - } void UpdateStartAddress(u32 start); void UpdateSelectedAddress(u32 selected, bool page = false); - void DrawTable(QPainter& painter, const QPalette& palette, s32 height); + void DrawTable(QPainter& painter, const QPalette& palette, s32 height, DebugInterface& cpu); void SelectAt(QPoint pos); - u128 GetSelectedSegment(); - void InsertAtCurrentSelection(const QString& text); + 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); + bool KeyPress(int key, QChar keychar, DebugInterface& cpu); MemoryViewType GetViewType() { @@ -106,7 +104,6 @@ public: } }; - class MemoryViewWidget final : public DebuggerWidget { Q_OBJECT diff --git a/pcsx2-qt/MainWindow.cpp b/pcsx2-qt/MainWindow.cpp index 5c12a1b923..8d78b30da3 100644 --- a/pcsx2-qt/MainWindow.cpp +++ b/pcsx2-qt/MainWindow.cpp @@ -12,6 +12,7 @@ #include "QtHost.h" #include "QtUtils.h" #include "SettingWidgetBinder.h" +#include "Debugger/Docking/DockManager.h" #include "Settings/AchievementLoginDialog.h" #include "Settings/ControllerSettingsWindow.h" #include "Settings/GameListSettingsWidget.h" @@ -620,12 +621,7 @@ void MainWindow::quit() void MainWindow::destroySubWindows() { - if (m_debugger_window) - { - m_debugger_window->close(); - m_debugger_window->deleteLater(); - m_debugger_window = nullptr; - } + DebuggerWindow::destroyInstance(); if (m_controller_settings_window) { @@ -850,12 +846,8 @@ void MainWindow::onAchievementsHardcoreModeChanged(bool enabled) { // If PauseOnEntry is enabled, we prompt the user to disable Hardcore Mode // or cancel the action later, so we should keep the debugger around - if (m_debugger_window && !DebugInterface::getPauseOnEntry()) - { - m_debugger_window->close(); - m_debugger_window->deleteLater(); - m_debugger_window = nullptr; - } + if (g_debugger_window && !DebugInterface::getPauseOnEntry()) + DebuggerWindow::destroyInstance(); } } @@ -1145,7 +1137,7 @@ bool MainWindow::shouldMouseLock() const if (!Host::GetBoolSettingValue("EmuCore", "EnableMouseLock", false)) return false; - bool windowsHidden = (!m_debugger_window || m_debugger_window->isHidden()) && + bool windowsHidden = (!g_debugger_window || g_debugger_window->isHidden()) && (!m_controller_settings_window || m_controller_settings_window->isHidden()) && (!m_settings_window || m_settings_window->isHidden()); @@ -1481,7 +1473,7 @@ void MainWindow::onGameListEntryContextMenuRequested(const QPoint& point) connect(action, &QAction::triggered, [this, entry]() { DebugInterface::setPauseOnEntry(true); startGameListEntry(entry); - getDebuggerWindow()->show(); + DebuggerWindow::getInstance()->show(); }); } @@ -1821,10 +1813,10 @@ void MainWindow::updateTheme() { // The debugger hates theme changes. // We have unfortunately to destroy it and recreate it. - const bool debugger_is_open = m_debugger_window ? m_debugger_window->isVisible() : false; - const QSize debugger_size = m_debugger_window ? m_debugger_window->size() : QSize(); - const QPoint debugger_pos = m_debugger_window ? m_debugger_window->pos() : QPoint(); - if (m_debugger_window) + const bool debugger_is_open = g_debugger_window ? g_debugger_window->isVisible() : false; + const QSize debugger_size = g_debugger_window ? g_debugger_window->size() : QSize(); + const QPoint debugger_pos = g_debugger_window ? g_debugger_window->pos() : QPoint(); + if (g_debugger_window) { if (QMessageBox::question(this, tr("Theme Change"), tr("Changing the theme will close the debugger window. Any unsaved data will be lost. Do you want to continue?"), @@ -1837,16 +1829,15 @@ void MainWindow::updateTheme() QtHost::UpdateApplicationTheme(); reloadThemeSpecificImages(); - if (m_debugger_window) + if (g_debugger_window) { - m_debugger_window->deleteLater(); - m_debugger_window = nullptr; - getDebuggerWindow(); // populates m_debugger_window - m_debugger_window->resize(debugger_size); - m_debugger_window->move(debugger_pos); + DebuggerWindow::destroyInstance(); + DebuggerWindow::createInstance(); + g_debugger_window->resize(debugger_size); + g_debugger_window->move(debugger_pos); if (debugger_is_open) { - m_debugger_window->show(); + g_debugger_window->show(); } } } @@ -2779,23 +2770,9 @@ void MainWindow::doSettings(const char* category /* = nullptr */) dlg->setCategory(category); } -DebuggerWindow* MainWindow::getDebuggerWindow() -{ - if (!m_debugger_window) - { - // Setup KDDockWidgets. - DockManager::configure_docking_system(); - - // Don't pass us (this) as the parent, otherwise the window is always on top of the mainwindow (on windows at least) - m_debugger_window = new DebuggerWindow(nullptr); - } - - return m_debugger_window; -} - void MainWindow::openDebugger() { - DebuggerWindow* dwnd = getDebuggerWindow(); + DebuggerWindow* dwnd = DebuggerWindow::getInstance(); dwnd->isVisible() ? dwnd->activateWindow() : dwnd->show(); } diff --git a/pcsx2-qt/MainWindow.h b/pcsx2-qt/MainWindow.h index 7a8f3b2747..8bbcfcd814 100644 --- a/pcsx2-qt/MainWindow.h +++ b/pcsx2-qt/MainWindow.h @@ -260,8 +260,6 @@ private: InputRecordingViewer* getInputRecordingViewer(); void updateInputRecordingActions(bool started); - DebuggerWindow* getDebuggerWindow(); - void doControllerSettings(ControllerSettingsWindow::Category category = ControllerSettingsWindow::Category::Count); QString getDiscDevicePath(const QString& title); @@ -291,8 +289,6 @@ private: InputRecordingViewer* m_input_recording_viewer = nullptr; AutoUpdaterDialog* m_auto_updater_dialog = nullptr; - DebuggerWindow* m_debugger_window = nullptr; - QProgressBar* m_status_progress_widget = nullptr; QLabel* m_status_verbose_widget = nullptr; QLabel* m_status_renderer_widget = nullptr; diff --git a/pcsx2-qt/Settings/DebugAnalysisSettingsWidget.h b/pcsx2-qt/Settings/DebugAnalysisSettingsWidget.h index 410b109aee..cce32395b3 100644 --- a/pcsx2-qt/Settings/DebugAnalysisSettingsWidget.h +++ b/pcsx2-qt/Settings/DebugAnalysisSettingsWidget.h @@ -7,7 +7,6 @@ #include "Config.h" #include -#include class SettingsWindow; diff --git a/pcsx2-qt/pcsx2-qt.vcxproj b/pcsx2-qt/pcsx2-qt.vcxproj index 082edce0e4..96e5e3fa1d 100644 --- a/pcsx2-qt/pcsx2-qt.vcxproj +++ b/pcsx2-qt/pcsx2-qt.vcxproj @@ -47,9 +47,10 @@ %(AdditionalIncludeDirectories);$(SolutionDir)3rdparty\fast_float\include %(AdditionalIncludeDirectories);$(SolutionDir)3rdparty\demangler\include %(AdditionalIncludeDirectories);$(SolutionDir)3rdparty\ccc\src + %(AdditionalIncludeDirectories);$(SolutionDir)3rdparty\rapidjson\include %(AdditionalIncludeDirectories);$(SolutionDir)pcsx2 - %(AdditionalIncludeDirectories);$(ProjectDir)\Settings;$(ProjectDir)\GameList;$(ProjectDir)\Tools\InputRecording;$(ProjectDir)\Debugger;$(ProjectDir)\Debugger\Breakpoints;$(ProjectDir)\Debugger\Memory;$(ProjectDir)\Debugger\SymbolTree + %(AdditionalIncludeDirectories);$(ProjectDir)\Settings;$(ProjectDir)\GameList;$(ProjectDir)\Tools\InputRecording;$(ProjectDir)\Debugger;$(ProjectDir)\Debugger\Breakpoints;$(ProjectDir)\Debugger\Docking;$(ProjectDir)\Debugger\Memory;$(ProjectDir)\Debugger\SymbolTree Use PrecompiledHeader.h PrecompiledHeader.h;%(ForcedIncludeFiles) @@ -113,7 +114,6 @@ - @@ -123,6 +123,13 @@ + + + + + + + @@ -220,7 +227,6 @@ - @@ -230,6 +236,13 @@ + + + + + + + @@ -285,7 +298,6 @@ - @@ -294,6 +306,10 @@ + + + + @@ -433,6 +449,12 @@ Document + + Document + + + Document + Document diff --git a/pcsx2-qt/pcsx2-qt.vcxproj.filters b/pcsx2-qt/pcsx2-qt.vcxproj.filters index 5963e150f7..ec38a431d1 100644 --- a/pcsx2-qt/pcsx2-qt.vcxproj.filters +++ b/pcsx2-qt/pcsx2-qt.vcxproj.filters @@ -281,9 +281,6 @@ Debugger - - Debugger - Debugger @@ -308,6 +305,27 @@ Debugger\Breakpoints + + Debugger\Docking + + + Debugger\Docking + + + Debugger\Docking + + + Debugger\Docking + + + Debugger\Docking + + + Debugger\Docking + + + Debugger\Docking + Debugger\Memory @@ -332,9 +350,6 @@ moc - - moc - moc @@ -359,6 +374,18 @@ moc + + moc + + + moc + + + moc + + + moc + moc @@ -580,9 +607,6 @@ Debugger - - Debugger - Debugger @@ -607,6 +631,27 @@ Debugger\Breakpoints + + Debugger\Docking + + + Debugger\Docking + + + Debugger\Docking + + + Debugger\Docking + + + Debugger\Docking + + + Debugger\Docking + + + Debugger\Docking + Debugger\Memory @@ -752,6 +797,12 @@ Debugger\Breakpoints + + Debugger\Docking + + + Debugger\Docking + Debugger\Memory diff --git a/pcsx2/Config.h b/pcsx2/Config.h index 00da38c6a6..3ce087f897 100644 --- a/pcsx2/Config.h +++ b/pcsx2/Config.h @@ -1374,7 +1374,6 @@ namespace EmuFolders extern std::string AppRoot; extern std::string DataRoot; extern std::string Settings; - extern std::string DebuggerSettings; extern std::string Bios; extern std::string Snapshots; extern std::string Savestates; @@ -1390,6 +1389,8 @@ namespace EmuFolders extern std::string Textures; extern std::string InputProfiles; extern std::string Videos; + extern std::string DebuggerLayouts; + extern std::string DebuggerSettings; /// Initializes critical folders (AppRoot, DataRoot, Settings). Call once on startup. void SetAppRoot(); diff --git a/pcsx2/DebugTools/DebugInterface.cpp b/pcsx2/DebugTools/DebugInterface.cpp index 189a9b5f43..99b4b7ab8f 100644 --- a/pcsx2/DebugTools/DebugInterface.cpp +++ b/pcsx2/DebugTools/DebugInterface.cpp @@ -158,6 +158,57 @@ bool DebugInterface::parseExpression(PostfixExpression& exp, u64& dest, std::str return parsePostfixExpression(exp, &funcs, dest, error); } +DebugInterface& DebugInterface::get(BreakPointCpu cpu) +{ + switch (cpu) + { + case BREAKPOINT_EE: + return r5900Debug; + case BREAKPOINT_IOP: + return r3000Debug; + default: + { + } + } + + pxFailRel("DebugInterface::get called with invalid cpu enum."); + return r5900Debug; +} + +const char* DebugInterface::cpuName(BreakPointCpu cpu) +{ + switch (cpu) + { + case BREAKPOINT_EE: + return "EE"; + case BREAKPOINT_IOP: + return "IOP"; + default: + { + } + } + + pxFailRel("DebugInterface::cpuName called with invalid cpu enum."); + return ""; +} + +const char* DebugInterface::longCpuName(BreakPointCpu cpu) +{ + switch (cpu) + { + case BREAKPOINT_EE: + return TRANSLATE("DebugInterface", "Emotion Engine"); + case BREAKPOINT_IOP: + return TRANSLATE("DebugInteface", "Input Output Processor"); + default: + { + } + } + + pxFailRel("DebugInterface::longCpuName called with invalid cpu enum."); + return ""; +} + // // R5900DebugInterface // diff --git a/pcsx2/DebugTools/DebugInterface.h b/pcsx2/DebugTools/DebugInterface.h index 3e89473aa8..170e5dc4da 100644 --- a/pcsx2/DebugTools/DebugInterface.h +++ b/pcsx2/DebugTools/DebugInterface.h @@ -33,6 +33,11 @@ enum BreakPointCpu BREAKPOINT_IOP_AND_EE = 0x03 }; +inline std::vector DEBUG_CPUS = { + BREAKPOINT_EE, + BREAKPOINT_IOP, +}; + class MemoryReader { public: @@ -86,9 +91,6 @@ public: virtual SymbolImporter* GetSymbolImporter() const = 0; virtual std::vector> GetThreadList() const = 0; - bool evaluateExpression(const char* expression, u64& dest, std::string& error); - bool initExpression(const char* exp, PostfixExpression& dest, std::string& error); - bool parseExpression(PostfixExpression& exp, u64& dest, std::string& error); bool isAlive(); bool isCpuPaused(); void pauseCpu(); @@ -98,9 +100,17 @@ public: std::optional getCallerStackPointer(const ccc::Function& currentFunction); std::optional getStackFrameSize(const ccc::Function& currentFunction); + bool evaluateExpression(const char* expression, u64& dest, std::string& error); + bool initExpression(const char* exp, PostfixExpression& dest, std::string& error); + bool parseExpression(PostfixExpression& exp, u64& dest, std::string& error); + static void setPauseOnEntry(bool pauseOnEntry) { m_pause_on_entry = pauseOnEntry; }; static bool getPauseOnEntry() { return m_pause_on_entry; } + static DebugInterface& get(BreakPointCpu cpu); + static const char* cpuName(BreakPointCpu cpu); + static const char* longCpuName(BreakPointCpu cpu); + private: static bool m_pause_on_entry; }; diff --git a/pcsx2/Pcsx2Config.cpp b/pcsx2/Pcsx2Config.cpp index a73fa8de98..21b30eac04 100644 --- a/pcsx2/Pcsx2Config.cpp +++ b/pcsx2/Pcsx2Config.cpp @@ -152,6 +152,7 @@ namespace EmuFolders std::string AppRoot; std::string DataRoot; std::string Settings; + std::string DebuggerLayouts; std::string DebuggerSettings; std::string Bios; std::string Snapshots; @@ -2245,6 +2246,8 @@ void EmuFolders::SetDefaults(SettingsInterface& si) si.SetStringValue("Folders", "Textures", "textures"); si.SetStringValue("Folders", "InputProfiles", "inputprofiles"); si.SetStringValue("Folders", "Videos", "videos"); + si.SetStringValue("Folders", "DebuggerLayouts", "debuggerlayouts"); + si.SetStringValue("Folders", "DebuggerSettings", "debuggersettings"); } static std::string LoadPathFromSettings(SettingsInterface& si, const std::string& root, const char* name, const char* def) @@ -2271,6 +2274,7 @@ void EmuFolders::LoadConfig(SettingsInterface& si) Textures = LoadPathFromSettings(si, DataRoot, "Textures", "textures"); InputProfiles = LoadPathFromSettings(si, DataRoot, "InputProfiles", "inputprofiles"); Videos = LoadPathFromSettings(si, DataRoot, "Videos", "videos"); + DebuggerLayouts = LoadPathFromSettings(si, Settings, "DebuggerLayouts", "debuggerlayouts"); DebuggerSettings = LoadPathFromSettings(si, Settings, "DebuggerSettings", "debuggersettings"); Console.WriteLn("BIOS Directory: %s", Bios.c_str()); @@ -2288,6 +2292,7 @@ void EmuFolders::LoadConfig(SettingsInterface& si) Console.WriteLn("Textures Directory: %s", Textures.c_str()); Console.WriteLn("Input Profile Directory: %s", InputProfiles.c_str()); Console.WriteLn("Video Dumping Directory: %s", Videos.c_str()); + Console.WriteLn("Debugger Layouts Directory: %s", DebuggerLayouts.c_str()); Console.WriteLn("Debugger Settings Directory: %s", DebuggerSettings.c_str()); } @@ -2304,11 +2309,12 @@ bool EmuFolders::EnsureFoldersExist() result = FileSystem::CreateDirectoryPath(Covers.c_str(), false) && result; result = FileSystem::CreateDirectoryPath(GameSettings.c_str(), false) && result; result = FileSystem::CreateDirectoryPath(UserResources.c_str(), false) && result; - result = FileSystem::CreateDirectoryPath(DebuggerSettings.c_str(), false) && result; result = FileSystem::CreateDirectoryPath(Cache.c_str(), false) && result; result = FileSystem::CreateDirectoryPath(Textures.c_str(), false) && result; result = FileSystem::CreateDirectoryPath(InputProfiles.c_str(), false) && result; result = FileSystem::CreateDirectoryPath(Videos.c_str(), false) && result; + result = FileSystem::CreateDirectoryPath(DebuggerLayouts.c_str(), false) && result; + result = FileSystem::CreateDirectoryPath(DebuggerSettings.c_str(), false) && result; return result; }