First Commit

This commit is contained in:
2025-11-18 14:18:26 -07:00
parent 33eb6e3707
commit 27277ec342
6106 changed files with 3571167 additions and 0 deletions

View File

@@ -0,0 +1,926 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#include "DockLayout.h"
#include "Debugger/DebuggerView.h"
#include "Debugger/DebuggerWindow.h"
#include "Debugger/JsonValueWrapper.h"
#include "common/Assertions.h"
#include "common/Console.h"
#include "common/FileSystem.h"
#include "common/Path.h"
#include <kddockwidgets/Config.h>
#include <kddockwidgets/DockWidget.h>
#include <kddockwidgets/LayoutSaver.h>
#include <kddockwidgets/core/DockRegistry.h>
#include <kddockwidgets/core/DockWidget.h>
#include <kddockwidgets/core/Group.h>
#include <kddockwidgets/core/Layout.h>
#include <kddockwidgets/core/ViewFactory.h>
#include <kddockwidgets/qtwidgets/Group.h>
#include <kddockwidgets/qtwidgets/MainWindow.h>
#include "rapidjson/document.h"
#include "rapidjson/prettywriter.h"
const char* DEBUGGER_LAYOUT_FILE_FORMAT = "PCSX2 Debugger User Interface Layout";
// Increment this whenever there is a breaking change to the JSON format.
const u32 DEBUGGER_LAYOUT_FILE_VERSION_MAJOR = 2;
// Increment this whenever there is a non-breaking change to the JSON format.
const u32 DEBUGGER_LAYOUT_FILE_VERSION_MINOR = 0;
DockLayout::DockLayout(
QString name,
BreakPointCpu cpu,
bool is_default,
const std::string& base_name,
DockLayout::Index index)
: m_name(name)
, m_cpu(cpu)
, m_is_default(is_default)
, m_base_layout(base_name)
{
reset();
save(index);
}
DockLayout::DockLayout(
QString name,
BreakPointCpu cpu,
bool is_default,
DockLayout::Index index)
: m_name(name)
, m_cpu(cpu)
, m_is_default(is_default)
{
save(index);
}
DockLayout::DockLayout(
QString name,
BreakPointCpu cpu,
bool is_default,
const DockLayout& layout_to_clone,
DockLayout::Index index)
: m_name(name)
, m_cpu(cpu)
, m_is_default(is_default)
, m_next_id(layout_to_clone.m_next_id)
, m_base_layout(layout_to_clone.m_base_layout)
, m_toolbars(layout_to_clone.m_toolbars)
, m_geometry(layout_to_clone.m_geometry)
{
for (const auto& [unique_name, widget_to_clone] : layout_to_clone.m_widgets)
{
auto widget_description = DockTables::DEBUGGER_VIEWS.find(widget_to_clone->metaObject()->className());
if (widget_description == DockTables::DEBUGGER_VIEWS.end())
continue;
DebuggerViewParameters parameters;
parameters.unique_name = unique_name;
parameters.id = widget_to_clone->id();
parameters.cpu = &DebugInterface::get(cpu);
parameters.cpu_override = widget_to_clone->cpuOverride();
DebuggerView* new_widget = widget_description->second.create_widget(parameters);
new_widget->setCustomDisplayName(widget_to_clone->customDisplayName());
new_widget->setPrimary(widget_to_clone->isPrimary());
m_widgets.emplace(unique_name, new_widget);
}
save(index);
}
DockLayout::DockLayout(
const std::string& path,
DockLayout::LoadResult& result,
DockLayout::Index& index_last_session,
DockLayout::Index index)
{
load(path, result, index_last_session);
}
DockLayout::~DockLayout()
{
for (auto& [unique_name, widget] : m_widgets)
{
pxAssert(widget.get());
delete widget;
}
}
const QString& DockLayout::name() const
{
return m_name;
}
void DockLayout::setName(QString name)
{
m_name = std::move(name);
}
BreakPointCpu DockLayout::cpu() const
{
return m_cpu;
}
bool DockLayout::isDefault() const
{
return m_is_default;
}
void DockLayout::setCpu(BreakPointCpu cpu)
{
m_cpu = cpu;
for (auto& [unique_name, widget] : m_widgets)
{
pxAssert(widget.get());
if (!widget->setCpu(DebugInterface::get(cpu)))
recreateDebuggerView(unique_name);
}
}
void DockLayout::freeze()
{
pxAssert(m_is_active);
m_is_active = false;
if (g_debugger_window)
m_toolbars = g_debugger_window->saveState();
// Store the geometry of all the dock widgets as JSON.
KDDockWidgets::LayoutSaver saver(KDDockWidgets::RestoreOption_RelativeToMainWindow);
m_geometry = saver.serializeLayout();
// Delete the dock widgets.
for (KDDockWidgets::Core::DockWidget* dock : KDDockWidgets::DockRegistry::self()->dockwidgets())
{
// Make sure the dock widget releases ownership of its content.
auto view = static_cast<KDDockWidgets::QtWidgets::DockWidget*>(dock->view());
view->setWidget(new QWidget());
delete dock;
}
}
void DockLayout::thaw()
{
pxAssert(!m_is_active);
m_is_active = true;
if (!g_debugger_window)
return;
// Restore the state of the toolbars.
if (m_toolbars.isEmpty())
{
const DockTables::DefaultDockLayout* base_layout = DockTables::defaultLayout(m_base_layout);
if (base_layout)
{
for (QToolBar* toolbar : g_debugger_window->findChildren<QToolBar*>())
if (base_layout->toolbars.contains(toolbar->objectName().toStdString()))
toolbar->show();
}
}
else
{
g_debugger_window->restoreState(m_toolbars);
}
if (m_geometry.isEmpty())
{
// This is a newly created layout with no geometry information.
setupDefaultLayout();
}
else
{
// Create all the dock widgets.
KDDockWidgets::LayoutSaver saver(KDDockWidgets::RestoreOption_RelativeToMainWindow);
if (!saver.restoreLayout(m_geometry))
{
// We've failed to restore the geometry, so just tear down whatever
// dock widgets may exist and then setup the default layout.
for (KDDockWidgets::Core::DockWidget* dock : KDDockWidgets::DockRegistry::self()->dockwidgets())
{
// Make sure the dock widget releases ownership of its content.
auto view = static_cast<KDDockWidgets::QtWidgets::DockWidget*>(dock->view());
view->setWidget(new QWidget());
delete dock;
}
setupDefaultLayout();
}
}
// Check that all the dock widgets have been restored correctly.
std::vector<QString> orphaned_debugger_views;
for (auto& [unique_name, widget] : m_widgets)
{
auto [controller, view] = DockUtils::dockWidgetFromName(unique_name);
if (!controller || !view)
{
Console.Error("Debugger: Failed to restore dock widget '%s'.", unique_name.toStdString().c_str());
orphaned_debugger_views.emplace_back(unique_name);
}
}
// Delete any debugger views that haven't been restored correctly.
for (const QString& unique_name : orphaned_debugger_views)
{
auto widget_iterator = m_widgets.find(unique_name);
pxAssert(widget_iterator != m_widgets.end());
setPrimaryDebuggerView(widget_iterator->second.get(), false);
delete widget_iterator->second.get();
m_widgets.erase(widget_iterator);
}
updateDockWidgetTitles();
}
bool DockLayout::canReset()
{
return DockTables::defaultLayout(m_base_layout) != nullptr;
}
void DockLayout::reset()
{
pxAssert(!m_is_active);
for (auto& [unique_name, widget] : m_widgets)
{
pxAssert(widget.get());
delete widget;
}
m_next_id = 0;
m_toolbars.clear();
m_widgets.clear();
m_geometry.clear();
const DockTables::DefaultDockLayout* base_layout = DockTables::defaultLayout(m_base_layout);
if (!base_layout)
return;
for (size_t i = 0; i < base_layout->widgets.size(); i++)
{
auto iterator = DockTables::DEBUGGER_VIEWS.find(base_layout->widgets[i].type);
pxAssertRel(iterator != DockTables::DEBUGGER_VIEWS.end(), "Invalid default layout.");
const DockTables::DebuggerViewDescription& dock_description = iterator->second;
DebuggerViewParameters parameters;
std::tie(parameters.unique_name, parameters.id) =
generateNewUniqueName(base_layout->widgets[i].type.c_str());
parameters.cpu = &DebugInterface::get(m_cpu);
if (parameters.unique_name.isEmpty())
continue;
DebuggerView* widget = dock_description.create_widget(parameters);
widget->setPrimary(true);
m_widgets.emplace(parameters.unique_name, widget);
}
}
KDDockWidgets::Core::DockWidget* DockLayout::createDockWidget(const QString& name)
{
pxAssert(m_is_active);
pxAssert(KDDockWidgets::LayoutSaver::restoreInProgress());
auto widget_iterator = m_widgets.find(name);
if (widget_iterator == m_widgets.end())
return nullptr;
DebuggerView* widget = widget_iterator->second;
if (!widget)
return nullptr;
pxAssert(widget->uniqueName() == name);
auto view = static_cast<KDDockWidgets::QtWidgets::DockWidget*>(
KDDockWidgets::Config::self().viewFactory()->createDockWidget(name));
view->setWidget(widget);
return view->asController<KDDockWidgets::Core::DockWidget>();
}
void DockLayout::updateDockWidgetTitles()
{
if (!m_is_active)
return;
// Translate default debugger view names.
for (auto& [unique_name, widget] : m_widgets)
widget->retranslateDisplayName();
// Determine if any widgets have duplicate display names.
std::map<QString, std::vector<DebuggerView*>> display_name_to_widgets;
for (auto& [unique_name, widget] : m_widgets)
display_name_to_widgets[widget->displayNameWithoutSuffix()].emplace_back(widget.get());
for (auto& [display_name, widgets] : display_name_to_widgets)
{
std::sort(widgets.begin(), widgets.end(),
[&](const DebuggerView* lhs, const DebuggerView* rhs) {
return lhs->id() < rhs->id();
});
for (size_t i = 0; i < widgets.size(); i++)
{
std::optional<int> suffix_number;
if (widgets.size() != 1)
suffix_number = static_cast<int>(i + 1);
widgets[i]->setDisplayNameSuffixNumber(suffix_number);
}
}
// Propagate the new names from the debugger views to the dock widgets.
for (auto& [unique_name, widget] : m_widgets)
{
auto [controller, view] = DockUtils::dockWidgetFromName(widget->uniqueName());
if (!controller)
continue;
controller->setTitle(widget->displayName());
}
}
const std::map<QString, QPointer<DebuggerView>>& DockLayout::debuggerViews()
{
return m_widgets;
}
bool DockLayout::hasDebuggerView(const QString& unique_name)
{
return m_widgets.find(unique_name) != m_widgets.end();
}
size_t DockLayout::countDebuggerViewsOfType(const char* type)
{
size_t count = 0;
for (const auto& [unique_name, widget] : m_widgets)
{
if (strcmp(widget->metaObject()->className(), type) == 0)
count++;
}
return count;
}
void DockLayout::createDebuggerView(const std::string& type)
{
pxAssert(m_is_active);
if (!g_debugger_window)
return;
auto description_iterator = DockTables::DEBUGGER_VIEWS.find(type);
pxAssert(description_iterator != DockTables::DEBUGGER_VIEWS.end());
const DockTables::DebuggerViewDescription& description = description_iterator->second;
DebuggerViewParameters parameters;
std::tie(parameters.unique_name, parameters.id) = generateNewUniqueName(type.c_str());
parameters.cpu = &DebugInterface::get(m_cpu);
if (parameters.unique_name.isEmpty())
return;
DebuggerView* widget = description.create_widget(parameters);
m_widgets.emplace(parameters.unique_name, widget);
setPrimaryDebuggerView(widget, countDebuggerViewsOfType(type.c_str()) == 0);
auto view = static_cast<KDDockWidgets::QtWidgets::DockWidget*>(
KDDockWidgets::Config::self().viewFactory()->createDockWidget(widget->uniqueName()));
view->setWidget(widget);
KDDockWidgets::Core::DockWidget* controller = view->asController<KDDockWidgets::Core::DockWidget>();
pxAssert(controller);
DockUtils::insertDockWidgetAtPreferredLocation(controller, description.preferred_location, g_debugger_window);
updateDockWidgetTitles();
}
void DockLayout::recreateDebuggerView(const QString& unique_name)
{
if (!g_debugger_window)
return;
auto debugger_view_iterator = m_widgets.find(unique_name);
pxAssert(debugger_view_iterator != m_widgets.end());
DebuggerView* old_debugger_view = debugger_view_iterator->second;
auto description_iterator = DockTables::DEBUGGER_VIEWS.find(old_debugger_view->metaObject()->className());
pxAssert(description_iterator != DockTables::DEBUGGER_VIEWS.end());
const DockTables::DebuggerViewDescription& description = description_iterator->second;
DebuggerViewParameters parameters;
parameters.unique_name = old_debugger_view->uniqueName();
parameters.id = old_debugger_view->id();
parameters.cpu = &DebugInterface::get(m_cpu);
parameters.cpu_override = old_debugger_view->cpuOverride();
DebuggerView* new_debugger_view = description.create_widget(parameters);
new_debugger_view->setCustomDisplayName(old_debugger_view->customDisplayName());
new_debugger_view->setPrimary(old_debugger_view->isPrimary());
debugger_view_iterator->second = new_debugger_view;
if (m_is_active)
{
auto [controller, view] = DockUtils::dockWidgetFromName(unique_name);
if (view)
view->setWidget(new_debugger_view);
}
delete old_debugger_view;
}
void DockLayout::destroyDebuggerView(const QString& unique_name)
{
pxAssert(m_is_active);
if (!g_debugger_window)
return;
auto debugger_view_iterator = m_widgets.find(unique_name);
if (debugger_view_iterator == m_widgets.end())
return;
setPrimaryDebuggerView(debugger_view_iterator->second.get(), false);
delete debugger_view_iterator->second.get();
m_widgets.erase(debugger_view_iterator);
auto [controller, view] = DockUtils::dockWidgetFromName(unique_name);
if (!controller)
return;
controller->deleteLater();
updateDockWidgetTitles();
}
void DockLayout::setPrimaryDebuggerView(DebuggerView* widget, bool is_primary)
{
bool present = false;
for (auto& [unique_name, test_widget] : m_widgets)
{
if (test_widget.get() == widget)
{
present = true;
break;
}
}
if (!present)
return;
if (is_primary)
{
// Set the passed widget as the primary widget.
for (auto& [unique_name, test_widget] : m_widgets)
{
if (strcmp(test_widget->metaObject()->className(), widget->metaObject()->className()) == 0)
{
test_widget->setPrimary(test_widget.get() == widget);
}
}
}
else if (widget->isPrimary())
{
// Set an arbitrary widget as the primary widget.
bool next = true;
for (auto& [unique_name, test_widget] : m_widgets)
{
if (test_widget != widget &&
strcmp(test_widget->metaObject()->className(), widget->metaObject()->className()) == 0)
{
test_widget->setPrimary(next);
next = false;
}
}
// If we haven't set another widget as the primary one we can't make
// this one not the primary one.
if (!next)
widget->setPrimary(false);
}
}
void DockLayout::deleteFile()
{
if (m_layout_file_path.empty())
return;
if (!FileSystem::DeleteFilePath(m_layout_file_path.c_str()))
Console.Error("Debugger: Failed to delete layout file '%s'.", m_layout_file_path.c_str());
}
bool DockLayout::save(DockLayout::Index layout_index)
{
if (!g_debugger_window)
return false;
if (m_is_active)
{
m_toolbars = g_debugger_window->saveState();
// Store the geometry of all the dock widgets as JSON.
KDDockWidgets::LayoutSaver saver(KDDockWidgets::RestoreOption_RelativeToMainWindow);
m_geometry = saver.serializeLayout();
}
// Serialize the layout as JSON.
rapidjson::Document json(rapidjson::kObjectType);
rapidjson::Document geometry;
const char* cpu_name = DebugInterface::cpuName(m_cpu);
u32 default_layout_hash = DockTables::hashDefaultLayouts();
rapidjson::Value format;
format.SetString(DEBUGGER_LAYOUT_FILE_FORMAT, strlen(DEBUGGER_LAYOUT_FILE_FORMAT));
json.AddMember("format", format, json.GetAllocator());
json.AddMember("versionMajor", DEBUGGER_LAYOUT_FILE_VERSION_MAJOR, json.GetAllocator());
json.AddMember("versionMinor", DEBUGGER_LAYOUT_FILE_VERSION_MINOR, json.GetAllocator());
json.AddMember("defaultLayoutHash", default_layout_hash, json.GetAllocator());
std::string name_str = m_name.toStdString();
json.AddMember("name", rapidjson::Value().SetString(name_str.c_str(), name_str.size()), json.GetAllocator());
json.AddMember("target", rapidjson::Value().SetString(cpu_name, strlen(cpu_name)), json.GetAllocator());
json.AddMember("index", static_cast<int>(layout_index), json.GetAllocator());
json.AddMember("isDefault", m_is_default, json.GetAllocator());
json.AddMember("nextId", m_next_id, json.GetAllocator());
if (!m_base_layout.empty())
{
rapidjson::Value base_layout;
base_layout.SetString(m_base_layout.c_str(), m_base_layout.size());
json.AddMember("baseLayout", base_layout, json.GetAllocator());
}
if (!m_toolbars.isEmpty())
{
std::string toolbars_str = m_toolbars.toBase64().toStdString();
rapidjson::Value toolbars;
toolbars.SetString(toolbars_str.data(), toolbars_str.size(), json.GetAllocator());
json.AddMember("toolbars", toolbars, json.GetAllocator());
}
rapidjson::Value dock_widgets(rapidjson::kArrayType);
for (auto& [unique_name, widget] : m_widgets)
{
pxAssert(widget.get());
rapidjson::Value object(rapidjson::kObjectType);
std::string name_str = unique_name.toStdString();
rapidjson::Value name;
name.SetString(name_str.c_str(), name_str.size(), json.GetAllocator());
object.AddMember("uniqueName", name, json.GetAllocator());
object.AddMember("id", widget->id(), json.GetAllocator());
const char* type_str = widget->metaObject()->className();
rapidjson::Value type;
type.SetString(type_str, strlen(type_str), json.GetAllocator());
object.AddMember("type", type, json.GetAllocator());
if (widget->cpuOverride().has_value())
{
const char* cpu_name = DebugInterface::cpuName(*widget->cpuOverride());
rapidjson::Value target;
target.SetString(cpu_name, strlen(cpu_name));
object.AddMember("target", target, json.GetAllocator());
}
JsonValueWrapper wrapper(object, json.GetAllocator());
widget->toJson(wrapper);
dock_widgets.PushBack(object, json.GetAllocator());
}
json.AddMember("dockWidgets", dock_widgets, json.GetAllocator());
if (!m_geometry.isEmpty() && !geometry.Parse(m_geometry).HasParseError())
json.AddMember("geometry", geometry, json.GetAllocator());
rapidjson::StringBuffer string_buffer;
rapidjson::PrettyWriter<rapidjson::StringBuffer> writer(string_buffer);
json.Accept(writer);
std::string safe_name = Path::SanitizeFileName(m_name.toStdString());
// Create a temporary file first so that we don't corrupt an existing file
// in the case that we succeed in opening the file but fail to write our
// data to it.
std::string temp_file_path = Path::Combine(EmuFolders::DebuggerLayouts, safe_name + ".tmp");
if (!FileSystem::WriteStringToFile(temp_file_path.c_str(), string_buffer.GetString()))
{
Console.Error("Debugger: Failed to save temporary layout file '%s'.", temp_file_path.c_str());
FileSystem::DeleteFilePath(temp_file_path.c_str());
return false;
}
// Now move the layout to its final location.
std::string file_path = Path::Combine(EmuFolders::DebuggerLayouts, safe_name + ".json");
if (!FileSystem::RenamePath(temp_file_path.c_str(), file_path.c_str()))
{
Console.Error("Debugger: Failed to move layout file to '%s'.", file_path.c_str());
FileSystem::DeleteFilePath(temp_file_path.c_str());
return false;
}
// If the layout has been renamed we need to delete the old file.
if (file_path != m_layout_file_path)
deleteFile();
m_layout_file_path = std::move(file_path);
return true;
}
void DockLayout::load(
const std::string& path,
LoadResult& result,
DockLayout::Index& index_last_session)
{
pxAssert(!m_is_active);
result = SUCCESS;
std::optional<std::string> text = FileSystem::ReadFileToString(path.c_str());
if (!text.has_value())
{
Console.Error("Debugger: Failed to open layout file '%s'.", path.c_str());
result = FILE_NOT_FOUND;
return;
}
rapidjson::Document json;
if (json.Parse(text->c_str()).HasParseError() || !json.IsObject())
{
Console.Error("Debugger: Failed to parse layout file '%s' as JSON.", path.c_str());
result = INVALID_FORMAT;
return;
}
auto format = json.FindMember("format");
if (format == json.MemberEnd() ||
!format->value.IsString() ||
strcmp(format->value.GetString(), DEBUGGER_LAYOUT_FILE_FORMAT) != 0)
{
Console.Error("Debugger: Layout file '%s' has missing or invalid 'format' property.", path.c_str());
result = INVALID_FORMAT;
return;
}
auto version_major = json.FindMember("versionMajor");
if (version_major == json.MemberEnd() || !version_major->value.IsInt())
{
Console.Error("Debugger: Layout file '%s' has missing or invalid 'versionMajor' property.", path.c_str());
result = MAJOR_VERSION_MISMATCH;
return;
}
if (version_major->value.GetInt() != DEBUGGER_LAYOUT_FILE_VERSION_MAJOR)
{
result = MAJOR_VERSION_MISMATCH;
return;
}
auto version_minor = json.FindMember("versionMinor");
if (version_minor == json.MemberEnd() || !version_minor->value.IsInt())
{
Console.Error("Debugger: Layout file '%s' has missing or invalid 'versionMinor' property.", path.c_str());
result = MAJOR_VERSION_MISMATCH;
return;
}
auto default_layout_hash = json.FindMember("defaultLayoutHash");
if (default_layout_hash == json.MemberEnd() || !default_layout_hash->value.IsUint())
{
Console.Error("Debugger: Layout file '%s' has missing or invalid 'defaultLayoutHash' property.", path.c_str());
result = MAJOR_VERSION_MISMATCH;
return;
}
if (default_layout_hash->value.GetUint() != DockTables::hashDefaultLayouts())
result = DEFAULT_LAYOUT_HASH_MISMATCH;
auto name = json.FindMember("name");
if (name != json.MemberEnd() && name->value.IsString())
m_name = name->value.GetString();
else
m_name = QCoreApplication::translate("DockLayout", "Unnamed");
m_name.truncate(DockUtils::MAX_LAYOUT_NAME_SIZE);
auto target = json.FindMember("target");
m_cpu = BREAKPOINT_EE;
if (target != json.MemberEnd() && target->value.IsString())
{
for (BreakPointCpu cpu : DEBUG_CPUS)
if (strcmp(DebugInterface::cpuName(cpu), target->value.GetString()) == 0)
m_cpu = cpu;
}
auto index = json.FindMember("index");
if (index != json.MemberEnd() && index->value.IsInt())
index_last_session = index->value.GetInt();
auto is_default = json.FindMember("isDefault");
if (is_default != json.MemberEnd() && is_default->value.IsBool())
m_is_default = is_default->value.GetBool();
auto next_id = json.FindMember("nextId");
if (next_id != json.MemberBegin() && next_id->value.IsUint64())
m_next_id = next_id->value.GetUint64();
auto base_layout = json.FindMember("baseLayout");
if (base_layout != json.MemberEnd() && base_layout->value.IsString())
m_base_layout = base_layout->value.GetString();
auto toolbars = json.FindMember("toolbars");
if (toolbars != json.MemberEnd() && toolbars->value.IsString())
m_toolbars = QByteArray::fromBase64(toolbars->value.GetString());
auto dock_widgets = json.FindMember("dockWidgets");
if (dock_widgets != json.MemberEnd() && dock_widgets->value.IsArray())
{
for (rapidjson::Value& object : dock_widgets->value.GetArray())
{
auto unique_name = object.FindMember("uniqueName");
if (unique_name == object.MemberEnd() || !unique_name->value.IsString())
continue;
auto id = object.FindMember("id");
if (id == object.MemberEnd() || !id->value.IsUint64())
continue;
auto widgets_iterator = m_widgets.find(unique_name->value.GetString());
if (widgets_iterator != m_widgets.end())
continue;
auto type = object.FindMember("type");
if (type == object.MemberEnd() || !type->value.IsString())
continue;
auto description = DockTables::DEBUGGER_VIEWS.find(type->value.GetString());
if (description == DockTables::DEBUGGER_VIEWS.end())
continue;
std::optional<BreakPointCpu> cpu_override;
auto target = object.FindMember("target");
if (target != object.MemberEnd() && target->value.IsString())
{
for (BreakPointCpu cpu : DEBUG_CPUS)
if (strcmp(DebugInterface::cpuName(cpu), target->value.GetString()) == 0)
cpu_override = cpu;
}
DebuggerViewParameters parameters;
parameters.unique_name = unique_name->value.GetString();
parameters.id = id->value.GetUint64();
parameters.cpu = &DebugInterface::get(m_cpu);
parameters.cpu_override = cpu_override;
DebuggerView* widget = description->second.create_widget(parameters);
JsonValueWrapper wrapper(object, json.GetAllocator());
if (!widget->fromJson(wrapper))
{
delete widget;
continue;
}
m_widgets.emplace(unique_name->value.GetString(), widget);
}
}
auto geometry = json.FindMember("geometry");
if (geometry != json.MemberEnd() && geometry->value.IsObject())
{
rapidjson::StringBuffer string_buffer;
rapidjson::Writer<rapidjson::StringBuffer> writer(string_buffer);
geometry->value.Accept(writer);
m_geometry = QByteArray(string_buffer.GetString(), string_buffer.GetSize());
}
m_layout_file_path = path;
validatePrimaryDebuggerViews();
}
void DockLayout::validatePrimaryDebuggerViews()
{
std::map<std::string, std::vector<DebuggerView*>> type_to_widgets;
for (const auto& [unique_name, widget] : m_widgets)
type_to_widgets[widget->metaObject()->className()].emplace_back(widget.get());
for (auto& [type, widgets] : type_to_widgets)
{
u32 primary_widgets = 0;
// Make sure at most one widget is marked as primary.
for (DebuggerView* widget : widgets)
{
if (widget->isPrimary())
{
if (primary_widgets != 0)
widget->setPrimary(false);
primary_widgets++;
}
}
// If none of the widgets were marked as primary, just set the first one
// as the primary one.
if (primary_widgets == 0)
widgets[0]->setPrimary(true);
}
}
void DockLayout::setupDefaultLayout()
{
pxAssert(m_is_active);
if (!g_debugger_window)
return;
const DockTables::DefaultDockLayout* base_layout = DockTables::defaultLayout(m_base_layout);
if (!base_layout)
return;
std::vector<KDDockWidgets::QtWidgets::DockWidget*> groups(base_layout->groups.size(), nullptr);
for (const DockTables::DefaultDockWidgetDescription& dock_description : base_layout->widgets)
{
const DockTables::DefaultDockGroupDescription& group =
base_layout->groups[static_cast<u32>(dock_description.group)];
DebuggerView* widget = nullptr;
for (auto& [unique_name, test_widget] : m_widgets)
if (test_widget->metaObject()->className() == dock_description.type)
widget = test_widget;
if (!widget)
continue;
auto view = static_cast<KDDockWidgets::QtWidgets::DockWidget*>(
KDDockWidgets::Config::self().viewFactory()->createDockWidget(widget->uniqueName()));
view->setWidget(widget);
if (!groups[static_cast<u32>(dock_description.group)])
{
KDDockWidgets::QtWidgets::DockWidget* parent = nullptr;
if (group.parent != DockTables::DefaultDockGroup::ROOT)
parent = groups[static_cast<u32>(group.parent)];
g_debugger_window->addDockWidget(view, group.location, parent);
groups[static_cast<u32>(dock_description.group)] = view;
}
else
{
groups[static_cast<u32>(dock_description.group)]->addDockWidgetAsTab(view);
}
}
for (KDDockWidgets::Core::Group* group : KDDockWidgets::DockRegistry::self()->groups())
group->setCurrentTabIndex(0);
}
std::pair<QString, u64> DockLayout::generateNewUniqueName(const char* type)
{
QString name;
u64 id;
do
{
if (m_next_id == INT_MAX)
return {QString(), 0};
id = m_next_id;
name = QStringLiteral("%1-%2").arg(type).arg(static_cast<qulonglong>(m_next_id));
m_next_id++;
} while (hasDebuggerView(name));
return {name, id};
}

