diff --git a/pcsx2-qt/CMakeLists.txt b/pcsx2-qt/CMakeLists.txt index 322f23ee62..0f0ee4b9a3 100644 --- a/pcsx2-qt/CMakeLists.txt +++ b/pcsx2-qt/CMakeLists.txt @@ -196,6 +196,8 @@ target_sources(pcsx2-qt PRIVATE Debugger/Docking/DockLayout.h Debugger/Docking/DockManager.cpp Debugger/Docking/DockManager.h + Debugger/Docking/DockMenuBar.cpp + Debugger/Docking/DockMenuBar.h Debugger/Docking/DockTables.cpp Debugger/Docking/DockTables.h Debugger/Docking/DockUtils.cpp diff --git a/pcsx2-qt/Debugger/DebuggerWindow.cpp b/pcsx2-qt/Debugger/DebuggerWindow.cpp index 9f4f55c683..a92c800412 100644 --- a/pcsx2-qt/Debugger/DebuggerWindow.cpp +++ b/pcsx2-qt/Debugger/DebuggerWindow.cpp @@ -112,7 +112,7 @@ DebuggerWindow::DebuggerWindow(QWidget* parent) QMenuBar* menu_bar = menuBar(); - setMenuWidget(m_dock_manager->createLayoutSwitcher(menu_bar)); + setMenuWidget(m_dock_manager->createMenuBar(menu_bar)); Host::RunOnCPUThread([]() { R5900SymbolImporter.OnDebuggerOpened(); diff --git a/pcsx2-qt/Debugger/Docking/DockManager.cpp b/pcsx2-qt/Debugger/Docking/DockManager.cpp index e76b031d47..abc0b0386d 100644 --- a/pcsx2-qt/Debugger/Docking/DockManager.cpp +++ b/pcsx2-qt/Debugger/Docking/DockManager.cpp @@ -25,7 +25,6 @@ #include #include #include -#include DockManager::DockManager(QObject* parent) : QObject(parent) @@ -33,9 +32,6 @@ DockManager::DockManager(QObject* parent) QTimer* autosave_timer = new QTimer(this); connect(autosave_timer, &QTimer::timeout, this, &DockManager::saveCurrentLayout); autosave_timer->start(60 * 1000); - - m_blink_timer = new QTimer(this); - connect(m_blink_timer, &QTimer::timeout, this, &DockManager::layoutSwitcherUpdateBlink); } void DockManager::configureDockingSystem() @@ -144,17 +140,13 @@ void DockManager::switchToLayout(DockLayout::Index layout_index, bool blink_tab) layout.thaw(); int tab_index = static_cast(layout_index); - if (m_switcher && tab_index >= 0 && tab_index < m_plus_tab_index) - { - m_ignore_current_tab_changed = true; - m_switcher->setCurrentIndex(tab_index); - m_ignore_current_tab_changed = false; - } + if (m_menu_bar && tab_index >= 0) + m_menu_bar->onCurrentLayoutChanged(layout_index); } } if (blink_tab) - layoutSwitcherStartBlink(); + m_menu_bar->startBlink(m_current_layout); } bool DockManager::switchToLayoutWithCPU(BreakPointCpu cpu, bool blink_tab) @@ -476,168 +468,195 @@ void DockManager::createWindowsMenu(QMenu* menu) menu->addAction(toggle.action); } -QWidget* DockManager::createLayoutSwitcher(QWidget* menu_bar) +QWidget* DockManager::createMenuBar(QWidget* original_menu_bar) { - QWidget* container = new QWidget; + pxAssert(!m_menu_bar); - QHBoxLayout* layout = new QHBoxLayout; - layout->setContentsMargins(0, 2, 2, 0); - container->setLayout(layout); + m_menu_bar = new DockMenuBar(original_menu_bar); - QWidget* menu_wrapper = new QWidget; - menu_wrapper->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Preferred); - layout->addWidget(menu_wrapper); + connect(m_menu_bar, &DockMenuBar::currentLayoutChanged, this, [this](DockLayout::Index layout_index) { + if (layout_index >= m_layouts.size()) + return; - QHBoxLayout* menu_layout = new QHBoxLayout; - menu_layout->setContentsMargins(0, 4, 0, 4); - menu_wrapper->setLayout(menu_layout); - - menu_layout->addWidget(menu_bar); - - m_switcher = new QTabBar; - m_switcher->setContentsMargins(0, 0, 0, 0); - m_switcher->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Preferred); - m_switcher->setContextMenuPolicy(Qt::CustomContextMenu); - m_switcher->setMovable(true); - layout->addWidget(m_switcher); + 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(); - connect(m_switcher, &QTabBar::tabMoved, this, &DockManager::layoutSwitcherTabMoved); - connect(m_switcher, &QTabBar::customContextMenuRequested, this, &DockManager::layoutSwitcherContextMenu); - - QWidget* spacer = new QWidget; - spacer->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); - layout->addWidget(spacer); - bool layout_locked = Host::GetBaseBoolSettingValue("Debugger/UserInterface", "LayoutLocked", true); + setLayoutLocked(layout_locked, false); - QPushButton* lock_layout_toggle = new QPushButton; - lock_layout_toggle->setCheckable(true); - lock_layout_toggle->setChecked(layout_locked); - lock_layout_toggle->setFlat(true); - connect(lock_layout_toggle, &QPushButton::toggled, this, [this, lock_layout_toggle](bool checked) { - setLayoutLocked(checked, lock_layout_toggle, true); - }); - layout->addWidget(lock_layout_toggle); - - setLayoutLocked(layout_locked, lock_layout_toggle, false); - - return container; + return m_menu_bar; } void DockManager::updateLayoutSwitcher() { - if (!m_switcher) - return; - - disconnect(m_tab_connection); - - for (int i = m_switcher->count(); i > 0; i--) - m_switcher->removeTab(i - 1); - - for (DockLayout& layout : m_layouts) - { - const char* cpu_name = DebugInterface::cpuName(layout.cpu()); - QString tab_name = QString("%1 (%2)").arg(layout.name()).arg(cpu_name); - m_switcher->addTab(tab_name); - } - - m_plus_tab_index = m_switcher->addTab("+"); - m_current_tab_index = m_current_layout; - - if (m_current_layout != DockLayout::INVALID_INDEX) - m_switcher->setCurrentIndex(m_current_layout); - - // If we don't have any layouts, the currently selected tab will never be - // changed, so we respond to all clicks instead. - if (!m_layouts.empty()) - m_tab_connection = connect(m_switcher, &QTabBar::currentChanged, this, &DockManager::layoutSwitcherTabChanged); - else - m_tab_connection = connect(m_switcher, &QTabBar::tabBarClicked, this, &DockManager::layoutSwitcherTabChanged); - - layoutSwitcherStopBlink(); + if (m_menu_bar) + m_menu_bar->updateLayoutSwitcher(m_current_layout, m_layouts); } -void DockManager::layoutSwitcherTabChanged(int index) +void DockManager::newLayoutClicked() { - // Prevent recursion. - if (m_ignore_current_tab_changed) - return; + // The plus button has just been made the current tab, so set it back to the + // one corresponding to the current layout again. + m_menu_bar->onCurrentLayoutChanged(m_current_layout); - if (index == m_plus_tab_index) + 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 dialog = new LayoutEditorDialog( + name_validator, can_clone_current_layout, g_debugger_window); + + if (dialog->exec() == QDialog::Accepted && name_validator(dialog->name())) { - if (m_current_tab_index >= 0 && m_current_tab_index < m_plus_tab_index) + DockLayout::Index new_layout = DockLayout::INVALID_INDEX; + + const auto [mode, index] = dialog->initialState(); + switch (mode) { - m_ignore_current_tab_changed = true; - m_switcher->setCurrentIndex(m_current_tab_index); - m_ignore_current_tab_changed = false; + 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; + } } - 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 dialog = new LayoutEditorDialog( - name_validator, can_clone_current_layout, g_debugger_window); - - if (dialog->exec() == QDialog::Accepted && name_validator(dialog->name())) + if (new_layout != DockLayout::INVALID_INDEX) { - 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) - return; - - DockLayout::Index old_layout = m_current_layout; - - // Freeze the current layout so we can copy the geometry. - switchToLayout(DockLayout::INVALID_INDEX); - - new_layout = createLayout(dialog->name(), dialog->cpu(), false, m_layouts.at(old_layout)); - break; - } - } - updateLayoutSwitcher(); switchToLayout(new_layout); } - - delete dialog.get(); } - else - { - DockLayout::Index layout_index = static_cast(index); - if (layout_index < 0 || layout_index >= m_layouts.size()) - return; - switchToLayout(layout_index); - m_current_tab_index = index; - } + delete dialog.get(); } -void DockManager::layoutSwitcherTabMoved(int from, int to) +void DockManager::openLayoutSwitcherContextMenu(const QPoint& pos, QTabBar* layout_switcher) { - DockLayout::Index from_index = static_cast(from); - DockLayout::Index to_index = static_cast(to); + DockLayout::Index layout_index = static_cast(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 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 @@ -660,135 +679,6 @@ void DockManager::layoutSwitcherTabMoved(int from, int to) m_current_layout = from_index; } -void DockManager::layoutSwitcherContextMenu(QPoint pos) -{ - DockLayout::Index layout_index = static_cast(m_switcher->tabAt(pos)); - if (layout_index >= m_layouts.size()) - return; - - DockLayout& layout = m_layouts[layout_index]; - - QMenu* menu = new QMenu(m_switcher); - menu->setAttribute(Qt::WA_DeleteOnClose); - - QAction* edit_action = menu->addAction(tr("Edit Layout")); - connect(edit_action, &QAction::triggered, [this, 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 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(); - }); - - QAction* reset_action = menu->addAction(tr("Reset Layout")); - reset_action->setEnabled(layout.canReset()); - reset_action->connect(reset_action, &QAction::triggered, [this, 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); - }); - - QAction* delete_action = menu->addAction(tr("Delete Layout")); - connect(delete_action, &QAction::triggered, [this, 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(); - }); - - menu->popup(m_switcher->mapToGlobal(pos)); -} - -void DockManager::layoutSwitcherStartBlink() -{ - if (!m_switcher) - return; - - layoutSwitcherStopBlink(); - - if (m_current_layout == DockLayout::INVALID_INDEX) - return; - - m_blink_tab = m_current_layout; - m_blink_stage = 0; - m_blink_timer->start(500); - - layoutSwitcherUpdateBlink(); -} - -void DockManager::layoutSwitcherUpdateBlink() -{ - if (!m_switcher) - return; - - if (m_blink_tab < m_switcher->count()) - { - if (m_blink_stage % 2 == 0) - m_switcher->setTabTextColor(m_blink_tab, Qt::red); - else - m_switcher->setTabTextColor(m_blink_tab, m_switcher->palette().text().color()); - } - - m_blink_stage++; - - if (m_blink_stage > 7) - m_blink_timer->stop(); -} - -void DockManager::layoutSwitcherStopBlink() -{ - if (m_blink_timer->isActive()) - { - if (m_blink_tab < m_switcher->count()) - m_switcher->setTabTextColor(m_blink_tab, m_switcher->palette().text().color()); - - m_blink_timer->stop(); - } -} - bool DockManager::hasNameConflict(const QString& name, DockLayout::Index layout_index) { std::string safe_name = Path::SanitizeFileName(name.toStdString()); @@ -879,23 +769,17 @@ bool DockManager::isLayoutLocked() return m_layout_locked; } -void DockManager::setLayoutLocked(bool locked, QPushButton* lock_layout_toggle, bool write_back) +void DockManager::setLayoutLockedAndSaveSetting(bool locked) +{ + setLayoutLocked(locked, true); +} + +void DockManager::setLayoutLocked(bool locked, bool save_setting) { m_layout_locked = locked; - if (lock_layout_toggle) - { - if (m_layout_locked) - { - lock_layout_toggle->setText(tr("Layout Locked")); - lock_layout_toggle->setIcon(QIcon::fromTheme(QString::fromUtf8("padlock-lock"))); - } - else - { - lock_layout_toggle->setText(tr("Layout Unlocked")); - lock_layout_toggle->setIcon(QIcon::fromTheme(QString::fromUtf8("padlock-unlock"))); - } - } + if (m_menu_bar) + m_menu_bar->onLockStateChanged(locked); updateToolBarLockState(); @@ -909,7 +793,7 @@ void DockManager::setLayoutLocked(bool locked, QPushButton* lock_layout_toggle, stack->tabBar()->setTabText(0, stack->tabBar()->tabText(0)); } - if (write_back) + if (save_setting) { Host::SetBaseBoolSettingValue("Debugger/UserInterface", "LayoutLocked", m_layout_locked); Host::CommitBaseSettingChanges(); diff --git a/pcsx2-qt/Debugger/Docking/DockManager.h b/pcsx2-qt/Debugger/Docking/DockManager.h index c27df75dfc..62b0880991 100644 --- a/pcsx2-qt/Debugger/Docking/DockManager.h +++ b/pcsx2-qt/Debugger/Docking/DockManager.h @@ -4,6 +4,7 @@ #pragma once #include "Debugger/Docking/DockLayout.h" +#include "Debugger/Docking/DockMenuBar.h" #include #include @@ -68,14 +69,14 @@ public: void createToolsMenu(QMenu* menu); void createWindowsMenu(QMenu* menu); - QWidget* createLayoutSwitcher(QWidget* menu_bar); + QWidget* createMenuBar(QWidget* original_menu_bar); void updateLayoutSwitcher(); - void layoutSwitcherTabChanged(int index); - void layoutSwitcherTabMoved(int from, int to); - void layoutSwitcherContextMenu(QPoint pos); - void layoutSwitcherStartBlink(); - void layoutSwitcherUpdateBlink(); - void layoutSwitcherStopBlink(); + 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); @@ -91,7 +92,8 @@ public: void updateStyleSheets(); bool isLayoutLocked(); - void setLayoutLocked(bool locked, QPushButton* lock_layout_toggle, bool write_back); + void setLayoutLockedAndSaveSetting(bool locked); + void setLayoutLocked(bool locked, bool save_setting); void updateToolBarLockState(); std::optional cpu(); @@ -103,16 +105,7 @@ private: std::vector m_layouts; DockLayout::Index m_current_layout = DockLayout::INVALID_INDEX; - QTabBar* m_switcher = nullptr; - int m_plus_tab_index = -1; - int m_current_tab_index = -1; - bool m_ignore_current_tab_changed = false; - - QMetaObject::Connection m_tab_connection; + DockMenuBar* m_menu_bar = nullptr; bool m_layout_locked = true; - - QTimer* m_blink_timer = nullptr; - int m_blink_tab = 0; - int m_blink_stage = 0; }; diff --git a/pcsx2-qt/Debugger/Docking/DockMenuBar.cpp b/pcsx2-qt/Debugger/Docking/DockMenuBar.cpp new file mode 100644 index 0000000000..e88e8bfd5e --- /dev/null +++ b/pcsx2-qt/Debugger/Docking/DockMenuBar.cpp @@ -0,0 +1,184 @@ +// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team +// SPDX-License-Identifier: GPL-3.0+ + +#include "DockMenuBar.h" + +#include +#include +#include +#include + +DockMenuBar::DockMenuBar(QWidget* original_menu_bar, QWidget* parent) + : QWidget(parent) +{ + QHBoxLayout* layout = new QHBoxLayout; + layout->setContentsMargins(0, 2, 2, 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, 4, 0, 4); + 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::Fixed, QSizePolicy::Preferred); + m_layout_switcher->setContextMenuPolicy(Qt::CustomContextMenu); + m_layout_switcher->setMovable(true); + layout->addWidget(m_layout_switcher); + + QWidget* spacer = new QWidget; + spacer->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); + layout->addWidget(spacer); + + connect(m_layout_switcher, &QTabBar::tabMoved, this, [this](int from, int to) { + DockLayout::Index from_index = static_cast(from); + DockLayout::Index to_index = static_cast(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); + m_layout_locked_toggle->setFlat(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); +} + +void DockMenuBar::updateLayoutSwitcher(DockLayout::Index current_index, const std::vector& 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(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(); + } +} + +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(index); + emit currentLayoutChanged(layout_index); + } + else if (index == m_plus_tab_index) + { + emit newButtonClicked(); + } +} diff --git a/pcsx2-qt/Debugger/Docking/DockMenuBar.h b/pcsx2-qt/Debugger/Docking/DockMenuBar.h new file mode 100644 index 0000000000..6ea77e4b5f --- /dev/null +++ b/pcsx2-qt/Debugger/Docking/DockMenuBar.h @@ -0,0 +1,56 @@ +// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team +// SPDX-License-Identifier: GPL-3.0+ + +#pragma once + +#include "Debugger/Docking/DockLayout.h" + +#include +#include +#include +#include + +class DockMenuBar : public QWidget +{ + Q_OBJECT + +public: + DockMenuBar(QWidget* original_menu_bar, QWidget* parent = nullptr); + + void updateLayoutSwitcher(DockLayout::Index current_index, const std::vector& layouts); + + // 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(); + +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); + +private: + void tabChanged(int index); + + + 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; +}; diff --git a/pcsx2-qt/pcsx2-qt.vcxproj b/pcsx2-qt/pcsx2-qt.vcxproj index 228f5a77d4..248803b504 100644 --- a/pcsx2-qt/pcsx2-qt.vcxproj +++ b/pcsx2-qt/pcsx2-qt.vcxproj @@ -126,6 +126,7 @@ + @@ -242,6 +243,7 @@ + @@ -313,6 +315,7 @@ + diff --git a/pcsx2-qt/pcsx2-qt.vcxproj.filters b/pcsx2-qt/pcsx2-qt.vcxproj.filters index a6488172bd..0b461826c7 100644 --- a/pcsx2-qt/pcsx2-qt.vcxproj.filters +++ b/pcsx2-qt/pcsx2-qt.vcxproj.filters @@ -488,6 +488,12 @@ moc + + moc + + + Debugger\Docking + @@ -719,6 +725,9 @@ Debugger\SymbolTree + + Debugger\Docking +