From ef513608f6ee86ac7f0a26ce2a61912d4f943538 Mon Sep 17 00:00:00 2001 From: chaoticgd <43898262+chaoticgd@users.noreply.github.com> Date: Mon, 27 Jan 2025 12:49:39 +0000 Subject: [PATCH] Debugger: Add support for multiple UI layouts --- pcsx2-qt/CMakeLists.txt | 17 +- pcsx2-qt/Debugger/DebuggerWidget.cpp | 65 ++- pcsx2-qt/Debugger/DebuggerWidget.h | 28 +- pcsx2-qt/Debugger/DebuggerWindow.cpp | 25 +- pcsx2-qt/Debugger/DebuggerWindow.h | 10 +- pcsx2-qt/Debugger/DebuggerWindow.ui | 6 - pcsx2-qt/Debugger/DockManager.cpp | 108 ---- pcsx2-qt/Debugger/DockManager.h | 42 -- pcsx2-qt/Debugger/Docking/DockLayout.cpp | 540 ++++++++++++++++++ pcsx2-qt/Debugger/Docking/DockLayout.h | 110 ++++ pcsx2-qt/Debugger/Docking/DockManager.cpp | 452 +++++++++++++++ pcsx2-qt/Debugger/Docking/DockManager.h | 91 +++ pcsx2-qt/Debugger/Docking/DockTables.cpp | 102 ++++ pcsx2-qt/Debugger/Docking/DockTables.h | 79 +++ pcsx2-qt/Debugger/Docking/DockViews.cpp | 146 +++++ pcsx2-qt/Debugger/Docking/DockViews.h | 30 + .../Debugger/Docking/LayoutEditorDialog.cpp | 98 ++++ .../Debugger/Docking/LayoutEditorDialog.h | 45 ++ .../Debugger/Docking/LayoutEditorDialog.ui | 115 ++++ pcsx2-qt/Debugger/Docking/NoLayoutsWidget.cpp | 15 + pcsx2-qt/Debugger/Docking/NoLayoutsWidget.h | 21 + pcsx2-qt/Debugger/Docking/NoLayoutsWidget.ui | 97 ++++ pcsx2-qt/Debugger/JsonValueWrapper.h | 34 ++ pcsx2-qt/Debugger/Memory/MemoryViewWidget.cpp | 63 +- pcsx2-qt/Debugger/Memory/MemoryViewWidget.h | 21 +- pcsx2-qt/MainWindow.cpp | 45 +- pcsx2-qt/MainWindow.h | 2 - .../Settings/DebugAnalysisSettingsWidget.h | 1 - pcsx2-qt/Settings/DebugSettingsWidget.ui | 11 +- pcsx2-qt/pcsx2-qt.vcxproj | 28 +- pcsx2-qt/pcsx2-qt.vcxproj.filters | 63 +- pcsx2/Config.h | 3 +- pcsx2/DebugTools/DebugInterface.cpp | 34 ++ pcsx2/DebugTools/DebugInterface.h | 15 +- pcsx2/Pcsx2Config.cpp | 8 +- 35 files changed, 2309 insertions(+), 261 deletions(-) delete mode 100644 pcsx2-qt/Debugger/DockManager.cpp delete mode 100644 pcsx2-qt/Debugger/DockManager.h create mode 100644 pcsx2-qt/Debugger/Docking/DockLayout.cpp create mode 100644 pcsx2-qt/Debugger/Docking/DockLayout.h create mode 100644 pcsx2-qt/Debugger/Docking/DockManager.cpp create mode 100644 pcsx2-qt/Debugger/Docking/DockManager.h create mode 100644 pcsx2-qt/Debugger/Docking/DockTables.cpp create mode 100644 pcsx2-qt/Debugger/Docking/DockTables.h create mode 100644 pcsx2-qt/Debugger/Docking/DockViews.cpp create mode 100644 pcsx2-qt/Debugger/Docking/DockViews.h create mode 100644 pcsx2-qt/Debugger/Docking/LayoutEditorDialog.cpp create mode 100644 pcsx2-qt/Debugger/Docking/LayoutEditorDialog.h create mode 100644 pcsx2-qt/Debugger/Docking/LayoutEditorDialog.ui create mode 100644 pcsx2-qt/Debugger/Docking/NoLayoutsWidget.cpp create mode 100644 pcsx2-qt/Debugger/Docking/NoLayoutsWidget.h create mode 100644 pcsx2-qt/Debugger/Docking/NoLayoutsWidget.ui create mode 100644 pcsx2-qt/Debugger/JsonValueWrapper.h diff --git a/pcsx2-qt/CMakeLists.txt b/pcsx2-qt/CMakeLists.txt index 1bdff961fc48e3..7a68f276b0ea8b 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,20 @@ 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/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 c4d2148ba50aa2..0a4fa83af4b226 100644 --- a/pcsx2-qt/Debugger/DebuggerWidget.cpp +++ b/pcsx2-qt/Debugger/DebuggerWidget.cpp @@ -3,18 +3,65 @@ #include "DebuggerWidget.h" +#include "JsonValueWrapper.h" + +#include "DebugTools/DebugInterface.h" + #include "common/Assertions.h" -DebuggerWidget::DebuggerWidget(DebugInterface* cpu, QWidget* parent) - : QWidget(parent) - , m_cpu(cpu) +DebugInterface& DebuggerWidget::cpu() const { + 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; } -DebugInterface& DebuggerWidget::cpu() const +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) +{ + rapidjson::Value cpu_name; + if (m_cpu_override) + { + switch (*m_cpu_override) + { + case BREAKPOINT_EE: + cpu_name.SetString("EE"); + break; + case BREAKPOINT_IOP: + cpu_name.SetString("IOP"); + break; + default: + return; + } + } + + json.value().AddMember("target", cpu_name, json.allocator()); +} + +void DebuggerWidget::fromJson(JsonValueWrapper& json) { - pxAssertRel(m_cpu, "DebuggerWidget::cpu() called on object that doesn't have a CPU type set."); - return *m_cpu; } void DebuggerWidget::applyMonospaceFont() @@ -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 53fec8c4b4f3f0..0e7be59ae9f28b 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 void 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 47be09fc85b958..deeab0756c347b 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,18 @@ #include "MainWindow.h" #include "AnalysisOptionsDialog.h" +DebuggerWindow* g_debugger_window = nullptr; + DebuggerWindow::DebuggerWindow(QWidget* parent) : KDDockWidgets::QtWidgets::MainWindow(QStringLiteral("DebuggerWindow"), {}, parent) - , m_dock_manager(this) { m_ui.setupUi(this); + // This needs to be set before we create the dock manager. + g_debugger_window = this; + + m_dock_manager = new DockManager(this); + 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); @@ -37,15 +45,28 @@ DebuggerWindow::DebuggerWindow(QWidget* parent) //m_ui.cpuTabs->addTab(m_cpuWidget_r5900, "R5900"); //m_ui.cpuTabs->addTab(m_cpuWidget_r3000, "R3000"); - m_dock_manager.switchToLayout(0); + m_dock_manager->switchToLayout(0); //QTabBar* tabs = new QTabBar(); //tabs->addTab("Test"); //m_ui.menuBar->layout()->addWidget(tabs); + + QMenuBar* menu_bar = menuBar(); + + setMenuWidget(m_dock_manager->createLayoutSwitcher(menu_bar)); + + connect(m_ui.menuWindows, &QMenu::aboutToShow, this, [this]() { + m_dock_manager->createWindowsMenu(m_ui.menuWindows); + }); } DebuggerWindow::~DebuggerWindow() = default; +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 void DebuggerWindow::setTabActiveStyle(BreakPointCpu enabledCpu) diff --git a/pcsx2-qt/Debugger/DebuggerWindow.h b/pcsx2-qt/Debugger/DebuggerWindow.h index 65df2dd71a44ef..8c97ae1c0c2986 100644 --- a/pcsx2-qt/Debugger/DebuggerWindow.h +++ b/pcsx2-qt/Debugger/DebuggerWindow.h @@ -5,10 +5,12 @@ #include "ui_DebuggerWindow.h" -#include "DockManager.h" +#include "DebugTools/DebugInterface.h" #include +class DockManager; + class DebuggerWindow : public KDDockWidgets::QtWidgets::MainWindow { Q_OBJECT @@ -17,6 +19,8 @@ class DebuggerWindow : public KDDockWidgets::QtWidgets::MainWindow DebuggerWindow(QWidget* parent); ~DebuggerWindow(); + DockManager& dockManager(); + public slots: void onVMStateChanged(); void onRunPause(); @@ -36,7 +40,9 @@ public slots: 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 c0bd9b948701d9..a406de537d83a5 100644 --- a/pcsx2-qt/Debugger/DebuggerWindow.ui +++ b/pcsx2-qt/Debugger/DebuggerWindow.ui @@ -77,11 +77,6 @@ Windows - - - Layouts - - View @@ -92,7 +87,6 @@ - diff --git a/pcsx2-qt/Debugger/DockManager.cpp b/pcsx2-qt/Debugger/DockManager.cpp deleted file mode 100644 index 409c3cece87b7b..00000000000000 --- 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 430908a8a14b3c..00000000000000 --- 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 00000000000000..75f88b81d6fe4c --- /dev/null +++ b/pcsx2-qt/Debugger/Docking/DockLayout.cpp @@ -0,0 +1,540 @@ +// 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/FileSystem.h" +#include "common/Path.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "rapidjson/document.h" +#include "rapidjson/prettywriter.h" + +DockLayout::DockLayout( + std::string name, BreakPointCpu cpu, const DockTables::DefaultDockLayout& default_layout, Index index) + : m_name(name) + , m_cpu(cpu) + , 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, Index index) + : m_name(name) + , m_cpu(cpu) +{ + save(index); +} + +DockLayout::DockLayout(std::string name, BreakPointCpu cpu, const DockLayout& layout_to_clone, Index index) + : m_name(name) + , m_cpu(cpu) +{ + 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); + } + + m_geometry = layout_to_clone.m_geometry; + m_base_layout = layout_to_clone.m_base_layout; + + save(index); +} + +DockLayout::DockLayout(const std::string& path, Index index) +{ + load(path); +} + +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; +} + +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; + + KDDockWidgets::LayoutSaver saver(KDDockWidgets::RestoreOption_RelativeToMainWindow); + + // Store the geometry of all the dock widgets as JSON. + 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. + populateDefaultLayout(window); + retranslateDockWidgets(); + return; + } + + // Create any dock widgets that were previously frozen during this session. + for (auto& [unique_name, widget] : m_widgets) + { + pxAssert(widget.get()); + + auto view = static_cast( + KDDockWidgets::Config::self().viewFactory()->createDockWidget(unique_name)); + view->setWidget(widget); + window->addDockWidget(view, KDDockWidgets::Location_OnBottom); + } + + m_restoring_layout = true; + + // Restore the geometry of the dock widgets we just recreated. + if (!saver.restoreLayout(m_geometry)) + { + m_restoring_layout = false; + + 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; + } + + // We failed to restore the geometry, so just setup the default layout. + populateDefaultLayout(window); + } + + m_restoring_layout = false; + + retranslateDockWidgets(); +} + +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(dock_widget->uniqueName()); + 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 (m_restoring_layout) + return; + + auto debugger_widget_iterator = m_widgets.find(dock_widget->uniqueName()); + if (debugger_widget_iterator == m_widgets.end()) + return; + + KDDockWidgets::Vector names{dock_widget->uniqueName()}; + KDDockWidgets::Vector dock_widgets = + KDDockWidgets::DockRegistry::self()->dockWidgets(names); + + m_widgets.erase(debugger_widget_iterator); + dock_widgets[0]->deleteLater(); +} + +bool DockLayout::hasDebuggerWidget(QString unique_name) +{ + return m_widgets.find(unique_name) != m_widgets.end(); +} + +void DockLayout::toggleDebuggerWidget(QString unique_name, DebuggerWindow* window) +{ + auto debugger_widget_iterator = m_widgets.find(unique_name); + + KDDockWidgets::Vector names{unique_name}; + KDDockWidgets::Vector dock_widgets = + KDDockWidgets::DockRegistry::self()->dockWidgets(names); + + if (debugger_widget_iterator == m_widgets.end()) + { + // Create the dock widget. + if (!dock_widgets.empty()) + 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; + } + + insertDockWidgetAtPreferredLocation(controller, description.preferred_location, window); + retranslateDockWidget(controller); + } + else + { + // Delete the dock widget. + if (dock_widgets.size() != 1) + return; + + m_widgets.erase(debugger_widget_iterator); + delete dock_widgets[0]; + } +} + +void DockLayout::recreateDebuggerWidget(QString unique_name) +{ + pxAssert(!m_is_frozen); + + auto debugger_widget_iterator = m_widgets.find(unique_name); + if (debugger_widget_iterator == m_widgets.end()) + return; + + KDDockWidgets::Vector names{unique_name}; + KDDockWidgets::Vector dock_widgets = + KDDockWidgets::DockRegistry::self()->dockWidgets(names); + if (dock_widgets.size() != 1) + return; + + auto description_iterator = DockTables::DEBUGGER_WIDGETS.find(unique_name); + if (description_iterator == DockTables::DEBUGGER_WIDGETS.end()) + return; + + DebuggerWidget* old_debugger_widget = debugger_widget_iterator->second; + KDDockWidgets::Core::DockWidget* dock_controller = dock_widgets[0]; + const DockTables::DebuggerWidgetDescription& description = description_iterator->second; + + auto dock_view = static_cast(dock_controller->view()); + pxAssert(dock_view->widget() == old_debugger_widget); + + 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; + + dock_view->setWidget(new_debugger_widget); + + delete old_debugger_widget; +} + +void DockLayout::deleteFile() +{ + if (m_layout_file_path.empty()) + return; + + FileSystem::DeleteFilePath(m_layout_file_path.c_str()); +} + +bool DockLayout::save(size_t layout_index) +{ + // Serialize the layout as JSON. + rapidjson::Document json(rapidjson::kObjectType); + rapidjson::Document geometry; + + json.AddMember("format", "PCSX2 Debugger User Interface Layout", json.GetAllocator()); + json.AddMember("version", DockTables::DEBUGGER_LAYOUT_FILE_VERSION, json.GetAllocator()); + + rapidjson::Value name; + name.SetString(m_name.c_str(), strlen(m_name.c_str())); + json.AddMember("name", name, json.GetAllocator()); + + json.AddMember("index", static_cast(layout_index), json.GetAllocator()); + + rapidjson::Value widgets(rapidjson::kArrayType); + for (auto& [unique_name, widget] : m_widgets) + { + pxAssert(widget.get()); + + rapidjson::Value object(rapidjson::kObjectType); + + 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()); + + if (!m_base_layout.empty()) + { + rapidjson::Value base_layout; + base_layout.SetString(m_base_layout.c_str(), m_base_layout.size()); + json.AddMember("base_layout", base_layout, json.GetAllocator()); + } + + rapidjson::StringBuffer string_buffer; + rapidjson::PrettyWriter writer(string_buffer); + json.Accept(writer); + + std::string safe_name = Path::SanitizeFileName(m_name); + + // Write out the JSON to a file. + std::string temp_file_path = Path::Combine(EmuFolders::DebuggerLayouts, safe_name + ".tmp"); + + if (!FileSystem::WriteStringToFile(temp_file_path.c_str(), string_buffer.GetString())) + return false; + + // Generate a name if a file doesn't already exist. + std::string file_path = Path::Combine(EmuFolders::DebuggerLayouts, safe_name + ".json"); + + if (!FileSystem::RenamePath(temp_file_path.c_str(), 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) + FileSystem::DeleteFilePath(m_layout_file_path.c_str()); + + m_layout_file_path = std::move(file_path); + + return true; +} + +void DockLayout::load(const std::string& path) +{ +} + +void DockLayout::populateDefaultLayout(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); +} + +void DockLayout::insertDockWidgetAtPreferredLocation( + KDDockWidgets::Core::DockWidget* dock_widget, + DockTables::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 DockTables::TOP_LEFT: + preferred_location = {0, 0}; + break; + case DockTables::TOP_MIDDLE: + preferred_location = {half_width, 0}; + break; + case DockTables::TOP_RIGHT: + preferred_location = {width, 0}; + break; + case DockTables::MIDDLE_LEFT: + preferred_location = {0, half_height}; + break; + case DockTables::MIDDLE_MIDDLE: + preferred_location = {half_width, half_height}; + break; + case DockTables::MIDDLE_RIGHT: + preferred_location = {width, half_height}; + break; + case DockTables::BOTTOM_LEFT: + preferred_location = {0, height}; + break; + case DockTables::BOTTOM_MIDDLE: + preferred_location = {half_width, height}; + break; + case DockTables::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()) + { + 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); + } +} diff --git a/pcsx2-qt/Debugger/Docking/DockLayout.h b/pcsx2-qt/Debugger/Docking/DockLayout.h new file mode 100644 index 00000000000000..2dddcc7941acce --- /dev/null +++ b/pcsx2-qt/Debugger/Docking/DockLayout.h @@ -0,0 +1,110 @@ +// 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; + +class DockLayout +{ +public: + using Index = size_t; + static const constexpr Index INVALID_INDEX = SIZE_MAX; + + // Create a new default layout. + DockLayout(std::string name, BreakPointCpu cpu, const DockTables::DefaultDockLayout& default_layout, Index index); + + // Create a new blank layout. + DockLayout(std::string name, BreakPointCpu cpu, Index index); + + // Clone an existing layout. + DockLayout(std::string name, BreakPointCpu cpu, const DockLayout& layout_to_clone, Index index); + + // Load a layout from a file. + DockLayout(const std::string& path, 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); + + // 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); + + 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(size_t layout_index); + +private: + void load(const std::string& path); + + void populateDefaultLayout(DebuggerWindow* window); + + void insertDockWidgetAtPreferredLocation( + KDDockWidgets::Core::DockWidget* dock_widget, + DockTables::PreferredLocation location, + KDDockWidgets::QtWidgets::MainWindow* 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; + + // 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 name of the default layout which this layout was based on. This will + // be used if the m_geometry variable above is empty. + std::string m_base_layout; + + // The 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; + + // Are we in LayoutSaver::restoreLayout? + bool m_restoring_layout = false; +}; diff --git a/pcsx2-qt/Debugger/Docking/DockManager.cpp b/pcsx2-qt/Debugger/Docking/DockManager.cpp new file mode 100644 index 00000000000000..d2851a58887632 --- /dev/null +++ b/pcsx2-qt/Debugger/Docking/DockManager.cpp @@ -0,0 +1,452 @@ +// 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 + +DockManager::DockManager(QObject* parent) + : QObject(parent) +{ + loadLayouts(); +} + +DockManager::~DockManager() +{ + switchToLayout(DockLayout::INVALID_INDEX); +} + +void DockManager::configureDockingSystem() +{ + KDDockWidgets::Config::self().setFlags( + KDDockWidgets::Config::Flag_HideTitleBarWhenTabsVisible | + KDDockWidgets::Config::Flag_AlwaysShowTabs | + KDDockWidgets::Config::Flag_AllowReorderTabs | + KDDockWidgets::Config::Flag_TabsHaveCloseButton | + KDDockWidgets::Config::Flag_TitleBarIsFocusable); + + KDDockWidgets::Config::self().setViewFactory(new DockViewFactory()); + KDDockWidgets::Config::self().setDragAboutToStartFunc(&DockManager::dragAboutToStart); +} + +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::setupDefaultLayouts); + + 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); + + //for (const FILESYSTEM_FIND_DATA& ffd : files) + // loadLayout(ffd.FileName); + + if (m_layouts.empty()) + setupDefaultLayouts(); +} + +bool DockManager::saveLayouts() +{ + for (DockLayout::Index i = 0; i < m_layouts.size(); i++) + if (!m_layouts[i].save(i)) + return false; + + return true; +} + +void DockManager::setupDefaultLayouts() +{ + 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, layout); + + switchToLayout(0); + + updateLayoutSwitcher(); +} + +void DockManager::createWindowsMenu(QMenu* menu) +{ + menu->clear(); + + QAction* reset_all_layouts_action = new QAction(tr("Reset All Layouts"), menu); + connect(reset_all_layouts_action, &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) + setupDefaultLayouts(); + }); + menu->addAction(reset_all_layouts_action); + + if (m_current_layout == DockLayout::INVALID_INDEX) + return; + + DockLayout& layout = m_layouts.at(m_current_layout); + + menu->addSeparator(); + + 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) { + m_layout_locked = 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(s32 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) { + std::string safe_name = Path::SanitizeFileName(name); + for (DockLayout& layout : m_layouts) + if (StringUtil::compareNoCase(Path::SanitizeFileName(layout.name()), safe_name)) + return false; + + return true; + }; + + 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(), default_layout); + break; + } + case LayoutEditorDialog::BLANK_LAYOUT: + { + new_layout = createLayout(dialog->name(), dialog->cpu()); + 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(), 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(s32 from, s32 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) { + 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 false; + + return true; + }; + + 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; + + deleteLayout(layout_index); + updateLayoutSwitcher(); + }); + menu->addAction(delete_action); + + menu->popup(m_switcher->mapToGlobal(pos)); +} + +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; +} + +bool DockManager::dragAboutToStart(KDDockWidgets::Core::Draggable* draggable) +{ + if (!g_debugger_window) + return false; + + return !g_debugger_window->dockManager().isLayoutLocked(); +} + +DockManager* m_dock_manager = nullptr; diff --git a/pcsx2-qt/Debugger/Docking/DockManager.h b/pcsx2-qt/Debugger/Docking/DockManager.h new file mode 100644 index 00000000000000..e8fb44eb9ce1a0 --- /dev/null +++ b/pcsx2-qt/Debugger/Docking/DockManager.h @@ -0,0 +1,91 @@ +// 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 + +namespace KDDockWidgets::Core +{ + class Draggable; +}; + +class DockManager : public QObject +{ + Q_OBJECT + +public: + DockManager(QObject* parent = nullptr); + ~DockManager(); + + DockManager(const DockManager& rhs) = delete; + DockManager& operator=(const DockManager& rhs) = delete; + + DockManager(DockManager&& rhs) = delete; + DockManager& operator=(DockManager&&) = delete; + + 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(); + + void setupDefaultLayouts(); + + void createWindowsMenu(QMenu* menu); + + QWidget* createLayoutSwitcher(QWidget* menu_bar); + void updateLayoutSwitcher(); + void layoutSwitcherTabChanged(s32 index); + void layoutSwitcherTabMoved(s32 from, s32 to); + void layoutSwitcherContextMenu(QPoint pos); + + void retranslateDockWidget(KDDockWidgets::Core::DockWidget* dock_widget); + void dockWidgetClosed(KDDockWidgets::Core::DockWidget* dock_widget); + + void recreateDebuggerWidget(QString unique_name); + + bool isLayoutLocked(); + +private: + static bool dragAboutToStart(KDDockWidgets::Core::Draggable* draggable); + + std::vector m_layouts; + DockLayout::Index m_current_layout = DockLayout::INVALID_INDEX; + + QTabBar* m_switcher = nullptr; + s32 m_plus_tab_index = -1; + s32 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 00000000000000..096d1dada46f77 --- /dev/null +++ b/pcsx2-qt/Debugger/Docking/DockTables.cpp @@ -0,0 +1,102 @@ +// 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" + +using namespace DockTables; + +// IMPORTANT: Bump this whenever you change any of the tables below so that when +// a user updates from an older version they get their layouts reset. +const u32 DockTables::DEBUGGER_LAYOUT_FILE_VERSION = 1; + +#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}, + }, + }, +}; diff --git a/pcsx2-qt/Debugger/Docking/DockTables.h b/pcsx2-qt/Debugger/Docking/DockTables.h new file mode 100644 index 00000000000000..c031196fe48057 --- /dev/null +++ b/pcsx2-qt/Debugger/Docking/DockTables.h @@ -0,0 +1,79 @@ +// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team +// SPDX-License-Identifier: GPL-3.0+ + +#pragma once + +#include "DebugTools/DebugInterface.h" + +#include + +#include + +class DebugInterface; +class DebuggerWidget; + +namespace DockTables +{ + // IMPORTANT: Bump this whenever you change any of the tables below so that + // when a user updates from an older version they get their layouts reset. + extern const u32 DEBUGGER_LAYOUT_FILE_VERSION; + + enum PreferredLocation + { + TOP_LEFT, + TOP_MIDDLE, + TOP_RIGHT, + MIDDLE_LEFT, + MIDDLE_MIDDLE, + MIDDLE_RIGHT, + BOTTOM_LEFT, + BOTTOM_MIDDLE, + BOTTOM_RIGHT + }; + + 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. + 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; +} // namespace DockTables diff --git a/pcsx2-qt/Debugger/Docking/DockViews.cpp b/pcsx2-qt/Debugger/Docking/DockViews.cpp new file mode 100644 index 00000000000000..069227779f613a --- /dev/null +++ b/pcsx2-qt/Debugger/Docking/DockViews.cpp @@ -0,0 +1,146 @@ +// 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& uniqueName, + KDDockWidgets::DockWidgetOptions options, + KDDockWidgets::LayoutSaverOptions layoutSaverOptions, + Qt::WindowFlags windowFlags) const +{ + auto view = new KDDockWidgets::QtWidgets::DockWidget(uniqueName, options, layoutSaverOptions, windowFlags); + connect(view, &KDDockWidgets::QtWidgets::DockWidget::isOpenChanged, this, &DockViewFactory::dockWidgetOpenStateChanged); + return view; +} + +void DockViewFactory::dockWidgetOpenStateChanged(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); +} + +KDDockWidgets::Core::View* DockViewFactory::createTabBar( + KDDockWidgets::Core::TabBar* tabBar, + KDDockWidgets::Core::View* parent) const +{ + auto view = new KDDockWidgets::QtWidgets::TabBar(tabBar, KDDockWidgets::QtCommon::View_qt::asQWidget(parent)); + view->setContextMenuPolicy(Qt::CustomContextMenu); + connect( + view, + &KDDockWidgets::QtWidgets::TabBar::customContextMenuRequested, + this, + &DockViewFactory::tabBarContextMenu); + return view; +} + +void DockViewFactory::tabBarContextMenu(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_bar, 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) + { + QAction* action = new QAction(DebugInterface::cpuName(cpu), 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 DockViewFactory::hasDebuggerWidget(KDDockWidgets::QtWidgets::TabBar* tab_bar, int tab_index) +{ + KDDockWidgets::Core::TabBar* tab_bar_controller = tab_bar->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; +} diff --git a/pcsx2-qt/Debugger/Docking/DockViews.h b/pcsx2-qt/Debugger/Docking/DockViews.h new file mode 100644 index 00000000000000..daa677a7acee9a --- /dev/null +++ b/pcsx2-qt/Debugger/Docking/DockViews.h @@ -0,0 +1,30 @@ +// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team +// SPDX-License-Identifier: GPL-3.0+ + +#pragma once + +#include +#include + +class DockManager; + +class DockViewFactory : public KDDockWidgets::QtWidgets::ViewFactory +{ + Q_OBJECT + +public: + KDDockWidgets::Core::View* createDockWidget( + const QString& uniqueName, + KDDockWidgets::DockWidgetOptions options = {}, + KDDockWidgets::LayoutSaverOptions layoutSaverOptions = {}, + Qt::WindowFlags windowFlags = {}) const override; + + void dockWidgetOpenStateChanged(bool open); + + KDDockWidgets::Core::View* createTabBar( + KDDockWidgets::Core::TabBar* tabBar, + KDDockWidgets::Core::View* parent) const override; + + void tabBarContextMenu(QPoint pos); + bool hasDebuggerWidget(KDDockWidgets::QtWidgets::TabBar* tab_bar, int tab_index); +}; diff --git a/pcsx2-qt/Debugger/Docking/LayoutEditorDialog.cpp b/pcsx2-qt/Debugger/Docking/LayoutEditorDialog.cpp new file mode 100644 index 00000000000000..ec7c4892acacfd --- /dev/null +++ b/pcsx2-qt/Debugger/Docking/LayoutEditorDialog.cpp @@ -0,0 +1,98 @@ +// 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) + m_ui.cpuEditor->addItem(DebugInterface::cpuName(cpu), 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 00000000000000..44c87b8eea6982 --- /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 00000000000000..cb24336513fdef --- /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 00000000000000..adb47c0e2b9f98 --- /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 00000000000000..4f7637700e8a0f --- /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 00000000000000..f727a71b4685b2 --- /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 00000000000000..28f8cd0ec43812 --- /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 bbb25a1ec80ab8..46f3f048df54a5 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 e410e799a29aac..ed5831e5d79822 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 @@ class MemoryViewTable } }; - class MemoryViewWidget final : public DebuggerWidget { Q_OBJECT diff --git a/pcsx2-qt/MainWindow.cpp b/pcsx2-qt/MainWindow.cpp index ce7ed58eda0045..dbd9c6b2c3bfe9 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" @@ -603,11 +604,11 @@ void MainWindow::quit() void MainWindow::destroySubWindows() { - if (m_debugger_window) + if (g_debugger_window) { - m_debugger_window->close(); - m_debugger_window->deleteLater(); - m_debugger_window = nullptr; + g_debugger_window->close(); + g_debugger_window->deleteLater(); + g_debugger_window = nullptr; } if (m_controller_settings_window) @@ -782,11 +783,11 @@ 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()) + if (g_debugger_window && !DebugInterface::getPauseOnEntry()) { - m_debugger_window->close(); - m_debugger_window->deleteLater(); - m_debugger_window = nullptr; + g_debugger_window->close(); + g_debugger_window->deleteLater(); + g_debugger_window = nullptr; } } } @@ -1735,10 +1736,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?"), @@ -1751,16 +1752,16 @@ void MainWindow::updateTheme() QtHost::UpdateApplicationTheme(); reloadThemeSpecificImages(); - if (m_debugger_window) + if (g_debugger_window) { - m_debugger_window->deleteLater(); - m_debugger_window = nullptr; + g_debugger_window->deleteLater(); + g_debugger_window = nullptr; getDebuggerWindow(); // populates m_debugger_window - m_debugger_window->resize(debugger_size); - m_debugger_window->move(debugger_pos); + g_debugger_window->resize(debugger_size); + g_debugger_window->move(debugger_pos); if (debugger_is_open) { - m_debugger_window->show(); + g_debugger_window->show(); } } } @@ -2614,16 +2615,16 @@ void MainWindow::doSettings(const char* category /* = nullptr */) DebuggerWindow* MainWindow::getDebuggerWindow() { - if (!m_debugger_window) + if (!g_debugger_window) { // Setup KDDockWidgets. - DockManager::configure_docking_system(); + DockManager::configureDockingSystem(); // 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); + static_cast(new DebuggerWindow(nullptr)); } - return m_debugger_window; + return g_debugger_window; } void MainWindow::openDebugger() diff --git a/pcsx2-qt/MainWindow.h b/pcsx2-qt/MainWindow.h index 0509b58be6a98a..7931f46d07d915 100644 --- a/pcsx2-qt/MainWindow.h +++ b/pcsx2-qt/MainWindow.h @@ -290,8 +290,6 @@ private Q_SLOTS: 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 410b109aee64d2..cce32395b3f303 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/Settings/DebugSettingsWidget.ui b/pcsx2-qt/Settings/DebugSettingsWidget.ui index e735510dcbeab4..e4b4cd8f2758da 100644 --- a/pcsx2-qt/Settings/DebugSettingsWidget.ui +++ b/pcsx2-qt/Settings/DebugSettingsWidget.ui @@ -6,7 +6,7 @@ 0 0 - 527 + 647 501 @@ -31,6 +31,11 @@ true + + + User Interface + + Analysis @@ -58,8 +63,8 @@ 0 0 - 523 - 464 + 645 + 469 diff --git a/pcsx2-qt/pcsx2-qt.vcxproj b/pcsx2-qt/pcsx2-qt.vcxproj index 3aee0fee68e9cc..f1a8637048e8b6 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,12 @@ + + + + + + @@ -220,7 +226,6 @@ - @@ -230,6 +235,12 @@ + + + + + + @@ -285,7 +296,6 @@ - @@ -294,6 +304,10 @@ + + + + @@ -433,6 +447,12 @@ Document + + Document + + + Document + Document diff --git a/pcsx2-qt/pcsx2-qt.vcxproj.filters b/pcsx2-qt/pcsx2-qt.vcxproj.filters index 5963e150f75052..ec0f0e6348ef08 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,24 @@ Debugger\Breakpoints + + Debugger\Docking + + + Debugger\Docking + + + Debugger\Docking + + + Debugger\Docking + + + Debugger\Docking + + + Debugger\Docking + Debugger\Memory @@ -332,9 +347,6 @@ moc - - moc - moc @@ -359,6 +371,18 @@ moc + + moc + + + moc + + + moc + + + moc + moc @@ -580,9 +604,6 @@ Debugger - - Debugger - Debugger @@ -607,6 +628,24 @@ Debugger\Breakpoints + + Debugger\Docking + + + Debugger\Docking + + + Debugger\Docking + + + Debugger\Docking + + + Debugger\Docking + + + Debugger\Docking + Debugger\Memory @@ -752,6 +791,12 @@ Debugger\Breakpoints + + Debugger\Docking + + + Debugger\Docking + Debugger\Memory diff --git a/pcsx2/Config.h b/pcsx2/Config.h index 7d76df7a451c55..cc4c7acb408a18 100644 --- a/pcsx2/Config.h +++ b/pcsx2/Config.h @@ -1364,7 +1364,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; @@ -1380,6 +1379,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 189a9b5f430375..c824722d4466b4 100644 --- a/pcsx2/DebugTools/DebugInterface.cpp +++ b/pcsx2/DebugTools/DebugInterface.cpp @@ -158,6 +158,40 @@ 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 ""; +} + // // R5900DebugInterface // diff --git a/pcsx2/DebugTools/DebugInterface.h b/pcsx2/DebugTools/DebugInterface.h index 3e89473aa87480..5ae5cfbe1ca76f 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 @@ class DebugInterface : public MemoryReader 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,16 @@ class DebugInterface : public MemoryReader 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); + private: static bool m_pause_on_entry; }; diff --git a/pcsx2/Pcsx2Config.cpp b/pcsx2/Pcsx2Config.cpp index 9197fb2e1c96b5..d7aa26a8cf6574 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; @@ -2226,6 +2227,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) @@ -2252,6 +2255,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()); @@ -2269,6 +2273,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()); } @@ -2285,11 +2290,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; }