View File

@@ -0,0 +1,163 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#pragma once
#include "Debugger/Docking/DockTables.h"
#include "DebugTools/DebugInterface.h"
#include <kddockwidgets/MainWindow.h>
#include <kddockwidgets/DockWidget.h>
#include <QtCore/QPointer>
class DebuggerView;
class DebuggerWindow;
extern const char* DEBUGGER_LAYOUT_FILE_FORMAT;
// Increment this whenever there is a breaking change to the JSON format.
extern const u32 DEBUGGER_LAYOUT_FILE_VERSION_MAJOR;
// Increment this whenever there is a non-breaking change to the JSON format.
extern const u32 DEBUGGER_LAYOUT_FILE_VERSION_MINOR;
class DockLayout
{
public:
using Index = size_t;
static const constexpr Index INVALID_INDEX = SIZE_MAX;
enum LoadResult
{
SUCCESS,
FILE_NOT_FOUND,
INVALID_FORMAT,
MAJOR_VERSION_MISMATCH,
DEFAULT_LAYOUT_HASH_MISMATCH,
CONFLICTING_NAME
};
// Create a layout based on a default layout.
DockLayout(
QString name,
BreakPointCpu cpu,
bool is_default,
const std::string& base_name,
DockLayout::Index index);
// Create a new blank layout.
DockLayout(
QString name,
BreakPointCpu cpu,
bool is_default,
DockLayout::Index index);
// Clone an existing layout.
DockLayout(
QString name,
BreakPointCpu cpu,
bool is_default,
const DockLayout& layout_to_clone,
DockLayout::Index index);
// Load a layout from a file.
DockLayout(
const std::string& path,
LoadResult& result,
DockLayout::Index& index_last_session,
DockLayout::Index index);
~DockLayout();
DockLayout(const DockLayout& rhs) = delete;
DockLayout& operator=(const DockLayout& rhs) = delete;
DockLayout(DockLayout&& rhs) = default;
DockLayout& operator=(DockLayout&&) = default;
const QString& name() const;
void setName(QString name);
BreakPointCpu cpu() const;
void setCpu(BreakPointCpu cpu);
bool isDefault() const;
// Tear down and save the state of all the dock widgets from this layout.
void freeze();
// Restore the state of all the dock widgets from this layout.
void thaw();
bool canReset();
void reset();
KDDockWidgets::Core::DockWidget* createDockWidget(const QString& name);
void updateDockWidgetTitles();
const std::map<QString, QPointer<DebuggerView>>& debuggerViews();
bool hasDebuggerView(const QString& unique_name);
size_t countDebuggerViewsOfType(const char* type);
void createDebuggerView(const std::string& type);
void recreateDebuggerView(const QString& unique_name);
void destroyDebuggerView(const QString& unique_name);
void setPrimaryDebuggerView(DebuggerView* widget, bool is_primary);
void deleteFile();
bool save(DockLayout::Index layout_index);
private:
void load(
const std::string& path,
DockLayout::LoadResult& result,
DockLayout::Index& index_last_session);
// Make sure there is only a single primary debugger view of each type.
void validatePrimaryDebuggerViews();
void setupDefaultLayout();
std::pair<QString, u64> generateNewUniqueName(const char* type);
// The name displayed in the user interface. Also used to determine the
// file name for the layout file.
QString m_name;
// The default target for dock widgets in this layout. This can be
// overriden on a per-widget basis.
BreakPointCpu m_cpu;
// Is this one of the default layouts?
bool m_is_default = false;
// A counter used to generate new unique names for dock widgets.
u64 m_next_id = 0;
// The name of the default layout which this layout was based on. This will
// be used if the m_geometry variable above is empty.
std::string m_base_layout;
// The state of all the toolbars, saved and restored using
// QMainWindow::saveState and QMainWindow::restoreState respectively.
QByteArray m_toolbars;
// All the dock widgets currently open in this layout. If this is the active
// layout then these will be owned by the docking system, otherwise they
// won't be and will need to be cleaned up separately.
std::map<QString, QPointer<DebuggerView>> m_widgets;
// The geometry of all the dock widgets, converted to JSON by the
// LayoutSaver class from KDDockWidgets.
QByteArray m_geometry;
// The absolute file path of the corresponding layout file as it currently
// exists exists on disk, or empty if no such file exists.
std::string m_layout_file_path;
// If this layout is the currently selected layout this will be true,
// otherwise it will be false.
bool m_is_active = false;
};

View File

@@ -0,0 +1,874 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#include "DockManager.h"
#include "Debugger/DebuggerView.h"
#include "Debugger/DebuggerWindow.h"
#include "Debugger/Docking/DockTables.h"
#include "Debugger/Docking/DockViews.h"
#include "Debugger/Docking/DropIndicators.h"
#include "Debugger/Docking/LayoutEditorDialog.h"
#include "Debugger/Docking/NoLayoutsWidget.h"
#include "common/Assertions.h"
#include "common/FileSystem.h"
#include "common/StringUtil.h"
#include "common/Path.h"
#include <kddockwidgets/Config.h>
#include <kddockwidgets/core/Group.h>
#include <kddockwidgets/core/Stack.h>
#include <kddockwidgets/core/indicators/SegmentedDropIndicatorOverlay.h>
#include <kddockwidgets/qtwidgets/Stack.h>
#include <QtCore/QTimer>
#include <QtWidgets/QMessageBox>
#include <QtWidgets/QProxyStyle>
#include <QtWidgets/QStyleFactory>
DockManager::DockManager(QObject* parent)
: QObject(parent)
{
QTimer* autosave_timer = new QTimer(this);
connect(autosave_timer, &QTimer::timeout, this, &DockManager::saveCurrentLayout);
autosave_timer->start(60 * 1000);
}
void DockManager::configureDockingSystem()
{
std::string indicator_style = Host::GetBaseStringSettingValue(
"Debugger/UserInterface", "DropIndicatorStyle", "Classic");
if (indicator_style == "Segmented" || indicator_style == "Minimalistic")
{
KDDockWidgets::Core::ViewFactory::s_dropIndicatorType = KDDockWidgets::DropIndicatorType::Segmented;
DockSegmentedDropIndicatorOverlay::s_indicator_style = indicator_style;
}
else
{
KDDockWidgets::Core::ViewFactory::s_dropIndicatorType = KDDockWidgets::DropIndicatorType::Classic;
}
static bool done = false;
if (done)
return;
KDDockWidgets::initFrontend(KDDockWidgets::FrontendType::QtWidgets);
KDDockWidgets::Config& config = KDDockWidgets::Config::self();
config.setFlags(
KDDockWidgets::Config::Flag_HideTitleBarWhenTabsVisible |
KDDockWidgets::Config::Flag_AlwaysShowTabs |
KDDockWidgets::Config::Flag_AllowReorderTabs |
KDDockWidgets::Config::Flag_TitleBarIsFocusable);
// We set this flag regardless of whether or not the windowing system
// supports compositing since it's only used by the built-in docking
// indicator, and we only fall back to that if compositing is disabled.
config.setInternalFlags(KDDockWidgets::Config::InternalFlag_DisableTranslucency);
config.setDockWidgetFactoryFunc(&DockManager::dockWidgetFactory);
config.setViewFactory(new DockViewFactory());
config.setDragAboutToStartFunc(&DockManager::dragAboutToStart);
config.setStartDragDistance(std::max(QApplication::startDragDistance(), 32));
done = true;
}
bool DockManager::deleteLayout(DockLayout::Index layout_index)
{
pxAssertRel(layout_index != DockLayout::INVALID_INDEX,
"DockManager::deleteLayout called with INVALID_INDEX.");
if (layout_index == m_current_layout)
{
DockLayout::Index other_layout = DockLayout::INVALID_INDEX;
if (layout_index + 1 < m_layouts.size())
other_layout = layout_index + 1;
else if (layout_index > 0)
other_layout = layout_index - 1;
switchToLayout(other_layout);
}
m_layouts.at(layout_index).deleteFile();
m_layouts.erase(m_layouts.begin() + layout_index);
// All the layouts after the one being deleted have been shifted over by
// one, so adjust the current layout index accordingly.
if (m_current_layout > layout_index && m_current_layout != DockLayout::INVALID_INDEX)
m_current_layout--;
if (m_layouts.empty() && g_debugger_window)
{
NoLayoutsWidget* widget = new NoLayoutsWidget;
connect(widget->createDefaultLayoutsButton(), &QPushButton::clicked, this, &DockManager::resetAllLayouts);
KDDockWidgets::QtWidgets::DockWidget* dock = new KDDockWidgets::QtWidgets::DockWidget("placeholder");
dock->setTitle(tr("No Layouts"));
dock->setWidget(widget);
g_debugger_window->addDockWidget(dock, KDDockWidgets::Location_OnTop);
}
return true;
}
void DockManager::switchToLayout(DockLayout::Index layout_index, bool blink_tab)
{
if (layout_index != m_current_layout)
{
if (m_current_layout != DockLayout::INVALID_INDEX)
{
DockLayout& layout = m_layouts.at(m_current_layout);
layout.freeze();
layout.save(m_current_layout);
}
// Clear out the existing positions of toolbars so they don't affect
// where new toolbars appear for other layouts.
if (g_debugger_window)
g_debugger_window->clearToolBarState();
updateToolBarLockState();
m_current_layout = layout_index;
if (m_current_layout != DockLayout::INVALID_INDEX)
{
DockLayout& layout = m_layouts.at(m_current_layout);
layout.thaw();
int tab_index = static_cast<int>(layout_index);
if (tab_index >= 0 && m_menu_bar)
m_menu_bar->onCurrentLayoutChanged(layout_index);
}
}
if (blink_tab && m_menu_bar)
m_menu_bar->startBlink(m_current_layout);
}
bool DockManager::switchToLayoutWithCPU(BreakPointCpu cpu, bool blink_tab)
{
// Don't interrupt the user if the current layout already has the right CPU.
if (m_current_layout != DockLayout::INVALID_INDEX && m_layouts.at(m_current_layout).cpu() == cpu)
{
switchToLayout(m_current_layout, blink_tab);
return true;
}
for (DockLayout::Index i = 0; i < m_layouts.size(); i++)
{
if (m_layouts[i].cpu() == cpu)
{
switchToLayout(i, blink_tab);
return true;
}
}
return false;
}
void DockManager::loadLayouts()
{
m_layouts.clear();
// Load the layouts.
FileSystem::FindResultsArray files;
FileSystem::FindFiles(
EmuFolders::DebuggerLayouts.c_str(),
"*.json",
FILESYSTEM_FIND_FILES | FILESYSTEM_FIND_HIDDEN_FILES,
&files);
bool needs_reset = false;
std::vector<DockLayout::Index> indices_last_session;
for (const FILESYSTEM_FIND_DATA& ffd : files)
{
DockLayout::LoadResult result;
DockLayout::Index index_last_session = DockLayout::INVALID_INDEX;
DockLayout::Index index =
createLayout(ffd.FileName, result, index_last_session);
DockLayout& layout = m_layouts.at(index);
// Try to make sure the layout has a unique name.
const QString& name = layout.name();
QString new_name = name;
if (result == DockLayout::SUCCESS || result == DockLayout::DEFAULT_LAYOUT_HASH_MISMATCH)
{
for (int i = 2; hasNameConflict(new_name, index) && i < 100; i++)
{
if (i == 99)
{
result = DockLayout::CONFLICTING_NAME;
break;
}
new_name = QString("%1 #%2").arg(name).arg(i);
}
}
needs_reset |= result != DockLayout::SUCCESS;
if (result != DockLayout::SUCCESS && result != DockLayout::DEFAULT_LAYOUT_HASH_MISMATCH)
{
deleteLayout(index);
// Only delete the file if we've identified that it's actually a
// layout file.
if (result == DockLayout::MAJOR_VERSION_MISMATCH || result == DockLayout::CONFLICTING_NAME)
FileSystem::DeleteFilePath(ffd.FileName.c_str());
continue;
}
if (new_name != name)
{
layout.setName(new_name);
layout.save(index);
}
indices_last_session.emplace_back(index_last_session);
}
// Make sure the layouts remain in the same order they were in previously.
std::vector<size_t> layout_indices;
for (size_t i = 0; i < m_layouts.size(); i++)
layout_indices.emplace_back(i);
std::sort(layout_indices.begin(), layout_indices.end(),
[&indices_last_session](size_t lhs, size_t rhs) {
DockLayout::Index lhs_index_last_session = indices_last_session.at(lhs);
DockLayout::Index rhs_index_last_session = indices_last_session.at(rhs);
return lhs_index_last_session < rhs_index_last_session;
});
bool order_changed = false;
std::vector<DockLayout> sorted_layouts;
for (size_t i = 0; i < layout_indices.size(); i++)
{
if (i != indices_last_session[layout_indices[i]])
order_changed = true;
sorted_layouts.emplace_back(std::move(m_layouts[layout_indices[i]]));
}
m_layouts = std::move(sorted_layouts);
if (m_layouts.empty() || needs_reset)
resetDefaultLayouts();
else
updateLayoutSwitcher();
// Make sure the indices in the existing layout files match up with the
// indices of any new layouts.
if (order_changed)
saveLayouts();
}
bool DockManager::saveLayouts()
{
for (DockLayout::Index i = 0; i < m_layouts.size(); i++)
if (!m_layouts[i].save(i))
return false;
return true;
}
bool DockManager::saveCurrentLayout()
{
if (m_current_layout == DockLayout::INVALID_INDEX)
return true;
return m_layouts.at(m_current_layout).save(m_current_layout);
}
void DockManager::resetAllLayouts()
{
switchToLayout(DockLayout::INVALID_INDEX);
for (DockLayout& layout : m_layouts)
layout.deleteFile();
m_layouts.clear();
for (const DockTables::DefaultDockLayout& layout : DockTables::DEFAULT_DOCK_LAYOUTS)
{
QString name = QCoreApplication::translate("DebuggerLayout", layout.name.c_str());
createLayout(name, layout.cpu, true, layout.name);
}
switchToLayout(0);
updateLayoutSwitcher();
saveLayouts();
}
void DockManager::resetDefaultLayouts()
{
switchToLayout(DockLayout::INVALID_INDEX);
std::vector<DockLayout> old_layouts = std::move(m_layouts);
m_layouts = std::vector<DockLayout>();
for (const DockTables::DefaultDockLayout& layout : DockTables::DEFAULT_DOCK_LAYOUTS)
{
QString name = QCoreApplication::translate("DebuggerLayout", layout.name.c_str());
createLayout(name, layout.cpu, true, layout.name);
}
for (DockLayout& layout : old_layouts)
if (!layout.isDefault())
m_layouts.emplace_back(std::move(layout));
else
layout.deleteFile();
switchToLayout(0);
updateLayoutSwitcher();
saveLayouts();
}
void DockManager::createToolsMenu(QMenu* menu)
{
menu->clear();
if (m_current_layout == DockLayout::INVALID_INDEX || !g_debugger_window)
return;
for (QToolBar* widget : g_debugger_window->findChildren<QToolBar*>())
{
QAction* action = menu->addAction(widget->windowTitle());
action->setText(widget->windowTitle());
action->setCheckable(true);
action->setChecked(widget->isVisible());
connect(action, &QAction::triggered, this, [widget]() {
widget->setVisible(!widget->isVisible());
});
menu->addAction(action);
}
}
void DockManager::createWindowsMenu(QMenu* menu)
{
menu->clear();
if (m_current_layout == DockLayout::INVALID_INDEX)
return;
DockLayout& layout = m_layouts.at(m_current_layout);
// Create a menu that allows for multiple dock widgets of the same type to
// be opened.
QMenu* add_another_menu = menu->addMenu(tr("Add Another..."));
std::vector<DebuggerView*> add_another_widgets;
std::set<std::string> add_another_types;
for (const auto& [unique_name, widget] : layout.debuggerViews())
{
std::string type = widget->metaObject()->className();
if (widget->supportsMultipleInstances() && !add_another_types.contains(type))
{
add_another_widgets.emplace_back(widget);
add_another_types.emplace(type);
}
}
std::sort(add_another_widgets.begin(), add_another_widgets.end(),
[](const DebuggerView* lhs, const DebuggerView* rhs) {
if (lhs->displayNameWithoutSuffix() == rhs->displayNameWithoutSuffix())
return lhs->displayNameSuffixNumber() < rhs->displayNameSuffixNumber();
return lhs->displayNameWithoutSuffix() < rhs->displayNameWithoutSuffix();
});
for (DebuggerView* widget : add_another_widgets)
{
const char* type = widget->metaObject()->className();
const auto description_iterator = DockTables::DEBUGGER_VIEWS.find(type);
pxAssert(description_iterator != DockTables::DEBUGGER_VIEWS.end());
QAction* action = add_another_menu->addAction(description_iterator->second.display_name);
connect(action, &QAction::triggered, this, [this, type]() {
if (m_current_layout == DockLayout::INVALID_INDEX)
return;
m_layouts.at(m_current_layout).createDebuggerView(type);
});
}
if (add_another_widgets.empty())
add_another_menu->setDisabled(true);
menu->addSeparator();
struct DebuggerViewToggle
{
QString display_name;
std::optional<int> suffix_number;
QAction* action;
};
std::vector<DebuggerViewToggle> toggles;
std::set<std::string> toggle_types;
// Create a menu item for each open debugger view.
for (const auto& pair : layout.debuggerViews())
{
DebuggerView* widget = pair.second.get();
QAction* action = new QAction(menu);
action->setText(widget->displayName());
action->setCheckable(true);
action->setChecked(true);
connect(action, &QAction::triggered, this, [this, unique_name = pair.first]() {
if (m_current_layout == DockLayout::INVALID_INDEX)
return;
m_layouts.at(m_current_layout).destroyDebuggerView(unique_name);
});
DebuggerViewToggle& toggle = toggles.emplace_back();
toggle.display_name = widget->displayNameWithoutSuffix();
toggle.suffix_number = widget->displayNameSuffixNumber();
toggle.action = action;
toggle_types.emplace(widget->metaObject()->className());
}
// Create menu items to open debugger views without any open instances.
for (const auto& pair : DockTables::DEBUGGER_VIEWS)
{
const std::string& type = pair.first;
const DockTables::DebuggerViewDescription& desc = pair.second;
if (!toggle_types.contains(type))
{
QString display_name = QCoreApplication::translate("DebuggerView", desc.display_name);
QAction* action = new QAction(menu);
action->setText(display_name);
action->setCheckable(true);
action->setChecked(false);
connect(action, &QAction::triggered, this, [this, type]() {
if (m_current_layout == DockLayout::INVALID_INDEX)
return;
m_layouts.at(m_current_layout).createDebuggerView(type);
});
DebuggerViewToggle& toggle = toggles.emplace_back();
toggle.display_name = display_name;
toggle.suffix_number = std::nullopt;
toggle.action = action;
}
}
std::sort(toggles.begin(), toggles.end(),
[](const DebuggerViewToggle& lhs, const DebuggerViewToggle& rhs) {
if (lhs.display_name == rhs.display_name)
return lhs.suffix_number < rhs.suffix_number;
return lhs.display_name < rhs.display_name;
});
for (const DebuggerViewToggle& toggle : toggles)
menu->addAction(toggle.action);
}
QWidget* DockManager::createMenuBar(QWidget* original_menu_bar)
{
pxAssert(!m_menu_bar);
m_menu_bar = new DockMenuBar(original_menu_bar);
connect(m_menu_bar, &DockMenuBar::currentLayoutChanged, this, [this](DockLayout::Index layout_index) {
if (layout_index >= m_layouts.size())
return;
switchToLayout(layout_index);
});
connect(m_menu_bar, &DockMenuBar::newButtonClicked, this, &DockManager::newLayoutClicked);
connect(m_menu_bar, &DockMenuBar::layoutMoved, this, &DockManager::layoutSwitcherTabMoved);
connect(m_menu_bar, &DockMenuBar::lockButtonToggled, this, &DockManager::setLayoutLockedAndSaveSetting);
connect(m_menu_bar, &DockMenuBar::layoutSwitcherContextMenuRequested,
this, &DockManager::openLayoutSwitcherContextMenu);
updateLayoutSwitcher();
bool layout_locked = Host::GetBaseBoolSettingValue("Debugger/UserInterface", "LayoutLocked", true);
setLayoutLocked(layout_locked, false);
return m_menu_bar;
}
void DockManager::updateLayoutSwitcher()
{
if (m_menu_bar)
m_menu_bar->updateLayoutSwitcher(m_current_layout, m_layouts);
}
void DockManager::newLayoutClicked()
{
// The plus button has just been made the current tab, so set it back to the
// one corresponding to the current layout again.
if (m_menu_bar)
m_menu_bar->onCurrentLayoutChanged(m_current_layout);
auto name_validator = [this](const QString& name) {
return !hasNameConflict(name, DockLayout::INVALID_INDEX);
};
bool can_clone_current_layout = m_current_layout != DockLayout::INVALID_INDEX;
QPointer<LayoutEditorDialog> dialog = new LayoutEditorDialog(
name_validator, can_clone_current_layout, g_debugger_window);
if (dialog->exec() == QDialog::Accepted && name_validator(dialog->name()))
{
DockLayout::Index new_layout = DockLayout::INVALID_INDEX;
const auto [mode, index] = dialog->initialState();
switch (mode)
{
case LayoutEditorDialog::DEFAULT_LAYOUT:
{
const DockTables::DefaultDockLayout& default_layout = DockTables::DEFAULT_DOCK_LAYOUTS.at(index);
new_layout = createLayout(dialog->name(), dialog->cpu(), false, default_layout.name);
break;
}
case LayoutEditorDialog::BLANK_LAYOUT:
{
new_layout = createLayout(dialog->name(), dialog->cpu(), false);
break;
}
case LayoutEditorDialog::CLONE_LAYOUT:
{
if (m_current_layout == DockLayout::INVALID_INDEX)
break;
DockLayout::Index old_layout = m_current_layout;
// Freeze the current layout so we can copy the geometry.
switchToLayout(DockLayout::INVALID_INDEX);
new_layout = createLayout(dialog->name(), dialog->cpu(), false, m_layouts.at(old_layout));
break;
}
}
if (new_layout != DockLayout::INVALID_INDEX)
{
updateLayoutSwitcher();
switchToLayout(new_layout);
}
}
delete dialog.get();
}
void DockManager::openLayoutSwitcherContextMenu(const QPoint& pos, QTabBar* layout_switcher)
{
DockLayout::Index layout_index = static_cast<DockLayout::Index>(layout_switcher->tabAt(pos));
if (layout_index >= m_layouts.size())
return;
DockLayout& layout = m_layouts[layout_index];
QMenu* menu = new QMenu(layout_switcher);
menu->setAttribute(Qt::WA_DeleteOnClose);
QAction* edit_action = menu->addAction(tr("Edit Layout"));
connect(edit_action, &QAction::triggered, [this, layout_index]() {
editLayoutClicked(layout_index);
});
QAction* reset_action = menu->addAction(tr("Reset Layout"));
reset_action->setEnabled(layout.canReset());
reset_action->connect(reset_action, &QAction::triggered, [this, layout_index]() {
resetLayoutClicked(layout_index);
});
QAction* delete_action = menu->addAction(tr("Delete Layout"));
connect(delete_action, &QAction::triggered, [this, layout_index]() {
deleteLayoutClicked(layout_index);
});
menu->popup(layout_switcher->mapToGlobal(pos));
}
void DockManager::editLayoutClicked(DockLayout::Index layout_index)
{
if (layout_index >= m_layouts.size())
return;
DockLayout& layout = m_layouts[layout_index];
auto name_validator = [this, layout_index](const QString& name) {
return !hasNameConflict(name, layout_index);
};
QPointer<LayoutEditorDialog> dialog = new LayoutEditorDialog(
layout.name(), layout.cpu(), name_validator, g_debugger_window);
if (dialog->exec() != QDialog::Accepted || !name_validator(dialog->name()))
return;
layout.setName(dialog->name());
layout.setCpu(dialog->cpu());
layout.save(layout_index);
delete dialog.get();
updateLayoutSwitcher();
}
void DockManager::resetLayoutClicked(DockLayout::Index layout_index)
{
if (layout_index >= m_layouts.size())
return;
DockLayout& layout = m_layouts[layout_index];
if (!layout.canReset())
return;
QString text = tr("Are you sure you want to reset layout '%1'?").arg(layout.name());
if (QMessageBox::question(g_debugger_window, tr("Confirmation"), text) != QMessageBox::Yes)
return;
bool current_layout = layout_index == m_current_layout;
if (current_layout)
switchToLayout(DockLayout::INVALID_INDEX);
layout.reset();
layout.save(layout_index);
if (current_layout)
switchToLayout(layout_index);
}
void DockManager::deleteLayoutClicked(DockLayout::Index layout_index)
{
if (layout_index >= m_layouts.size())
return;
DockLayout& layout = m_layouts[layout_index];
QString text = tr("Are you sure you want to delete layout '%1'?").arg(layout.name());
if (QMessageBox::question(g_debugger_window, tr("Confirmation"), text) != QMessageBox::Yes)
return;
deleteLayout(layout_index);
updateLayoutSwitcher();
}
void DockManager::layoutSwitcherTabMoved(DockLayout::Index from_index, DockLayout::Index to_index)
{
if (from_index >= m_layouts.size() || to_index >= m_layouts.size())
{
// This happens when the user tries to move a layout to the right of the
// plus button.
updateLayoutSwitcher();
return;
}
DockLayout& from_layout = m_layouts[from_index];
DockLayout& to_layout = m_layouts[to_index];
std::swap(from_layout, to_layout);
from_layout.save(from_index);
to_layout.save(to_index);
if (from_index == m_current_layout)
m_current_layout = to_index;
else if (to_index == m_current_layout)
m_current_layout = from_index;
}
bool DockManager::hasNameConflict(const QString& name, DockLayout::Index layout_index)
{
std::string safe_name = Path::SanitizeFileName(name.toStdString());
for (DockLayout::Index i = 0; i < m_layouts.size(); i++)
{
std::string other_safe_name = Path::SanitizeFileName(m_layouts[i].name().toStdString());
if (i != layout_index && StringUtil::compareNoCase(other_safe_name, safe_name))
return true;
}
return false;
}
void DockManager::updateDockWidgetTitles()
{
if (m_current_layout == DockLayout::INVALID_INDEX)
return;
m_layouts.at(m_current_layout).updateDockWidgetTitles();
}
const std::map<QString, QPointer<DebuggerView>>& DockManager::debuggerViews()
{
static std::map<QString, QPointer<DebuggerView>> dummy;
if (m_current_layout == DockLayout::INVALID_INDEX)
return dummy;
return m_layouts.at(m_current_layout).debuggerViews();
}
size_t DockManager::countDebuggerViewsOfType(const char* type)
{
if (m_current_layout == DockLayout::INVALID_INDEX)
return 0;
return m_layouts.at(m_current_layout).countDebuggerViewsOfType(type);
}
void DockManager::recreateDebuggerView(const QString& unique_name)
{
if (m_current_layout == DockLayout::INVALID_INDEX)
return;
m_layouts.at(m_current_layout).recreateDebuggerView(unique_name);
}
void DockManager::destroyDebuggerView(const QString& unique_name)
{
if (m_current_layout == DockLayout::INVALID_INDEX)
return;
m_layouts.at(m_current_layout).destroyDebuggerView(unique_name);
}
void DockManager::setPrimaryDebuggerView(DebuggerView* widget, bool is_primary)
{
if (m_current_layout == DockLayout::INVALID_INDEX)
return;
m_layouts.at(m_current_layout).setPrimaryDebuggerView(widget, is_primary);
}
void DockManager::switchToDebuggerView(DebuggerView* widget)
{
if (m_current_layout == DockLayout::INVALID_INDEX)
return;
for (const auto& [unique_name, test_widget] : m_layouts.at(m_current_layout).debuggerViews())
{
if (widget == test_widget)
{
auto [controller, view] = DockUtils::dockWidgetFromName(unique_name);
controller->setAsCurrentTab();
break;
}
}
}
void DockManager::updateTheme()
{
if (m_menu_bar)
m_menu_bar->updateTheme();
for (DockLayout& layout : m_layouts)
for (const auto& [unique_name, widget] : layout.debuggerViews())
widget->updateStyleSheet();
// KDDockWidgets::QtWidgets::TabBar sets its own style to a subclass of
// QProxyStyle in its constructor, so we need to update that here.
for (KDDockWidgets::Core::Group* group : KDDockWidgets::DockRegistry::self()->groups())
{
auto tab_bar = static_cast<KDDockWidgets::QtWidgets::TabBar*>(group->tabBar()->view());
if (QProxyStyle* style = qobject_cast<QProxyStyle*>(tab_bar->style()))
style->setBaseStyle(QStyleFactory::create(qApp->style()->name()));
}
}
bool DockManager::isLayoutLocked()
{
return m_layout_locked;
}
void DockManager::setLayoutLockedAndSaveSetting(bool locked)
{
setLayoutLocked(locked, true);
}
void DockManager::setLayoutLocked(bool locked, bool save_setting)
{
m_layout_locked = locked;
if (m_menu_bar)
m_menu_bar->onLockStateChanged(locked);
updateToolBarLockState();
for (KDDockWidgets::Core::Group* group : KDDockWidgets::DockRegistry::self()->groups())
{
auto stack = static_cast<KDDockWidgets::QtWidgets::Stack*>(group->stack()->view());
stack->setTabsClosable(!m_layout_locked);
// HACK: Make sure the sizes of the tabs get updated.
if (stack->tabBar()->count() > 0)
stack->tabBar()->setTabText(0, stack->tabBar()->tabText(0));
}
if (save_setting)
{
Host::SetBaseBoolSettingValue("Debugger/UserInterface", "LayoutLocked", m_layout_locked);
Host::CommitBaseSettingChanges();
}
}
void DockManager::updateToolBarLockState()
{
if (!g_debugger_window)
return;
for (QToolBar* toolbar : g_debugger_window->findChildren<QToolBar*>())
toolbar->setMovable(!m_layout_locked || toolbar->isFloating());
}
std::optional<BreakPointCpu> DockManager::cpu()
{
if (m_current_layout == DockLayout::INVALID_INDEX)
return std::nullopt;
return m_layouts.at(m_current_layout).cpu();
}
KDDockWidgets::Core::DockWidget* DockManager::dockWidgetFactory(const QString& name)
{
if (!g_debugger_window)
return nullptr;
DockManager& manager = g_debugger_window->dockManager();
if (manager.m_current_layout == DockLayout::INVALID_INDEX)
return nullptr;
return manager.m_layouts.at(manager.m_current_layout).createDockWidget(name);
}
bool DockManager::dragAboutToStart(KDDockWidgets::Core::Draggable* draggable)
{
bool locked = true;
if (g_debugger_window)
locked = g_debugger_window->dockManager().isLayoutLocked();
KDDockWidgets::Config::self().setDropIndicatorsInhibited(locked);
if (draggable->isInProgrammaticDrag())
return true;
// Allow floating windows to be dragged around even if the layout is locked.
if (draggable->isWindow())
return true;
if (!g_debugger_window)
return false;
return !locked;
}

View File

@@ -0,0 +1,111 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#pragma once
#include "Debugger/Docking/DockLayout.h"
#include "Debugger/Docking/DockMenuBar.h"
#include <kddockwidgets/MainWindow.h>
#include <kddockwidgets/DockWidget.h>
#include <kddockwidgets/core/DockRegistry.h>
#include <kddockwidgets/core/DockWidget.h>
#include <kddockwidgets/core/Draggable_p.h>
#include <QtCore/QPointer>
#include <QtWidgets/QPushButton>
#include <QtWidgets/QTabBar>
class DockManager : public QObject
{
Q_OBJECT
public:
DockManager(QObject* parent = nullptr);
DockManager(const DockManager& rhs) = delete;
DockManager& operator=(const DockManager& rhs) = delete;
DockManager(DockManager&& rhs) = delete;
DockManager& operator=(DockManager&&) = delete;
// This needs to be called before any KDDockWidgets objects are created
// including the debugger window itself.
static void configureDockingSystem();
template <typename... Args>
DockLayout::Index createLayout(Args&&... args)
{
DockLayout::Index layout_index = m_layouts.size();
if (m_layouts.empty())
{
// Delete the placeholder created in DockManager::deleteLayout.
for (KDDockWidgets::Core::DockWidget* dock : KDDockWidgets::DockRegistry::self()->dockwidgets())
delete dock;
}
m_layouts.emplace_back(std::forward<Args>(args)..., layout_index);
return layout_index;
}
bool deleteLayout(DockLayout::Index layout_index);
void switchToLayout(DockLayout::Index layout_index, bool blink_tab = false);
bool switchToLayoutWithCPU(BreakPointCpu cpu, bool blink_tab = false);
void loadLayouts();
bool saveLayouts();
bool saveCurrentLayout();
QString currentLayoutName();
bool canResetCurrentLayout();
void resetCurrentLayout();
void resetDefaultLayouts();
void resetAllLayouts();
void createToolsMenu(QMenu* menu);
void createWindowsMenu(QMenu* menu);
QWidget* createMenuBar(QWidget* original_menu_bar);
void updateLayoutSwitcher();
void newLayoutClicked();
void openLayoutSwitcherContextMenu(const QPoint& pos, QTabBar* layout_switcher);
void editLayoutClicked(DockLayout::Index layout_index);
void resetLayoutClicked(DockLayout::Index layout_index);
void deleteLayoutClicked(DockLayout::Index layout_index);
void layoutSwitcherTabMoved(DockLayout::Index from_index, DockLayout::Index to_index);
bool hasNameConflict(const QString& name, DockLayout::Index layout_index);
void updateDockWidgetTitles();
const std::map<QString, QPointer<DebuggerView>>& debuggerViews();
size_t countDebuggerViewsOfType(const char* type);
void recreateDebuggerView(const QString& unique_name);
void destroyDebuggerView(const QString& unique_name);
void setPrimaryDebuggerView(DebuggerView* widget, bool is_primary);
void switchToDebuggerView(DebuggerView* widget);
void updateTheme();
bool isLayoutLocked();
void setLayoutLockedAndSaveSetting(bool locked);
void setLayoutLocked(bool locked, bool save_setting);
void updateToolBarLockState();
std::optional<BreakPointCpu> cpu();
private:
static KDDockWidgets::Core::DockWidget* dockWidgetFactory(const QString& name);
static bool dragAboutToStart(KDDockWidgets::Core::Draggable* draggable);
std::vector<DockLayout> m_layouts;
DockLayout::Index m_current_layout = DockLayout::INVALID_INDEX;
DockMenuBar* m_menu_bar = nullptr;
bool m_layout_locked = true;
};

View File

@@ -0,0 +1,342 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#include "DockMenuBar.h"
#include <QtCore/QTimer>
#include <QtGui/QPainter>
#include <QtGui/QPaintEvent>
#include <QtWidgets/QBoxLayout>
#include <QtWidgets/QStyleFactory>
#include <QtWidgets/QStyleOption>
static const int OUTER_MENU_MARGIN = 2;
static const int INNER_MENU_MARGIN = 4;
DockMenuBar::DockMenuBar(QWidget* original_menu_bar, QWidget* parent)
: QWidget(parent)
, m_original_menu_bar(original_menu_bar)
{
QHBoxLayout* layout = new QHBoxLayout;
layout->setContentsMargins(0, OUTER_MENU_MARGIN, OUTER_MENU_MARGIN, 0);
setLayout(layout);
QWidget* menu_wrapper = new QWidget;
menu_wrapper->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Preferred);
layout->addWidget(menu_wrapper);
QHBoxLayout* menu_layout = new QHBoxLayout;
menu_layout->setContentsMargins(0, INNER_MENU_MARGIN, 0, INNER_MENU_MARGIN);
menu_wrapper->setLayout(menu_layout);
menu_layout->addWidget(original_menu_bar);
m_layout_switcher = new QTabBar;
m_layout_switcher->setContentsMargins(0, 0, 0, 0);
m_layout_switcher->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred);
m_layout_switcher->setContextMenuPolicy(Qt::CustomContextMenu);
m_layout_switcher->setDrawBase(false);
m_layout_switcher->setExpanding(false);
m_layout_switcher->setMovable(true);
layout->addWidget(m_layout_switcher);
connect(m_layout_switcher, &QTabBar::tabMoved, this, [this](int from, int to) {
DockLayout::Index from_index = static_cast<DockLayout::Index>(from);
DockLayout::Index to_index = static_cast<DockLayout::Index>(to);
emit layoutMoved(from_index, to_index);
});
connect(m_layout_switcher, &QTabBar::customContextMenuRequested, this, [this](const QPoint& pos) {
emit layoutSwitcherContextMenuRequested(pos, m_layout_switcher);
});
m_blink_timer = new QTimer(this);
connect(m_blink_timer, &QTimer::timeout, this, &DockMenuBar::updateBlink);
m_layout_locked_toggle = new QPushButton;
m_layout_locked_toggle->setCheckable(true);
connect(m_layout_locked_toggle, &QPushButton::clicked, this, [this](bool checked) {
if (m_ignore_lock_state_changed)
return;
emit lockButtonToggled(checked);
});
layout->addWidget(m_layout_locked_toggle);
updateTheme();
}
void DockMenuBar::updateTheme()
{
DockMenuBarStyle* style = new DockMenuBarStyle(m_layout_switcher);
m_original_menu_bar->setStyle(style);
m_layout_switcher->setStyle(style);
m_layout_locked_toggle->setStyle(style);
delete m_style;
m_style = style;
}
void DockMenuBar::updateLayoutSwitcher(DockLayout::Index current_index, const std::vector<DockLayout>& layouts)
{
disconnect(m_tab_connection);
for (int i = m_layout_switcher->count(); i > 0; i--)
m_layout_switcher->removeTab(i - 1);
for (const DockLayout& layout : layouts)
{
const char* cpu_name = DebugInterface::cpuName(layout.cpu());
QString tab_name = QString("%1 (%2)").arg(layout.name()).arg(cpu_name);
m_layout_switcher->addTab(tab_name);
}
m_plus_tab_index = m_layout_switcher->addTab("+");
m_current_tab_index = current_index;
if (current_index != DockLayout::INVALID_INDEX)
m_layout_switcher->setCurrentIndex(current_index);
else
m_layout_switcher->setCurrentIndex(m_plus_tab_index);
// If we don't have any layouts, the currently selected tab will never be
// changed, so we respond to all clicks instead.
if (m_plus_tab_index > 0)
m_tab_connection = connect(m_layout_switcher, &QTabBar::currentChanged, this, &DockMenuBar::tabChanged);
else
m_tab_connection = connect(m_layout_switcher, &QTabBar::tabBarClicked, this, &DockMenuBar::tabChanged);
stopBlink();
}
void DockMenuBar::onCurrentLayoutChanged(DockLayout::Index current_index)
{
m_ignore_current_tab_changed = true;
if (current_index != DockLayout::INVALID_INDEX)
m_layout_switcher->setCurrentIndex(current_index);
else
m_layout_switcher->setCurrentIndex(m_plus_tab_index);
m_ignore_current_tab_changed = false;
}
void DockMenuBar::onLockStateChanged(bool layout_locked)
{
m_ignore_lock_state_changed = true;
m_layout_locked_toggle->setChecked(layout_locked);
if (layout_locked)
{
m_layout_locked_toggle->setText(tr("Layout Locked"));
m_layout_locked_toggle->setIcon(QIcon::fromTheme(QString::fromUtf8("padlock-lock")));
}
else
{
m_layout_locked_toggle->setText(tr("Layout Unlocked"));
m_layout_locked_toggle->setIcon(QIcon::fromTheme(QString::fromUtf8("padlock-unlock")));
}
m_ignore_lock_state_changed = false;
}
void DockMenuBar::startBlink(DockLayout::Index layout_index)
{
stopBlink();
if (layout_index == DockLayout::INVALID_INDEX)
return;
m_blink_tab = static_cast<int>(layout_index);
m_blink_stage = 0;
m_blink_timer->start(500);
updateBlink();
}
void DockMenuBar::updateBlink()
{
if (m_blink_tab < m_layout_switcher->count())
{
if (m_blink_stage % 2 == 0)
m_layout_switcher->setTabTextColor(m_blink_tab, Qt::red);
else
m_layout_switcher->setTabTextColor(m_blink_tab, m_layout_switcher->palette().text().color());
}
m_blink_stage++;
if (m_blink_stage > 7)
m_blink_timer->stop();
}
void DockMenuBar::stopBlink()
{
if (m_blink_timer->isActive())
{
if (m_blink_tab < m_layout_switcher->count())
m_layout_switcher->setTabTextColor(m_blink_tab, m_layout_switcher->palette().text().color());
m_blink_timer->stop();
}
}
int DockMenuBar::innerHeight() const
{
return m_original_menu_bar->sizeHint().height() + INNER_MENU_MARGIN * 2;
}
void DockMenuBar::paintEvent(QPaintEvent* event)
{
QPainter painter(this);
// This fixes the background colour of the menu bar when using the Windows
// Vista style.
QStyleOptionMenuItem menu_option;
menu_option.palette = palette();
menu_option.state = QStyle::State_None;
menu_option.menuItemType = QStyleOptionMenuItem::EmptyArea;
menu_option.checkType = QStyleOptionMenuItem::NotCheckable;
menu_option.rect = rect();
menu_option.menuRect = rect();
style()->drawControl(QStyle::CE_MenuBarEmptyArea, &menu_option, &painter, this);
}
void DockMenuBar::tabChanged(int index)
{
// Prevent recursion.
if (m_ignore_current_tab_changed)
return;
if (index < m_plus_tab_index)
{
DockLayout::Index layout_index = static_cast<DockLayout::Index>(index);
emit currentLayoutChanged(layout_index);
}
else if (index == m_plus_tab_index)
{
emit newButtonClicked();
}
}
// *****************************************************************************
DockMenuBarStyle::DockMenuBarStyle(QObject* parent)
: QProxyStyle(QStyleFactory::create(qApp->style()->name()))
{
setParent(parent);
}
void DockMenuBarStyle::drawControl(
ControlElement element,
const QStyleOption* option,
QPainter* painter,
const QWidget* widget) const
{
switch (element)
{
case CE_MenuBarItem:
{
const QStyleOptionMenuItem* opt = qstyleoption_cast<const QStyleOptionMenuItem*>(option);
if (!opt)
break;
QWidget* menu_wrapper = widget->parentWidget();
if (!menu_wrapper)
break;
const DockMenuBar* menu_bar = qobject_cast<const DockMenuBar*>(menu_wrapper->parentWidget());
if (!menu_bar)
break;
if (baseStyle()->name() != "fusion")
break;
// This mirrors a check in QFusionStyle::drawControl. If act is
// false, QFusionStyle will try to draw a border along the bottom.
bool act = opt->state & State_Selected && opt->state & State_Sunken;
if (act)
break;
// Extend the menu item to the bottom of the menu bar to fix the
// position in which it draws its bottom border. We also need to
// extend it up by the same amount so that the text isn't moved.
QStyleOptionMenuItem menu_opt = *opt;
int difference = (menu_bar->innerHeight() - option->rect.top()) - menu_opt.rect.height();
menu_opt.rect.adjust(0, -difference, 0, difference);
QProxyStyle::drawControl(element, &menu_opt, painter, widget);
return;
}
case CE_TabBarTab:
{
QProxyStyle::drawControl(element, option, painter, widget);
// Draw a slick-looking highlight under the currently selected tab.
if (baseStyle()->name() == "fusion")
{
const QStyleOptionTab* tab = qstyleoption_cast<const QStyleOptionTab*>(option);
if (tab && (tab->state & State_Selected))
{
painter->setPen(tab->palette.highlight().color());
painter->drawLine(tab->rect.bottomLeft(), tab->rect.bottomRight());
}
}
return;
}
case CE_MenuBarEmptyArea:
{
// Prevent it from drawing a border in the wrong position.
return;
}
default:
{
break;
}
}
QProxyStyle::drawControl(element, option, painter, widget);
}
QSize DockMenuBarStyle::sizeFromContents(
QStyle::ContentsType type, const QStyleOption* option, const QSize& contents_size, const QWidget* widget) const
{
QSize size = QProxyStyle::sizeFromContents(type, option, contents_size, widget);
#ifdef Q_OS_WIN32
// Adjust the sizes of the layout switcher tabs depending on the theme.
if (type == CT_TabBarTab)
{
const QStyleOptionTab* opt = qstyleoption_cast<const QStyleOptionTab*>(option);
if (!opt)
return size;
const QTabBar* tab_bar = qobject_cast<const QTabBar*>(widget);
if (!tab_bar)
return size;
const DockMenuBar* menu_bar = qobject_cast<const DockMenuBar*>(tab_bar->parentWidget());
if (!menu_bar)
return size;
if (baseStyle()->name() == "fusion" || baseStyle()->name() == "windowsvista")
{
// Make sure the tab extends to the bottom of the widget.
size.setHeight(menu_bar->innerHeight() - opt->rect.top());
}
else if (baseStyle()->name() == "windows11")
{
// Adjust the size of the tab such that it is vertically centred.
size.setHeight(menu_bar->innerHeight() - opt->rect.top() * 2 - OUTER_MENU_MARGIN);
// Make the plus button square.
if (opt->tabIndex + 1 == tab_bar->count())
size.setWidth(size.height());
}
}
#endif
return size;
}

View File

@@ -0,0 +1,93 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#pragma once
#include "Debugger/Docking/DockLayout.h"
#include <QtWidgets/QMenuBar>
#include <QtWidgets/QProxyStyle>
#include <QtWidgets/QPushButton>
#include <QtWidgets/QTabBar>
#include <QtWidgets/QWidget>
class DockMenuBarStyle;
// The widget that replaces the normal menu bar. This contains the original menu
// bar, the layout switcher and the layout locked/unlocked toggle button.
class DockMenuBar : public QWidget
{
Q_OBJECT
public:
DockMenuBar(QWidget* original_menu_bar, QWidget* parent = nullptr);
void updateLayoutSwitcher(DockLayout::Index current_index, const std::vector<DockLayout>& layouts);
void updateTheme();
// Notify the menu bar that a new layout has been selected.
void onCurrentLayoutChanged(DockLayout::Index current_index);
// Notify the menu bar that the layout has been locked/unlocked.
void onLockStateChanged(bool layout_locked);
void startBlink(DockLayout::Index layout_index);
void updateBlink();
void stopBlink();
int innerHeight() const;
Q_SIGNALS:
void currentLayoutChanged(DockLayout::Index layout_index);
void newButtonClicked();
void layoutMoved(DockLayout::Index from_index, DockLayout::Index to_index);
void lockButtonToggled(bool locked);
void layoutSwitcherContextMenuRequested(const QPoint& pos, QTabBar* layout_switcher);
protected:
void paintEvent(QPaintEvent* event) override;
private:
void tabChanged(int index);
QWidget* m_original_menu_bar;
QTabBar* m_layout_switcher;
QMetaObject::Connection m_tab_connection;
int m_plus_tab_index = -1;
int m_current_tab_index = -1;
bool m_ignore_current_tab_changed = false;
QTimer* m_blink_timer = nullptr;
int m_blink_tab = 0;
int m_blink_stage = 0;
QPushButton* m_layout_locked_toggle;
bool m_ignore_lock_state_changed = false;
DockMenuBarStyle* m_style = nullptr;
};
// Fixes some theming issues relating to the menu bar, the layout switcher and
// the layout locked/unlocked toggle button.
class DockMenuBarStyle : public QProxyStyle
{
Q_OBJECT
public:
DockMenuBarStyle(QObject* parent = nullptr);
void drawControl(
ControlElement element,
const QStyleOption* option,
QPainter* painter,
const QWidget* widget = nullptr) const override;
QSize sizeFromContents(
QStyle::ContentsType type,
const QStyleOption* option,
const QSize& contents_size,
const QWidget* widget = nullptr) const override;
};

View File

@@ -0,0 +1,209 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#include "DockTables.h"
#include "Debugger/DebuggerEvents.h"
#include "Debugger/DisassemblyView.h"
#include "Debugger/RegisterView.h"
#include "Debugger/StackView.h"
#include "Debugger/ThreadView.h"
#include "Debugger/Breakpoints/BreakpointView.h"
#include "Debugger/Memory/MemorySearchView.h"
#include "Debugger/Memory/MemoryView.h"
#include "Debugger/Memory/SavedAddressesView.h"
#include "Debugger/SymbolTree/SymbolTreeViews.h"
using namespace DockUtils;
static void hashDefaultLayout(const DockTables::DefaultDockLayout& layout, u32& hash);
static void hashDefaultGroup(const DockTables::DefaultDockGroupDescription& group, u32& hash);
static void hashDefaultDockWidget(const DockTables::DefaultDockWidgetDescription& widget, u32& hash);
static void hashNumber(u32 number, u32& hash);
static void hashString(const char* string, u32& hash);
#define DEBUGGER_VIEW(type, display_name, preferred_location) \
{ \
#type, \
{ \
[](const DebuggerViewParameters& parameters) -> DebuggerView* { \
DebuggerView* widget = new type(parameters); \
widget->handleEvent(DebuggerEvents::Refresh()); \
return widget; \
}, \
display_name, \
preferred_location \
} \
}
const std::map<std::string, DockTables::DebuggerViewDescription> DockTables::DEBUGGER_VIEWS = {
DEBUGGER_VIEW(BreakpointView, QT_TRANSLATE_NOOP("DebuggerView", "Breakpoints"), BOTTOM_MIDDLE),
DEBUGGER_VIEW(DisassemblyView, QT_TRANSLATE_NOOP("DebuggerView", "Disassembly"), TOP_RIGHT),
DEBUGGER_VIEW(FunctionTreeView, QT_TRANSLATE_NOOP("DebuggerView", "Functions"), TOP_LEFT),
DEBUGGER_VIEW(GlobalVariableTreeView, QT_TRANSLATE_NOOP("DebuggerView", "Globals"), BOTTOM_MIDDLE),
DEBUGGER_VIEW(LocalVariableTreeView, QT_TRANSLATE_NOOP("DebuggerView", "Locals"), BOTTOM_MIDDLE),
DEBUGGER_VIEW(MemorySearchView, QT_TRANSLATE_NOOP("DebuggerView", "Memory Search"), TOP_LEFT),
DEBUGGER_VIEW(MemoryView, QT_TRANSLATE_NOOP("DebuggerView", "Memory"), BOTTOM_MIDDLE),
DEBUGGER_VIEW(ParameterVariableTreeView, QT_TRANSLATE_NOOP("DebuggerView", "Parameters"), BOTTOM_MIDDLE),
DEBUGGER_VIEW(RegisterView, QT_TRANSLATE_NOOP("DebuggerView", "Registers"), TOP_LEFT),
DEBUGGER_VIEW(SavedAddressesView, QT_TRANSLATE_NOOP("DebuggerView", "Saved Addresses"), BOTTOM_MIDDLE),
DEBUGGER_VIEW(StackView, QT_TRANSLATE_NOOP("DebuggerView", "Stack"), BOTTOM_MIDDLE),
DEBUGGER_VIEW(ThreadView, QT_TRANSLATE_NOOP("DebuggerView", "Threads"), BOTTOM_MIDDLE),
};
#undef DEBUGGER_VIEW
const std::vector<DockTables::DefaultDockLayout> DockTables::DEFAULT_DOCK_LAYOUTS = {
{
.name = QT_TRANSLATE_NOOP("DebuggerLayout", "R5900"),
.cpu = BREAKPOINT_EE,
.groups = {
/* [DefaultDockGroup::TOP_RIGHT] = */ {KDDockWidgets::Location_OnRight, DefaultDockGroup::ROOT},
/* [DefaultDockGroup::BOTTOM] = */ {KDDockWidgets::Location_OnBottom, DefaultDockGroup::TOP_RIGHT},
/* [DefaultDockGroup::TOP_LEFT] = */ {KDDockWidgets::Location_OnLeft, DefaultDockGroup::TOP_RIGHT},
},
.widgets = {
/* DefaultDockGroup::TOP_RIGHT */
{"DisassemblyView", DefaultDockGroup::TOP_RIGHT},
/* DefaultDockGroup::BOTTOM */
{"MemoryView", DefaultDockGroup::BOTTOM},
{"BreakpointView", DefaultDockGroup::BOTTOM},
{"ThreadView", DefaultDockGroup::BOTTOM},
{"StackView", DefaultDockGroup::BOTTOM},
{"SavedAddressesView", DefaultDockGroup::BOTTOM},
{"GlobalVariableTreeView", DefaultDockGroup::BOTTOM},
{"LocalVariableTreeView", DefaultDockGroup::BOTTOM},
{"ParameterVariableTreeView", DefaultDockGroup::BOTTOM},
/* DefaultDockGroup::TOP_LEFT */
{"RegisterView", DefaultDockGroup::TOP_LEFT},
{"FunctionTreeView", DefaultDockGroup::TOP_LEFT},
{"MemorySearchView", DefaultDockGroup::TOP_LEFT},
},
.toolbars = {
"toolBarDebug",
"toolBarFile",
},
},
{
.name = QT_TRANSLATE_NOOP("DebuggerLayout", "R3000"),
.cpu = BREAKPOINT_IOP,
.groups = {
/* [DefaultDockGroup::TOP_RIGHT] = */ {KDDockWidgets::Location_OnRight, DefaultDockGroup::ROOT},
/* [DefaultDockGroup::BOTTOM] = */ {KDDockWidgets::Location_OnBottom, DefaultDockGroup::TOP_RIGHT},
/* [DefaultDockGroup::TOP_LEFT] = */ {KDDockWidgets::Location_OnLeft, DefaultDockGroup::TOP_RIGHT},
},
.widgets = {
/* DefaultDockGroup::TOP_RIGHT */
{"DisassemblyView", DefaultDockGroup::TOP_RIGHT},
/* DefaultDockGroup::BOTTOM */
{"MemoryView", DefaultDockGroup::BOTTOM},
{"BreakpointView", DefaultDockGroup::BOTTOM},
{"ThreadView", DefaultDockGroup::BOTTOM},
{"StackView", DefaultDockGroup::BOTTOM},
{"SavedAddressesView", DefaultDockGroup::BOTTOM},
{"GlobalVariableTreeView", DefaultDockGroup::BOTTOM},
{"LocalVariableTreeView", DefaultDockGroup::BOTTOM},
{"ParameterVariableTreeView", DefaultDockGroup::BOTTOM},
/* DefaultDockGroup::TOP_LEFT */
{"RegisterView", DefaultDockGroup::TOP_LEFT},
{"FunctionTreeView", DefaultDockGroup::TOP_LEFT},
{"MemorySearchView", DefaultDockGroup::TOP_LEFT},
},
.toolbars = {
"toolBarDebug",
"toolBarFile",
},
},
};
const DockTables::DefaultDockLayout* DockTables::defaultLayout(const std::string& name)
{
for (const DockTables::DefaultDockLayout& default_layout : DockTables::DEFAULT_DOCK_LAYOUTS)
if (default_layout.name == name)
return &default_layout;
return nullptr;
}
u32 DockTables::hashDefaultLayouts()
{
static std::optional<u32> hash;
if (hash.has_value())
return *hash;
hash.emplace(0);
u32 hash_version = 2;
hashNumber(hash_version, *hash);
hashNumber(static_cast<u32>(DEFAULT_DOCK_LAYOUTS.size()), *hash);
for (const DefaultDockLayout& layout : DEFAULT_DOCK_LAYOUTS)
hashDefaultLayout(layout, *hash);
return *hash;
}
static void hashDefaultLayout(const DockTables::DefaultDockLayout& layout, u32& hash)
{
hashString(layout.name.c_str(), hash);
hashString(DebugInterface::cpuName(layout.cpu), hash);
hashNumber(static_cast<u32>(layout.groups.size()), hash);
for (const DockTables::DefaultDockGroupDescription& group : layout.groups)
hashDefaultGroup(group, hash);
hashNumber(static_cast<u32>(layout.widgets.size()), hash);
for (const DockTables::DefaultDockWidgetDescription& widget : layout.widgets)
hashDefaultDockWidget(widget, hash);
hashNumber(static_cast<u32>(layout.toolbars.size()), hash);
for (const std::string& toolbar : layout.toolbars)
hashString(toolbar.c_str(), hash);
}
static void hashDefaultGroup(const DockTables::DefaultDockGroupDescription& group, u32& hash)
{
// This is inline here so that it's obvious that changing it will affect the
// result of the hash.
const char* location = "";
switch (group.location)
{
case KDDockWidgets::Location_None:
location = "none";
break;
case KDDockWidgets::Location_OnLeft:
location = "left";
break;
case KDDockWidgets::Location_OnTop:
location = "top";
break;
case KDDockWidgets::Location_OnRight:
location = "right";
break;
case KDDockWidgets::Location_OnBottom:
location = "bottom";
break;
}
hashString(location, hash);
hashNumber(static_cast<u32>(group.parent), hash);
}
static void hashDefaultDockWidget(const DockTables::DefaultDockWidgetDescription& widget, u32& hash)
{
hashString(widget.type.c_str(), hash);
hashNumber(static_cast<u32>(widget.group), hash);
}
static void hashNumber(u32 number, u32& hash)
{
hash = hash * 31 + number;
}
static void hashString(const char* string, u32& hash)
{
u32 size = static_cast<u32>(strlen(string));
hash = hash * 31 + size;
for (u32 i = 0; i < size; i++)
hash = hash * 31 + string[i];
}

View File

@@ -0,0 +1,71 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#pragma once
#include "DockUtils.h"
#include "DebugTools/DebugInterface.h"
#include <kddockwidgets/KDDockWidgets.h>
class MD5Digest;
class DebuggerView;
struct DebuggerViewParameters;
namespace DockTables
{
struct DebuggerViewDescription
{
DebuggerView* (*create_widget)(const DebuggerViewParameters& parameters);
// The untranslated string displayed as the dock widget tab text.
const char* display_name;
// This is used to determine which group dock widgets of this type are
// added to when they're opened from the Windows menu.
DockUtils::PreferredLocation preferred_location;
};
extern const std::map<std::string, DebuggerViewDescription> DEBUGGER_VIEWS;
enum class DefaultDockGroup
{
ROOT = -1,
TOP_RIGHT = 0,
BOTTOM = 1,
TOP_LEFT = 2
};
struct DefaultDockGroupDescription
{
KDDockWidgets::Location location;
DefaultDockGroup parent;
};
extern const std::vector<DefaultDockGroupDescription> DEFAULT_DOCK_GROUPS;
struct DefaultDockWidgetDescription
{
std::string type;
DefaultDockGroup group;
};
struct DefaultDockLayout
{
std::string name;
BreakPointCpu cpu;
std::vector<DefaultDockGroupDescription> groups;
std::vector<DefaultDockWidgetDescription> widgets;
std::set<std::string> toolbars;
};
extern const std::vector<DefaultDockLayout> DEFAULT_DOCK_LAYOUTS;
const DefaultDockLayout* defaultLayout(const std::string& name);
// This is used to determine if the user has updated and we need to recreate
// the default layouts.
u32 hashDefaultLayouts();
} // namespace DockTables

View File

@@ -0,0 +1,98 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#include "DockUtils.h"
#include <kddockwidgets/Config.h>
#include <kddockwidgets/core/DockRegistry.h>
#include <kddockwidgets/core/Group.h>
#include <kddockwidgets/qtwidgets/DockWidget.h>
#include <kddockwidgets/qtwidgets/Group.h>
DockUtils::DockWidgetPair DockUtils::dockWidgetFromName(const QString& unique_name)
{
KDDockWidgets::Vector<QString> names{unique_name};
KDDockWidgets::Vector<KDDockWidgets::Core::DockWidget*> dock_widgets =
KDDockWidgets::DockRegistry::self()->dockWidgets(names);
if (dock_widgets.size() != 1 || !dock_widgets[0])
return {};
return {dock_widgets[0], static_cast<KDDockWidgets::QtWidgets::DockWidget*>(dock_widgets[0]->view())};
}
void DockUtils::insertDockWidgetAtPreferredLocation(
KDDockWidgets::Core::DockWidget* dock_widget,
PreferredLocation location,
KDDockWidgets::QtWidgets::MainWindow* window)
{
int width = window->width();
int height = window->height();
int half_width = width / 2;
int half_height = height / 2;
QPoint preferred_location;
switch (location)
{
case DockUtils::TOP_LEFT:
preferred_location = {0, 0};
break;
case DockUtils::TOP_MIDDLE:
preferred_location = {half_width, 0};
break;
case DockUtils::TOP_RIGHT:
preferred_location = {width, 0};
break;
case DockUtils::MIDDLE_LEFT:
preferred_location = {0, half_height};
break;
case DockUtils::MIDDLE_MIDDLE:
preferred_location = {half_width, half_height};
break;
case DockUtils::MIDDLE_RIGHT:
preferred_location = {width, half_height};
break;
case DockUtils::BOTTOM_LEFT:
preferred_location = {0, height};
break;
case DockUtils::BOTTOM_MIDDLE:
preferred_location = {half_width, height};
break;
case DockUtils::BOTTOM_RIGHT:
preferred_location = {width, height};
break;
}
// Find the dock group which is closest to the preferred location.
KDDockWidgets::Core::Group* best_group = nullptr;
int best_distance_squared = 0;
for (KDDockWidgets::Core::Group* group_controller : KDDockWidgets::DockRegistry::self()->groups())
{
if (group_controller->isFloating())
continue;
auto group = static_cast<KDDockWidgets::QtWidgets::Group*>(group_controller->view());
QPoint local_midpoint = group->pos() + QPoint(group->width() / 2, group->height() / 2);
QPoint midpoint = group->mapTo(window, local_midpoint);
QPoint delta = midpoint - preferred_location;
int distance_squared = delta.x() * delta.x() + delta.y() * delta.y();
if (!best_group || distance_squared < best_distance_squared)
{
best_group = group_controller;
best_distance_squared = distance_squared;
}
}
if (best_group && best_group->dockWidgetCount() > 0)
{
KDDockWidgets::Core::DockWidget* other_dock_widget = best_group->dockWidgetAt(0);
other_dock_widget->addDockWidgetAsTab(dock_widget);
}
else
{
auto dock_view = static_cast<KDDockWidgets::QtWidgets::DockWidget*>(dock_widget->view());
window->addDockWidget(dock_view, KDDockWidgets::Location_OnTop);
}
}

View File

@@ -0,0 +1,40 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#pragma once
#include <kddockwidgets/KDDockWidgets.h>
#include <kddockwidgets/core/DockWidget.h>
#include <kddockwidgets/qtwidgets/MainWindow.h>
namespace DockUtils
{
inline const constexpr int MAX_LAYOUT_NAME_SIZE = 40;
inline const constexpr int MAX_DOCK_WIDGET_NAME_SIZE = 40;
struct DockWidgetPair
{
KDDockWidgets::Core::DockWidget* controller = nullptr;
KDDockWidgets::QtWidgets::DockWidget* view = nullptr;
};
DockWidgetPair dockWidgetFromName(const QString& unique_name);
enum PreferredLocation
{
TOP_LEFT,
TOP_MIDDLE,
TOP_RIGHT,
MIDDLE_LEFT,
MIDDLE_MIDDLE,
MIDDLE_RIGHT,
BOTTOM_LEFT,
BOTTOM_MIDDLE,
BOTTOM_RIGHT
};
void insertDockWidgetAtPreferredLocation(
KDDockWidgets::Core::DockWidget* dock_widget,
PreferredLocation location,
KDDockWidgets::QtWidgets::MainWindow* window);
} // namespace DockUtils

View File

@@ -0,0 +1,314 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#include "DockViews.h"
#include "QtUtils.h"
#include "Debugger/DebuggerView.h"
#include "Debugger/DebuggerWindow.h"
#include "Debugger/Docking/DockManager.h"
#include "Debugger/Docking/DropIndicators.h"
#include "DebugTools/DebugInterface.h"
#include <kddockwidgets/Config.h>
#include <kddockwidgets/core/TabBar.h>
#include <kddockwidgets/qtwidgets/views/DockWidget.h>
#include <QtGui/QActionGroup>
#include <QtGui/QPalette>
#include <QtWidgets/QInputDialog>
#include <QtWidgets/QMessageBox>
#include <QtWidgets/QMenu>
#include <QtWidgets/QStyleFactory>
KDDockWidgets::Core::View* DockViewFactory::createDockWidget(
const QString& unique_name,
KDDockWidgets::DockWidgetOptions options,
KDDockWidgets::LayoutSaverOptions layout_saver_options,
Qt::WindowFlags window_flags) const
{
return new DockWidget(unique_name, options, layout_saver_options, window_flags);
}
KDDockWidgets::Core::View* DockViewFactory::createTitleBar(
KDDockWidgets::Core::TitleBar* controller,
KDDockWidgets::Core::View* parent) const
{
return new DockTitleBar(controller, parent);
}
KDDockWidgets::Core::View* DockViewFactory::createStack(
KDDockWidgets::Core::Stack* controller,
KDDockWidgets::Core::View* parent) const
{
return new DockStack(controller, KDDockWidgets::QtCommon::View_qt::asQWidget(parent));
}
KDDockWidgets::Core::View* DockViewFactory::createTabBar(
KDDockWidgets::Core::TabBar* tabBar,
KDDockWidgets::Core::View* parent) const
{
return new DockTabBar(tabBar, KDDockWidgets::QtCommon::View_qt::asQWidget(parent));
}
KDDockWidgets::Core::ClassicIndicatorWindowViewInterface* DockViewFactory::createClassicIndicatorWindow(
KDDockWidgets::Core::ClassicDropIndicatorOverlay* classic_indicators,
KDDockWidgets::Core::View* parent) const
{
return new DockDropIndicatorProxy(classic_indicators);
}
KDDockWidgets::Core::ClassicIndicatorWindowViewInterface* DockViewFactory::createFallbackClassicIndicatorWindow(
KDDockWidgets::Core::ClassicDropIndicatorOverlay* classic_indicators,
KDDockWidgets::Core::View* parent) const
{
return KDDockWidgets::QtWidgets::ViewFactory::createClassicIndicatorWindow(classic_indicators, parent);
}
KDDockWidgets::Core::View* DockViewFactory::createSegmentedDropIndicatorOverlayView(
KDDockWidgets::Core::SegmentedDropIndicatorOverlay* controller,
KDDockWidgets::Core::View* parent) const
{
return new DockSegmentedDropIndicatorOverlay(controller, KDDockWidgets::QtCommon::View_qt::asQWidget(parent));
}
// *****************************************************************************
DockWidget::DockWidget(
const QString& unique_name,
KDDockWidgets::DockWidgetOptions options,
KDDockWidgets::LayoutSaverOptions layout_saver_options,
Qt::WindowFlags window_flags)
: KDDockWidgets::QtWidgets::DockWidget(unique_name, options, layout_saver_options, window_flags)
{
connect(this, &DockWidget::isOpenChanged, this, &DockWidget::openStateChanged);
}
void DockWidget::openStateChanged(bool open)
{
// The LayoutSaver class will close a bunch of dock widgets. We only want to
// delete the dock widgets when they're being closed by the user.
if (KDDockWidgets::LayoutSaver::restoreInProgress())
return;
if (!open && g_debugger_window)
g_debugger_window->dockManager().destroyDebuggerView(uniqueName());
}
// *****************************************************************************
DockTitleBar::DockTitleBar(KDDockWidgets::Core::TitleBar* controller, KDDockWidgets::Core::View* parent)
: KDDockWidgets::QtWidgets::TitleBar(controller, parent)
{
}
void DockTitleBar::mouseDoubleClickEvent(QMouseEvent* event)
{
if (g_debugger_window && !g_debugger_window->dockManager().isLayoutLocked())
KDDockWidgets::QtWidgets::TitleBar::mouseDoubleClickEvent(event);
else
event->ignore();
}
// *****************************************************************************
DockStack::DockStack(KDDockWidgets::Core::Stack* controller, QWidget* parent)
: KDDockWidgets::QtWidgets::Stack(controller, parent)
{
}
void DockStack::init()
{
KDDockWidgets::QtWidgets::Stack::init();
if (g_debugger_window)
{
bool locked = g_debugger_window->dockManager().isLayoutLocked();
setTabsClosable(!locked);
}
}
void DockStack::mouseDoubleClickEvent(QMouseEvent* ev)
{
if (g_debugger_window && !g_debugger_window->dockManager().isLayoutLocked())
KDDockWidgets::QtWidgets::Stack::mouseDoubleClickEvent(ev);
else
ev->ignore();
}
// *****************************************************************************
DockTabBar::DockTabBar(KDDockWidgets::Core::TabBar* controller, QWidget* parent)
: KDDockWidgets::QtWidgets::TabBar(controller, parent)
{
setContextMenuPolicy(Qt::CustomContextMenu);
connect(this, &DockTabBar::customContextMenuRequested, this, &DockTabBar::openContextMenu);
// The constructor of KDDockWidgets::QtWidgets::TabBar makes a QProxyStyle
// that ends up taking ownerhsip of the style for the entire application!
if (QProxyStyle* proxy_style = qobject_cast<QProxyStyle*>(style()))
{
if (proxy_style->baseStyle() == qApp->style())
proxy_style->baseStyle()->setParent(qApp);
proxy_style->setBaseStyle(QStyleFactory::create(qApp->style()->name()));
}
}
void DockTabBar::openContextMenu(QPoint pos)
{
if (!g_debugger_window)
return;
int tab_index = tabAt(pos);
// Filter out the placeholder widget displayed when there are no layouts.
auto [widget, controller, view] = widgetsFromTabIndex(tab_index);
if (!widget)
return;
size_t dock_widgets_of_type = g_debugger_window->dockManager().countDebuggerViewsOfType(
widget->metaObject()->className());
QMenu* menu = new QMenu(this);
menu->setAttribute(Qt::WA_DeleteOnClose);
QAction* rename_action = menu->addAction(tr("Rename"));
connect(rename_action, &QAction::triggered, this, [this, tab_index]() {
if (!g_debugger_window)
return;
auto [widget, controller, view] = widgetsFromTabIndex(tab_index);
if (!widget)
return;
bool ok;
QString new_name = QInputDialog::getText(
this, tr("Rename Window"), tr("New name:"), QLineEdit::Normal, widget->displayNameWithoutSuffix(), &ok);
if (!ok)
return;
if (!widget->setCustomDisplayName(new_name))
{
QMessageBox::warning(this, tr("Invalid Name"), tr("The specified name is too long."));
return;
}
g_debugger_window->dockManager().updateDockWidgetTitles();
});
QAction* reset_name_action = menu->addAction(tr("Reset Name"));
reset_name_action->setEnabled(!widget->customDisplayName().isEmpty());
connect(reset_name_action, &QAction::triggered, this, [this, tab_index] {
if (!g_debugger_window)
return;
auto [widget, controller, view] = widgetsFromTabIndex(tab_index);
if (!widget)
return;
widget->setCustomDisplayName(QString());
g_debugger_window->dockManager().updateDockWidgetTitles();
});
QAction* primary_action = menu->addAction(tr("Primary"));
primary_action->setCheckable(true);
primary_action->setChecked(widget->isPrimary());
primary_action->setEnabled(dock_widgets_of_type > 1);
connect(primary_action, &QAction::triggered, this, [this, tab_index](bool checked) {
if (!g_debugger_window)
return;
auto [widget, controller, view] = widgetsFromTabIndex(tab_index);
if (!widget)
return;
g_debugger_window->dockManager().setPrimaryDebuggerView(widget, checked);
});
QMenu* set_target_menu = menu->addMenu(tr("Set Target"));
QActionGroup* set_target_group = new QActionGroup(menu);
set_target_group->setExclusive(true);
for (BreakPointCpu cpu : DEBUG_CPUS)
{
const char* long_cpu_name = DebugInterface::longCpuName(cpu);
const char* cpu_name = DebugInterface::cpuName(cpu);
QString text = QString("%1 (%2)").arg(long_cpu_name).arg(cpu_name);
QAction* cpu_action = set_target_menu->addAction(text);
cpu_action->setCheckable(true);
cpu_action->setChecked(widget->cpuOverride().has_value() && *widget->cpuOverride() == cpu);
connect(cpu_action, &QAction::triggered, this, [this, tab_index, cpu]() {
setCpuOverrideForTab(tab_index, cpu);
});
set_target_group->addAction(cpu_action);
}
set_target_menu->addSeparator();
QAction* inherit_action = set_target_menu->addAction(tr("Inherit From Layout"));
inherit_action->setCheckable(true);
inherit_action->setChecked(!widget->cpuOverride().has_value());
connect(inherit_action, &QAction::triggered, this, [this, tab_index]() {
setCpuOverrideForTab(tab_index, std::nullopt);
});
set_target_group->addAction(inherit_action);
QAction* close_action = menu->addAction(tr("Close"));
connect(close_action, &QAction::triggered, this, [this, tab_index]() {
if (!g_debugger_window)
return;
auto [widget, controller, view] = widgetsFromTabIndex(tab_index);
if (!widget)
return;
g_debugger_window->dockManager().destroyDebuggerView(widget->uniqueName());
});
menu->popup(mapToGlobal(pos));
}
void DockTabBar::setCpuOverrideForTab(int tab_index, std::optional<BreakPointCpu> cpu_override)
{
if (!g_debugger_window)
return;
auto [widget, controller, view] = widgetsFromTabIndex(tab_index);
if (!widget)
return;
if (!widget->setCpuOverride(cpu_override))
g_debugger_window->dockManager().recreateDebuggerView(view->uniqueName());
g_debugger_window->dockManager().updateDockWidgetTitles();
}
DockTabBar::WidgetsFromTabIndexResult DockTabBar::widgetsFromTabIndex(int tab_index)
{
KDDockWidgets::Core::TabBar* tab_bar_controller = asController<KDDockWidgets::Core::TabBar>();
if (!tab_bar_controller)
return {};
KDDockWidgets::Core::DockWidget* dock_controller = tab_bar_controller->dockWidgetAt(tab_index);
if (!dock_controller)
return {};
auto dock_view = static_cast<KDDockWidgets::QtWidgets::DockWidget*>(dock_controller->view());
DebuggerView* widget = qobject_cast<DebuggerView*>(dock_view->widget());
if (!widget)
return {};
return {widget, dock_controller, dock_view};
}
void DockTabBar::mouseDoubleClickEvent(QMouseEvent* event)
{
if (g_debugger_window && !g_debugger_window->dockManager().isLayoutLocked())
KDDockWidgets::QtWidgets::TabBar::mouseDoubleClickEvent(event);
else
event->ignore();
}

View File

@@ -0,0 +1,113 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#pragma once
#include "DebugTools/DebugInterface.h"
#include <kddockwidgets/qtwidgets/ViewFactory.h>
#include <kddockwidgets/qtwidgets/views/DockWidget.h>
#include <kddockwidgets/qtwidgets/views/Stack.h>
#include <kddockwidgets/qtwidgets/views/TitleBar.h>
#include <kddockwidgets/qtwidgets/views/TabBar.h>
class DebuggerView;
class DockManager;
class DockViewFactory : public KDDockWidgets::QtWidgets::ViewFactory
{
Q_OBJECT
public:
KDDockWidgets::Core::View* createDockWidget(
const QString& unique_name,
KDDockWidgets::DockWidgetOptions options = {},
KDDockWidgets::LayoutSaverOptions layout_saver_options = {},
Qt::WindowFlags window_flags = {}) const override;
KDDockWidgets::Core::View* createTitleBar(
KDDockWidgets::Core::TitleBar* controller,
KDDockWidgets::Core::View* parent) const override;
KDDockWidgets::Core::View* createStack(
KDDockWidgets::Core::Stack* controller,
KDDockWidgets::Core::View* parent) const override;
KDDockWidgets::Core::View* createTabBar(
KDDockWidgets::Core::TabBar* tabBar,
KDDockWidgets::Core::View* parent) const override;
KDDockWidgets::Core::ClassicIndicatorWindowViewInterface* createClassicIndicatorWindow(
KDDockWidgets::Core::ClassicDropIndicatorOverlay* classic_indicators,
KDDockWidgets::Core::View* parent) const override;
KDDockWidgets::Core::ClassicIndicatorWindowViewInterface* createFallbackClassicIndicatorWindow(
KDDockWidgets::Core::ClassicDropIndicatorOverlay* classic_indicators,
KDDockWidgets::Core::View* parent) const;
KDDockWidgets::Core::View* createSegmentedDropIndicatorOverlayView(
KDDockWidgets::Core::SegmentedDropIndicatorOverlay* controller,
KDDockWidgets::Core::View* parent) const override;
};
class DockWidget : public KDDockWidgets::QtWidgets::DockWidget
{
Q_OBJECT
public:
DockWidget(
const QString& unique_name,
KDDockWidgets::DockWidgetOptions options,
KDDockWidgets::LayoutSaverOptions layout_saver_options,
Qt::WindowFlags window_flags);
protected:
void openStateChanged(bool open);
};
class DockTitleBar : public KDDockWidgets::QtWidgets::TitleBar
{
Q_OBJECT
public:
DockTitleBar(KDDockWidgets::Core::TitleBar* controller, KDDockWidgets::Core::View* parent = nullptr);
protected:
void mouseDoubleClickEvent(QMouseEvent* event) override;
};
class DockStack : public KDDockWidgets::QtWidgets::Stack
{
Q_OBJECT
public:
DockStack(KDDockWidgets::Core::Stack* controller, QWidget* parent = nullptr);
void init() override;
protected:
void mouseDoubleClickEvent(QMouseEvent* event) override;
};
class DockTabBar : public KDDockWidgets::QtWidgets::TabBar
{
Q_OBJECT
public:
DockTabBar(KDDockWidgets::Core::TabBar* controller, QWidget* parent = nullptr);
protected:
void openContextMenu(QPoint pos);
struct WidgetsFromTabIndexResult
{
DebuggerView* widget = nullptr;
KDDockWidgets::Core::DockWidget* controller = nullptr;
KDDockWidgets::QtWidgets::DockWidget* view = nullptr;
};
void setCpuOverrideForTab(int tab_index, std::optional<BreakPointCpu> cpu_override);
WidgetsFromTabIndexResult widgetsFromTabIndex(int tab_index);
void mouseDoubleClickEvent(QMouseEvent* event) override;
};

View File

@@ -0,0 +1,572 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#include "DropIndicators.h"
#include "QtUtils.h"
#include "Debugger/Docking/DockViews.h"
#include "common/Assertions.h"
#include <kddockwidgets/Config.h>
#include <kddockwidgets/core/Group.h>
#include <kddockwidgets/core/Platform.h>
#include <kddockwidgets/core/indicators/SegmentedDropIndicatorOverlay.h>
#include <kddockwidgets/qtwidgets/ViewFactory.h>
#include <QtGui/QPainter>
static std::pair<QColor, QColor> pickNiceColours(const QPalette& palette, bool hovered)
{
QColor fill = palette.highlight().color();
QColor outline = palette.highlight().color();
if (QtUtils::IsLightTheme(palette))
{
fill = fill.darker(200);
outline = outline.darker(200);
}
else
{
fill = fill.lighter(200);
outline = outline.lighter(200);
}
fill.setAlpha(200);
outline.setAlpha(255);
if (!hovered)
{
fill.setAlpha(fill.alpha() / 2);
outline.setAlpha(outline.alpha() / 2);
}
return {fill, outline};
}
// *****************************************************************************
DockDropIndicatorProxy::DockDropIndicatorProxy(KDDockWidgets::Core::ClassicDropIndicatorOverlay* classic_indicators)
: m_classic_indicators(classic_indicators)
{
recreateWindowIfNecessary();
}
DockDropIndicatorProxy::~DockDropIndicatorProxy()
{
delete m_window;
delete m_fallback_window;
}
void DockDropIndicatorProxy::setObjectName(const QString& name)
{
window()->setObjectName(name);
}
KDDockWidgets::DropLocation DockDropIndicatorProxy::hover(QPoint globalPos)
{
return window()->hover(globalPos);
}
QPoint DockDropIndicatorProxy::posForIndicator(KDDockWidgets::DropLocation loc) const
{
return window()->posForIndicator(loc);
}
void DockDropIndicatorProxy::updatePositions()
{
// Check if a compositor is running whenever a drag starts.
recreateWindowIfNecessary();
window()->updatePositions();
}
void DockDropIndicatorProxy::raise()
{
window()->raise();
}
void DockDropIndicatorProxy::setVisible(bool visible)
{
window()->setVisible(visible);
}
void DockDropIndicatorProxy::resize(QSize size)
{
window()->resize(size);
}
void DockDropIndicatorProxy::setGeometry(QRect rect)
{
window()->setGeometry(rect);
}
bool DockDropIndicatorProxy::isWindow() const
{
return window()->isWindow();
}
void DockDropIndicatorProxy::updateIndicatorVisibility()
{
window()->updateIndicatorVisibility();
}
KDDockWidgets::Core::ClassicIndicatorWindowViewInterface* DockDropIndicatorProxy::window()
{
if (!m_supports_compositing)
{
pxAssert(m_fallback_window);
return m_fallback_window;
}
pxAssert(m_window);
return m_window;
}
const KDDockWidgets::Core::ClassicIndicatorWindowViewInterface* DockDropIndicatorProxy::window() const
{
if (!m_supports_compositing)
{
pxAssert(m_fallback_window);
return m_fallback_window;
}
pxAssert(m_window);
return m_window;
}
void DockDropIndicatorProxy::recreateWindowIfNecessary()
{
bool supports_compositing = QtUtils::IsCompositorManagerRunning();
if (supports_compositing == m_supports_compositing && (m_window || m_fallback_window))
return;
m_supports_compositing = supports_compositing;
DockViewFactory* factory = static_cast<DockViewFactory*>(KDDockWidgets::Config::self().viewFactory());
if (supports_compositing)
{
if (!m_window)
m_window = new DockDropIndicatorWindow(m_classic_indicators);
QWidget* old_window = dynamic_cast<QWidget*>(m_fallback_window);
if (old_window)
{
m_window->setObjectName(old_window->objectName());
m_window->setVisible(old_window->isVisible());
m_window->setGeometry(old_window->geometry());
}
delete m_fallback_window;
m_fallback_window = nullptr;
}
else
{
if (!m_fallback_window)
m_fallback_window = factory->createFallbackClassicIndicatorWindow(m_classic_indicators, nullptr);
QWidget* old_window = dynamic_cast<QWidget*>(m_window);
if (old_window)
{
m_window->setObjectName(old_window->objectName());
m_window->setVisible(old_window->isVisible());
m_window->setGeometry(old_window->geometry());
}
delete m_window;
m_window = nullptr;
}
}
// *****************************************************************************
static const constexpr int IND_LEFT = 0;
static const constexpr int IND_TOP = 1;
static const constexpr int IND_RIGHT = 2;
static const constexpr int IND_BOTTOM = 3;
static const constexpr int IND_CENTER = 4;
static const constexpr int IND_OUTER_LEFT = 5;
static const constexpr int IND_OUTER_TOP = 6;
static const constexpr int IND_OUTER_RIGHT = 7;
static const constexpr int IND_OUTER_BOTTOM = 8;
static const constexpr int INDICATOR_SIZE = 40;
static const constexpr int INDICATOR_MARGIN = 10;
static bool isWayland()
{
return KDDockWidgets::Core::Platform::instance()->displayType() ==
KDDockWidgets::Core::Platform::DisplayType::Wayland;
}
static QWidget* parentForIndicatorWindow(KDDockWidgets::Core::ClassicDropIndicatorOverlay* classic_indicators)
{
if (isWayland())
return KDDockWidgets::QtCommon::View_qt::asQWidget(classic_indicators->view());
return nullptr;
}
static Qt::WindowFlags flagsForIndicatorWindow()
{
if (isWayland())
return Qt::Widget;
return Qt::Tool | Qt::BypassWindowManagerHint;
}
DockDropIndicatorWindow::DockDropIndicatorWindow(
KDDockWidgets::Core::ClassicDropIndicatorOverlay* classic_indicators)
: QWidget(parentForIndicatorWindow(classic_indicators), flagsForIndicatorWindow())
, m_classic_indicators(classic_indicators)
, m_indicators({
/* [IND_LEFT] = */ new DockDropIndicator(KDDockWidgets::DropLocation_Left, this),
/* [IND_TOP] = */ new DockDropIndicator(KDDockWidgets::DropLocation_Top, this),
/* [IND_RIGHT] = */ new DockDropIndicator(KDDockWidgets::DropLocation_Right, this),
/* [IND_BOTTOM] = */ new DockDropIndicator(KDDockWidgets::DropLocation_Bottom, this),
/* [IND_CENTER] = */ new DockDropIndicator(KDDockWidgets::DropLocation_Center, this),
/* [IND_OUTER_LEFT] = */ new DockDropIndicator(KDDockWidgets::DropLocation_OutterLeft, this),
/* [IND_OUTER_TOP] = */ new DockDropIndicator(KDDockWidgets::DropLocation_OutterTop, this),
/* [IND_OUTER_RIGHT] = */ new DockDropIndicator(KDDockWidgets::DropLocation_OutterRight, this),
/* [IND_OUTER_BOTTOM] = */ new DockDropIndicator(KDDockWidgets::DropLocation_OutterBottom, this),
})
{
setWindowFlag(Qt::FramelessWindowHint, true);
if (KDDockWidgets::Config::self().flags() & KDDockWidgets::Config::Flag_KeepAboveIfNotUtilityWindow)
setWindowFlag(Qt::WindowStaysOnTopHint, true);
setAttribute(Qt::WA_TranslucentBackground);
}
void DockDropIndicatorWindow::setObjectName(const QString& name)
{
QWidget::setObjectName(name);
}
KDDockWidgets::DropLocation DockDropIndicatorWindow::hover(QPoint globalPos)
{
KDDockWidgets::DropLocation result = KDDockWidgets::DropLocation_None;
for (DockDropIndicator* indicator : m_indicators)
{
if (indicator->isVisible())
{
bool hovered = indicator->rect().contains(indicator->mapFromGlobal(globalPos));
if (hovered != indicator->hovered)
{
indicator->hovered = hovered;
indicator->update();
}
if (hovered)
result = indicator->location;
}
}
return result;
}
QPoint DockDropIndicatorWindow::posForIndicator(KDDockWidgets::DropLocation loc) const
{
for (DockDropIndicator* indicator : m_indicators)
if (indicator->location == loc)
return indicator->mapToGlobal(indicator->rect().center());
return QPoint();
}
void DockDropIndicatorWindow::updatePositions()
{
DockDropIndicator* left = m_indicators[IND_LEFT];
DockDropIndicator* top = m_indicators[IND_TOP];
DockDropIndicator* right = m_indicators[IND_RIGHT];
DockDropIndicator* bottom = m_indicators[IND_BOTTOM];
DockDropIndicator* center = m_indicators[IND_CENTER];
DockDropIndicator* outer_left = m_indicators[IND_OUTER_LEFT];
DockDropIndicator* outer_top = m_indicators[IND_OUTER_TOP];
DockDropIndicator* outer_right = m_indicators[IND_OUTER_RIGHT];
DockDropIndicator* outer_bottom = m_indicators[IND_OUTER_BOTTOM];
QRect r = rect();
int half_indicator_width = INDICATOR_SIZE / 2;
outer_left->move(r.x() + INDICATOR_MARGIN, r.center().y() - half_indicator_width);
outer_bottom->move(r.center().x() - half_indicator_width, r.y() + height() - INDICATOR_SIZE - INDICATOR_MARGIN);
outer_top->move(r.center().x() - half_indicator_width, r.y() + INDICATOR_MARGIN);
outer_right->move(r.x() + width() - INDICATOR_SIZE - INDICATOR_MARGIN, r.center().y() - half_indicator_width);
KDDockWidgets::Core::Group* hovered_group = m_classic_indicators->hoveredGroup();
if (hovered_group)
{
QRect hoveredRect = hovered_group->view()->geometry();
center->move(r.topLeft() + hoveredRect.center() - QPoint(half_indicator_width, half_indicator_width));
top->move(center->pos() - QPoint(0, INDICATOR_SIZE + INDICATOR_MARGIN));
right->move(center->pos() + QPoint(INDICATOR_SIZE + INDICATOR_MARGIN, 0));
bottom->move(center->pos() + QPoint(0, INDICATOR_SIZE + INDICATOR_MARGIN));
left->move(center->pos() - QPoint(INDICATOR_SIZE + INDICATOR_MARGIN, 0));
}
}
void DockDropIndicatorWindow::raise()
{
QWidget::raise();
}
void DockDropIndicatorWindow::setVisible(bool is)
{
QWidget::setVisible(is);
}
void DockDropIndicatorWindow::resize(QSize size)
{
QWidget::resize(size);
}
void DockDropIndicatorWindow::setGeometry(QRect rect)
{
QWidget::setGeometry(rect);
}
bool DockDropIndicatorWindow::isWindow() const
{
return QWidget::isWindow();
}
void DockDropIndicatorWindow::updateIndicatorVisibility()
{
for (DockDropIndicator* indicator : m_indicators)
indicator->setVisible(m_classic_indicators->dropIndicatorVisible(indicator->location));
}
void DockDropIndicatorWindow::resizeEvent(QResizeEvent* ev)
{
QWidget::resizeEvent(ev);
updatePositions();
}
// *****************************************************************************
DockDropIndicator::DockDropIndicator(KDDockWidgets::DropLocation loc, QWidget* parent)
: QWidget(parent)
, location(loc)
{
setFixedSize(INDICATOR_SIZE, INDICATOR_SIZE);
setVisible(true);
}
void DockDropIndicator::paintEvent(QPaintEvent* event)
{
QPainter painter(this);
painter.setRenderHint(QPainter::Antialiasing, true);
auto [fill, outline] = pickNiceColours(palette(), hovered);
painter.setBrush(fill);
QPen pen;
pen.setColor(outline);
pen.setWidth(2);
painter.setPen(pen);
painter.drawRect(rect());
QRectF rf = rect();
QRectF outer = rf.marginsRemoved(QMarginsF(4.f, 4.f, 4.f, 4.f));
QPointF icon_position;
switch (location)
{
case KDDockWidgets::DropLocation_Left:
case KDDockWidgets::DropLocation_OutterLeft:
outer = outer.marginsRemoved(QMarginsF(0.f, 0.f, outer.width() / 2.f, 0.f));
icon_position = rf.marginsRemoved(QMarginsF(rf.width() / 2.f, 0.f, 0.f, 0.f)).center();
break;
case KDDockWidgets::DropLocation_Top:
case KDDockWidgets::DropLocation_OutterTop:
outer = outer.marginsRemoved(QMarginsF(0.f, 0.f, 0.f, outer.width() / 2.f));
icon_position = rf.marginsRemoved(QMarginsF(0.f, rf.width() / 2.f, 0.f, 0.f)).center();
break;
case KDDockWidgets::DropLocation_Right:
case KDDockWidgets::DropLocation_OutterRight:
outer = outer.marginsRemoved(QMarginsF(outer.width() / 2.f, 0.f, 0.f, 0.f));
icon_position = rf.marginsRemoved(QMarginsF(0.f, 0.f, rf.width() / 2.f, 0.f)).center();
break;
case KDDockWidgets::DropLocation_Bottom:
case KDDockWidgets::DropLocation_OutterBottom:
outer = outer.marginsRemoved(QMarginsF(0.f, outer.width() / 2.f, 0.f, 0.f));
icon_position = rf.marginsRemoved(QMarginsF(0.f, 0.f, 0.f, rf.width() / 2.f)).center();
break;
default:
{
}
}
painter.drawRect(outer);
float arrow_size = INDICATOR_SIZE / 10.f;
QPolygonF arrow;
switch (location)
{
case KDDockWidgets::DropLocation_Left:
arrow = {
icon_position + QPointF(-arrow_size, 0.f),
icon_position + QPointF(arrow_size, arrow_size * 2.f),
icon_position + QPointF(arrow_size, -arrow_size * 2.f),
};
break;
case KDDockWidgets::DropLocation_Top:
arrow = {
icon_position + QPointF(0.f, -arrow_size),
icon_position + QPointF(arrow_size * 2.f, arrow_size),
icon_position + QPointF(-arrow_size * 2.f, arrow_size),
};
break;
case KDDockWidgets::DropLocation_Right:
arrow = {
icon_position + QPointF(arrow_size, 0.f),
icon_position + QPointF(-arrow_size, arrow_size * 2.f),
icon_position + QPointF(-arrow_size, -arrow_size * 2.f),
};
break;
case KDDockWidgets::DropLocation_Bottom:
arrow = {
icon_position + QPointF(0.f, arrow_size),
icon_position + QPointF(arrow_size * 2.f, -arrow_size),
icon_position + QPointF(-arrow_size * 2.f, -arrow_size),
};
break;
default:
{
}
}
painter.drawPolygon(arrow);
}
// *****************************************************************************
std::string DockSegmentedDropIndicatorOverlay::s_indicator_style;
DockSegmentedDropIndicatorOverlay::DockSegmentedDropIndicatorOverlay(
KDDockWidgets::Core::SegmentedDropIndicatorOverlay* controller, QWidget* parent)
: KDDockWidgets::QtWidgets::SegmentedDropIndicatorOverlay(controller, parent)
{
}
void DockSegmentedDropIndicatorOverlay::paintEvent(QPaintEvent* event)
{
if (s_indicator_style == "Minimalistic")
drawMinimalistic();
else
drawSegmented();
}
void DockSegmentedDropIndicatorOverlay::drawSegmented()
{
QPainter painter(this);
painter.setRenderHint(QPainter::Antialiasing, true);
KDDockWidgets::Core::SegmentedDropIndicatorOverlay* controller =
asController<KDDockWidgets::Core::SegmentedDropIndicatorOverlay>();
const std::unordered_map<KDDockWidgets::DropLocation, QPolygon>& segments = controller->segments();
for (KDDockWidgets::DropLocation location :
{KDDockWidgets::DropLocation_Left,
KDDockWidgets::DropLocation_Top,
KDDockWidgets::DropLocation_Right,
KDDockWidgets::DropLocation_Bottom,
KDDockWidgets::DropLocation_Center,
KDDockWidgets::DropLocation_OutterLeft,
KDDockWidgets::DropLocation_OutterTop,
KDDockWidgets::DropLocation_OutterRight,
KDDockWidgets::DropLocation_OutterBottom})
{
auto segment = segments.find(location);
if (segment == segments.end() || segment->second.size() < 2)
continue;
bool hovered = segment->second.containsPoint(controller->hoveredPt(), Qt::OddEvenFill);
auto [fill, outline] = pickNiceColours(palette(), hovered);
painter.setBrush(fill);
QPen pen(outline);
pen.setWidth(1);
painter.setPen(pen);
int margin = KDDockWidgets::Core::SegmentedDropIndicatorOverlay::s_segmentGirth * 2;
// Make sure the rectangles don't intersect with each other.
QRect rect;
switch (location)
{
case KDDockWidgets::DropLocation_Top:
case KDDockWidgets::DropLocation_Bottom:
case KDDockWidgets::DropLocation_OutterTop:
case KDDockWidgets::DropLocation_OutterBottom:
{
rect = segment->second.boundingRect().marginsRemoved(QMargins(margin, 4, margin, 4));
break;
}
case KDDockWidgets::DropLocation_Left:
case KDDockWidgets::DropLocation_Right:
case KDDockWidgets::DropLocation_OutterLeft:
case KDDockWidgets::DropLocation_OutterRight:
{
rect = segment->second.boundingRect().marginsRemoved(QMargins(4, margin, 4, margin));
break;
}
default:
{
rect = segment->second.boundingRect().marginsRemoved(QMargins(4, 4, 4, 4));
break;
}
}
painter.drawRect(rect);
}
}
void DockSegmentedDropIndicatorOverlay::drawMinimalistic()
{
QPainter painter(this);
painter.setRenderHint(QPainter::Antialiasing, true);
KDDockWidgets::Core::SegmentedDropIndicatorOverlay* controller =
asController<KDDockWidgets::Core::SegmentedDropIndicatorOverlay>();
const std::unordered_map<KDDockWidgets::DropLocation, QPolygon>& segments = controller->segments();
for (KDDockWidgets::DropLocation location :
{KDDockWidgets::DropLocation_Left,
KDDockWidgets::DropLocation_Top,
KDDockWidgets::DropLocation_Right,
KDDockWidgets::DropLocation_Bottom,
KDDockWidgets::DropLocation_Center,
KDDockWidgets::DropLocation_OutterLeft,
KDDockWidgets::DropLocation_OutterTop,
KDDockWidgets::DropLocation_OutterRight,
KDDockWidgets::DropLocation_OutterBottom})
{
auto segment = segments.find(location);
if (segment == segments.end() || segment->second.size() < 2)
continue;
if (!segment->second.containsPoint(controller->hoveredPt(), Qt::OddEvenFill))
continue;
auto [fill, outline] = pickNiceColours(palette(), true);
painter.setBrush(fill);
QPen pen(outline);
pen.setWidth(1);
painter.setPen(pen);
painter.drawRect(segment->second.boundingRect());
}
}

View File

@@ -0,0 +1,108 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#pragma once
#include <kddockwidgets/core/indicators/ClassicDropIndicatorOverlay.h>
#include <kddockwidgets/core/views/ClassicIndicatorWindowViewInterface.h>
#include <kddockwidgets/qtwidgets/views/SegmentedDropIndicatorOverlay.h>
class DockDropIndicator;
// This switches between our custom drop indicators and KDDockWidget's built-in
// ones on the fly depending on whether or not we have a windowing system that
// supports compositing.
class DockDropIndicatorProxy : public KDDockWidgets::Core::ClassicIndicatorWindowViewInterface
{
public:
DockDropIndicatorProxy(KDDockWidgets::Core::ClassicDropIndicatorOverlay* classic_indicators);
~DockDropIndicatorProxy();
void setObjectName(const QString&) override;
KDDockWidgets::DropLocation hover(QPoint globalPos) override;
QPoint posForIndicator(KDDockWidgets::DropLocation) const override;
void updatePositions() override;
void raise() override;
void setVisible(bool visible) override;
void resize(QSize size) override;
void setGeometry(QRect rect) override;
bool isWindow() const override;
void updateIndicatorVisibility() override;
private:
KDDockWidgets::Core::ClassicIndicatorWindowViewInterface* window();
const KDDockWidgets::Core::ClassicIndicatorWindowViewInterface* window() const;
void recreateWindowIfNecessary();
KDDockWidgets::Core::ClassicIndicatorWindowViewInterface* m_window = nullptr;
KDDockWidgets::Core::ClassicIndicatorWindowViewInterface* m_fallback_window = nullptr;
bool m_supports_compositing = true;
KDDockWidgets::Core::ClassicDropIndicatorOverlay* m_classic_indicators = nullptr;
};
// Our default custom drop indicator implementation. This fits in with PCSX2's
// themes a lot better, but doesn't support windowing systems where compositing
// is disabled (it would show a black screen).
class DockDropIndicatorWindow : public QWidget, public KDDockWidgets::Core::ClassicIndicatorWindowViewInterface
{
Q_OBJECT
public:
DockDropIndicatorWindow(
KDDockWidgets::Core::ClassicDropIndicatorOverlay* classic_indicators);
void setObjectName(const QString& name) override;
KDDockWidgets::DropLocation hover(QPoint globalPos) override;
QPoint posForIndicator(KDDockWidgets::DropLocation loc) const override;
void updatePositions() override;
void raise() override;
void setVisible(bool visible) override;
void resize(QSize size) override;
void setGeometry(QRect rect) override;
bool isWindow() const override;
void updateIndicatorVisibility() override;
protected:
void resizeEvent(QResizeEvent* ev) override;
private:
KDDockWidgets::Core::ClassicDropIndicatorOverlay* m_classic_indicators;
std::vector<DockDropIndicator*> m_indicators;
};
class DockDropIndicator : public QWidget
{
Q_OBJECT
public:
DockDropIndicator(KDDockWidgets::DropLocation loc, QWidget* parent = nullptr);
KDDockWidgets::DropLocation location;
bool hovered = false;
protected:
void paintEvent(QPaintEvent* event) override;
};
// An alternative drop indicator design that can be enabled from the settings
// menu. For this one we don't need to worry about whether compositing is
// supported since it doesn't create its own window.
class DockSegmentedDropIndicatorOverlay : public KDDockWidgets::QtWidgets::SegmentedDropIndicatorOverlay
{
Q_OBJECT
public:
DockSegmentedDropIndicatorOverlay(
KDDockWidgets::Core::SegmentedDropIndicatorOverlay* controller, QWidget* parent = nullptr);
static std::string s_indicator_style;
protected:
void paintEvent(QPaintEvent* event) override;
private:
void drawSegmented();
void drawMinimalistic();
};

View File

@@ -0,0 +1,107 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#include "LayoutEditorDialog.h"
#include "Debugger/Docking/DockTables.h"
#include <QtWidgets/QPushButton>
Q_DECLARE_METATYPE(LayoutEditorDialog::InitialState);
LayoutEditorDialog::LayoutEditorDialog(NameValidator name_validator, bool can_clone_current_layout, QWidget* parent)
: QDialog(parent)
, m_name_validator(name_validator)
{
m_ui.setupUi(this);
setWindowTitle(tr("New Layout"));
setupInputWidgets(BREAKPOINT_EE, can_clone_current_layout);
onNameChanged();
}
LayoutEditorDialog::LayoutEditorDialog(
const QString& name, BreakPointCpu cpu, NameValidator name_validator, QWidget* parent)
: QDialog(parent)
, m_name_validator(name_validator)
{
m_ui.setupUi(this);
setWindowTitle(tr("Edit Layout"));
m_ui.nameEditor->setText(name);
setupInputWidgets(cpu, {});
m_ui.initialStateLabel->hide();
m_ui.initialStateEditor->hide();
onNameChanged();
}
QString LayoutEditorDialog::name()
{
return m_ui.nameEditor->text();
}
BreakPointCpu LayoutEditorDialog::cpu()
{
return static_cast<BreakPointCpu>(m_ui.cpuEditor->currentData().toInt());
}
LayoutEditorDialog::InitialState LayoutEditorDialog::initialState()
{
return m_ui.initialStateEditor->currentData().value<InitialState>();
}
void LayoutEditorDialog::setupInputWidgets(BreakPointCpu cpu, bool can_clone_current_layout)
{
connect(m_ui.nameEditor, &QLineEdit::textChanged, this, &LayoutEditorDialog::onNameChanged);
for (BreakPointCpu cpu : DEBUG_CPUS)
{
const char* long_cpu_name = DebugInterface::longCpuName(cpu);
const char* cpu_name = DebugInterface::cpuName(cpu);
QString text = QString("%1 (%2)").arg(long_cpu_name).arg(cpu_name);
m_ui.cpuEditor->addItem(text, cpu);
}
for (int i = 0; i < m_ui.cpuEditor->count(); i++)
if (m_ui.cpuEditor->itemData(i).toInt() == cpu)
m_ui.cpuEditor->setCurrentIndex(i);
for (size_t i = 0; i < DockTables::DEFAULT_DOCK_LAYOUTS.size(); i++)
m_ui.initialStateEditor->addItem(
tr("Create Default \"%1\" Layout").arg(tr(DockTables::DEFAULT_DOCK_LAYOUTS[i].name.c_str())),
QVariant::fromValue(InitialState(DEFAULT_LAYOUT, i)));
m_ui.initialStateEditor->addItem(tr("Create Blank Layout"), QVariant::fromValue(InitialState(BLANK_LAYOUT, 0)));
if (can_clone_current_layout)
m_ui.initialStateEditor->addItem(tr("Clone Current Layout"), QVariant::fromValue(InitialState(CLONE_LAYOUT, 0)));
m_ui.initialStateEditor->setCurrentIndex(0);
}
void LayoutEditorDialog::onNameChanged()
{
QString error_message;
if (m_ui.nameEditor->text().isEmpty())
{
error_message = tr("Name is empty.");
}
else if (m_ui.nameEditor->text().size() > DockUtils::MAX_LAYOUT_NAME_SIZE)
{
error_message = tr("Name too long.");
}
else if (!m_name_validator(m_ui.nameEditor->text()))
{
error_message = tr("A layout with that name already exists.");
}
m_ui.buttonBox->button(QDialogButtonBox::Ok)->setEnabled(error_message.isEmpty());
m_ui.errorMessage->setText(error_message);
}

View File

@@ -0,0 +1,45 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#pragma once
#include "ui_LayoutEditorDialog.h"
#include "DebugTools/DebugInterface.h"
#include <QtWidgets/QDialog>
class LayoutEditorDialog : public QDialog
{
Q_OBJECT
public:
using NameValidator = std::function<bool(const QString&)>;
enum CreationMode
{
DEFAULT_LAYOUT,
BLANK_LAYOUT,
CLONE_LAYOUT,
};
// Bundles together a creation mode and inital state.
using InitialState = std::pair<CreationMode, size_t>;
// Create a "New Layout" dialog.
LayoutEditorDialog(NameValidator name_validator, bool can_clone_current_layout, QWidget* parent = nullptr);
// Create a "Edit Layout" dialog.
LayoutEditorDialog(const QString& name, BreakPointCpu cpu, NameValidator name_validator, QWidget* parent = nullptr);
QString name();
BreakPointCpu cpu();
InitialState initialState();
private:
void setupInputWidgets(BreakPointCpu cpu, bool can_clone_current_layout);
void onNameChanged();
Ui::LayoutEditorDialog m_ui;
NameValidator m_name_validator;
};

View File

@@ -0,0 +1,115 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>LayoutEditorDialog</class>
<widget class="QDialog" name="LayoutEditorDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>400</width>
<height>150</height>
</rect>
</property>
<property name="windowTitle">
<string/>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<layout class="QFormLayout" name="formLayout">
<item row="0" column="0">
<widget class="QLabel" name="nameLabel">
<property name="text">
<string>Name</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="cpuLabel">
<property name="text">
<string>Target</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="initialStateLabel">
<property name="text">
<string>Initial State</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QComboBox" name="cpuEditor"/>
</item>
<item row="0" column="1">
<widget class="QLineEdit" name="nameEditor"/>
</item>
<item row="2" column="1">
<widget class="QComboBox" name="initialStateEditor"/>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QLabel" name="errorMessage">
<property name="styleSheet">
<string notr="true">color: red</string>
</property>
<property name="text">
<string/>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>LayoutEditorDialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>LayoutEditorDialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View File

@@ -0,0 +1,15 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#include "NoLayoutsWidget.h"
NoLayoutsWidget::NoLayoutsWidget(QWidget* parent)
: QWidget(parent)
{
m_ui.setupUi(this);
}
QPushButton* NoLayoutsWidget::createDefaultLayoutsButton()
{
return m_ui.createDefaultLayoutsButton;
}

View File

@@ -0,0 +1,21 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#pragma once
#include "ui_NoLayoutsWidget.h"
#include <QtWidgets/QPushButton>
class NoLayoutsWidget : public QWidget
{
Q_OBJECT
public:
NoLayoutsWidget(QWidget* parent = nullptr);
QPushButton* createDefaultLayoutsButton();
private:
Ui::NoLayoutsWidget m_ui;
};

View File

@@ -0,0 +1,97 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>NoLayoutsWidget</class>
<widget class="QWidget" name="NoLayoutsWidget">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>400</width>
<height>300</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<property name="autoFillBackground">
<bool>false</bool>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<spacer name="topSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>There are no layouts.</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<spacer name="leftSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="createDefaultLayoutsButton">
<property name="text">
<string>Create Default Layouts</string>
</property>
</widget>
</item>
<item>
<spacer name="rightSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item>
<spacer name="bottomSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>