dolphin/Source/Core/DolphinQt/Debugger/BranchWatchDialog.cpp

1271 lines
49 KiB
C++
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Copyright 2024 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "DolphinQt/Debugger/BranchWatchDialog.h"
#include <algorithm>
#include <optional>
#include <ranges>
#include <utility>
#include <QApplication>
#include <QCheckBox>
#include <QClipboard>
#include <QGridLayout>
#include <QGroupBox>
#include <QHeaderView>
#include <QLineEdit>
#include <QMenu>
#include <QMenuBar>
#include <QPushButton>
#include <QShortcut>
#include <QSortFilterProxyModel>
#include <QStatusBar>
#include <QString>
#include <QTableView>
#include <QTimer>
#include <QToolBar>
#include <QVBoxLayout>
#include <QVariant>
#include <fmt/format.h>
#include "Common/Assert.h"
#include "Common/CommonFuncs.h"
#include "Common/CommonTypes.h"
#include "Common/FileUtil.h"
#include "Common/IOFile.h"
#include "Common/Unreachable.h"
#include "Core/ConfigManager.h"
#include "Core/Core.h"
#include "Core/Debugger/BranchWatch.h"
#include "Core/Debugger/PPCDebugInterface.h"
#include "Core/PowerPC/Gekko.h"
#include "Core/PowerPC/PowerPC.h"
#include "Core/System.h"
#include "DolphinQt/Debugger/BranchWatchTableModel.h"
#include "DolphinQt/Debugger/CodeWidget.h"
#include "DolphinQt/Host.h"
#include "DolphinQt/QtUtils/DolphinFileDialog.h"
#include "DolphinQt/QtUtils/ModalMessageBox.h"
#include "DolphinQt/QtUtils/SetWindowDecorations.h"
#include "DolphinQt/Resources.h"
#include "DolphinQt/Settings.h"
class BranchWatchProxyModel final : public QSortFilterProxyModel
{
friend BranchWatchDialog;
public:
explicit BranchWatchProxyModel(const Core::BranchWatch& branch_watch, QObject* parent = nullptr)
: QSortFilterProxyModel(parent), m_branch_watch(branch_watch)
{
}
~BranchWatchProxyModel() override = default;
BranchWatchProxyModel(const BranchWatchProxyModel&) = delete;
BranchWatchProxyModel(BranchWatchProxyModel&&) = delete;
BranchWatchProxyModel& operator=(const BranchWatchProxyModel&) = delete;
BranchWatchProxyModel& operator=(BranchWatchProxyModel&&) = delete;
BranchWatchTableModel* sourceModel() const
{
return static_cast<BranchWatchTableModel*>(QSortFilterProxyModel::sourceModel());
}
void setSourceModel(BranchWatchTableModel* source_model)
{
QSortFilterProxyModel::setSourceModel(source_model);
}
// Virtual setSourceModel is forbidden for type-safety reasons. See sourceModel().
[[noreturn]] void setSourceModel(QAbstractItemModel* source_model) override { Crash(); }
bool filterAcceptsRow(int source_row, const QModelIndex& source_parent) const override;
template <bool BranchWatchProxyModel::*member>
void OnToggled(bool enabled)
{
this->*member = enabled;
invalidateRowsFilter();
}
template <QString BranchWatchProxyModel::*member>
void OnSymbolTextChanged(const QString& text)
{
this->*member = text;
invalidateRowsFilter();
}
template <std::optional<u32> BranchWatchProxyModel::*member>
void OnAddressTextChanged(const QString& text)
{
bool ok = false;
if (const u32 value = text.toUInt(&ok, 16); ok)
this->*member = value;
else
this->*member = std::nullopt;
invalidateRowsFilter();
}
bool IsBranchTypeAllowed(UGeckoInstruction inst) const;
void SetInspected(const QModelIndex& index) const;
const Core::BranchWatchSelectionValueType&
GetBranchWatchSelection(const QModelIndex& index) const;
private:
const Core::BranchWatch& m_branch_watch;
QString m_origin_symbol_name = {}, m_destin_symbol_name = {};
std::optional<u32> m_origin_min, m_origin_max, m_destin_min, m_destin_max;
bool m_b = {}, m_bl = {}, m_bc = {}, m_bcl = {}, m_blr = {}, m_blrl = {}, m_bclr = {},
m_bclrl = {}, m_bctr = {}, m_bctrl = {}, m_bcctr = {}, m_bcctrl = {};
bool m_cond_true = {}, m_cond_false = {};
};
bool BranchWatchProxyModel::filterAcceptsRow(int source_row, const QModelIndex&) const
{
const Core::BranchWatch::Selection::value_type& value = m_branch_watch.GetSelection()[source_row];
if (value.condition)
{
if (!m_cond_true)
return false;
}
else if (!m_cond_false)
return false;
const Core::BranchWatchCollectionKey& k = value.collection_ptr->first;
if (!IsBranchTypeAllowed(k.original_inst))
return false;
if (m_origin_min.has_value() && k.origin_addr < m_origin_min.value())
return false;
if (m_origin_max.has_value() && k.origin_addr > m_origin_max.value())
return false;
if (m_destin_min.has_value() && k.destin_addr < m_destin_min.value())
return false;
if (m_destin_max.has_value() && k.destin_addr > m_destin_max.value())
return false;
if (!m_origin_symbol_name.isEmpty())
{
if (const QVariant& symbol_name_v = sourceModel()->GetSymbolList()[source_row].origin_name;
!symbol_name_v.isValid() || !static_cast<const QString*>(symbol_name_v.data())
->contains(m_origin_symbol_name, Qt::CaseInsensitive))
return false;
}
if (!m_destin_symbol_name.isEmpty())
{
if (const QVariant& symbol_name_v = sourceModel()->GetSymbolList()[source_row].destin_name;
!symbol_name_v.isValid() || !static_cast<const QString*>(symbol_name_v.data())
->contains(m_destin_symbol_name, Qt::CaseInsensitive))
return false;
}
return true;
}
bool BranchWatchProxyModel::IsBranchTypeAllowed(UGeckoInstruction inst) const
{
switch (inst.OPCD)
{
case 18:
return inst.LK ? m_bl : m_b;
case 16:
return inst.LK ? m_bcl : m_bc;
case 19:
switch (inst.SUBOP10)
{
case 16:
if ((inst.BO & 0b10100) == 0b10100) // 1z1zz - Branch always
return inst.LK ? m_blrl : m_blr;
return inst.LK ? m_bclrl : m_bclr;
case 528:
if ((inst.BO & 0b10100) == 0b10100) // 1z1zz - Branch always
return inst.LK ? m_bctrl : m_bctr;
return inst.LK ? m_bcctrl : m_bcctr;
}
}
return false;
}
void BranchWatchProxyModel::SetInspected(const QModelIndex& index) const
{
sourceModel()->SetInspected(mapToSource(index));
}
const Core::BranchWatchSelectionValueType&
BranchWatchProxyModel::GetBranchWatchSelection(const QModelIndex& index) const
{
return sourceModel()->GetBranchWatchSelection(mapToSource(index));
}
BranchWatchDialog::BranchWatchDialog(Core::System& system, Core::BranchWatch& branch_watch,
PPCSymbolDB& ppc_symbol_db, CodeWidget* code_widget,
QWidget* parent)
: QDialog(parent), m_system(system), m_branch_watch(branch_watch), m_code_widget(code_widget)
{
setWindowTitle(tr("Branch Watch Tool"));
setWindowFlags((windowFlags() | Qt::WindowMinMaxButtonsHint) & ~Qt::WindowContextHelpButtonHint);
// Branch Watch Table
m_table_view = new QTableView(nullptr);
m_table_proxy = new BranchWatchProxyModel(m_branch_watch, m_table_view);
m_table_model = new BranchWatchTableModel(m_system, m_branch_watch, ppc_symbol_db, m_table_proxy);
m_table_proxy->setSourceModel(m_table_model);
m_table_proxy->setSortRole(UserRole::SortRole);
m_table_proxy->setSortCaseSensitivity(Qt::CaseInsensitive);
m_table_view->setModel(m_table_proxy);
m_table_view->setSortingEnabled(true);
m_table_view->sortByColumn(Column::Origin, Qt::AscendingOrder);
m_table_view->setSelectionMode(QAbstractItemView::ExtendedSelection);
m_table_view->setSelectionBehavior(QAbstractItemView::SelectRows);
m_table_view->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
m_table_view->setContextMenuPolicy(Qt::CustomContextMenu);
m_table_view->setEditTriggers(QAbstractItemView::NoEditTriggers);
m_table_view->setCornerButtonEnabled(false);
m_table_view->verticalHeader()->hide();
m_table_view->setColumnWidth(Column::Instruction, 50);
m_table_view->setColumnWidth(Column::Condition, 50);
m_table_view->setColumnWidth(Column::OriginSymbol, 250);
m_table_view->setColumnWidth(Column::DestinSymbol, 250);
// The default column width (100 units) is fine for the rest.
auto* const horizontal_header = m_table_view->horizontalHeader();
horizontal_header->setContextMenuPolicy(Qt::CustomContextMenu);
horizontal_header->setStretchLastSection(true);
horizontal_header->setSectionsMovable(true);
horizontal_header->setFirstSectionMovable(true);
connect(m_table_view, &QTableView::clicked, this, &BranchWatchDialog::OnTableClicked);
connect(m_table_view, &QTableView::customContextMenuRequested, this,
&BranchWatchDialog::OnTableContextMenu);
connect(horizontal_header, &QHeaderView::customContextMenuRequested, this,
&BranchWatchDialog::OnTableHeaderContextMenu);
connect(new QShortcut(QKeySequence(Qt::Key_Delete), this), &QShortcut::activated, this,
&BranchWatchDialog::OnTableDeleteKeypress);
// Status Bar
m_status_bar = new QStatusBar(nullptr);
m_status_bar->setSizeGripEnabled(false);
// Controls Toolbar
m_control_toolbar = new QToolBar(nullptr);
{
// Tool Controls
m_btn_start_pause = new QPushButton(tr("Start Branch Watch"), nullptr);
connect(m_btn_start_pause, &QPushButton::toggled, this, &BranchWatchDialog::OnStartPause);
m_btn_start_pause->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding);
m_btn_start_pause->setCheckable(true);
m_btn_clear_watch = new QPushButton(tr("Clear Branch Watch"), nullptr);
connect(m_btn_clear_watch, &QPushButton::clicked, this, &BranchWatchDialog::OnClearBranchWatch);
m_btn_clear_watch->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding);
m_btn_path_was_taken = new QPushButton(tr("Code Path Was Taken"), nullptr);
connect(m_btn_path_was_taken, &QPushButton::clicked, this,
&BranchWatchDialog::OnCodePathWasTaken);
m_btn_path_was_taken->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding);
m_btn_path_not_taken = new QPushButton(tr("Code Path Not Taken"), nullptr);
connect(m_btn_path_not_taken, &QPushButton::clicked, this,
&BranchWatchDialog::OnCodePathNotTaken);
m_btn_path_not_taken->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding);
auto* const layout = new QGridLayout(nullptr);
layout->addWidget(m_btn_start_pause, 0, 0);
layout->addWidget(m_btn_clear_watch, 1, 0);
layout->addWidget(m_btn_path_was_taken, 0, 1);
layout->addWidget(m_btn_path_not_taken, 1, 1);
auto* const group_box = new QGroupBox(tr("Tool Controls"), nullptr);
group_box->setLayout(layout);
group_box->setAlignment(Qt::AlignHCenter);
m_control_toolbar->addWidget(group_box);
}
{
// Spacer
auto* const widget = new QWidget(nullptr);
widget->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Preferred);
m_control_toolbar->addWidget(widget);
}
{
// Branch Type Filter Options
auto* const layout = new QGridLayout(nullptr);
const auto routine = [this, layout](const QString& text, const QString& tooltip, int row,
int column, void (BranchWatchProxyModel::*slot)(bool)) {
auto* const check_box = new QCheckBox(text, nullptr);
check_box->setToolTip(tooltip);
layout->addWidget(check_box, row, column);
connect(check_box, &QCheckBox::toggled, [this, slot](bool checked) {
(m_table_proxy->*slot)(checked);
UpdateStatus();
});
check_box->setChecked(true);
};
// clang-format off
routine(QStringLiteral("b" ), tr("Branch" ), 0, 0, &BranchWatchProxyModel::OnToggled<&BranchWatchProxyModel::m_b >);
routine(QStringLiteral("bl" ), tr("Branch (LR saved)" ), 0, 1, &BranchWatchProxyModel::OnToggled<&BranchWatchProxyModel::m_bl >);
routine(QStringLiteral("bc" ), tr("Branch Conditional" ), 0, 2, &BranchWatchProxyModel::OnToggled<&BranchWatchProxyModel::m_bc >);
routine(QStringLiteral("bcl" ), tr("Branch Conditional (LR saved)" ), 0, 3, &BranchWatchProxyModel::OnToggled<&BranchWatchProxyModel::m_bcl >);
routine(QStringLiteral("blr" ), tr("Branch to Link Register" ), 1, 0, &BranchWatchProxyModel::OnToggled<&BranchWatchProxyModel::m_blr >);
routine(QStringLiteral("blrl" ), tr("Branch to Link Register (LR saved)" ), 1, 1, &BranchWatchProxyModel::OnToggled<&BranchWatchProxyModel::m_blrl >);
routine(QStringLiteral("bclr" ), tr("Branch Conditional to Link Register" ), 1, 2, &BranchWatchProxyModel::OnToggled<&BranchWatchProxyModel::m_bclr >);
routine(QStringLiteral("bclrl" ), tr("Branch Conditional to Link Register (LR saved)" ), 1, 3, &BranchWatchProxyModel::OnToggled<&BranchWatchProxyModel::m_bclrl >);
routine(QStringLiteral("bctr" ), tr("Branch to Count Register" ), 2, 0, &BranchWatchProxyModel::OnToggled<&BranchWatchProxyModel::m_bctr >);
routine(QStringLiteral("bctrl" ), tr("Branch to Count Register (LR saved)" ), 2, 1, &BranchWatchProxyModel::OnToggled<&BranchWatchProxyModel::m_bctrl >);
routine(QStringLiteral("bcctr" ), tr("Branch Conditional to Count Register" ), 2, 2, &BranchWatchProxyModel::OnToggled<&BranchWatchProxyModel::m_bcctr >);
routine(QStringLiteral("bcctrl"), tr("Branch Conditional to Count Register (LR saved)"), 2, 3, &BranchWatchProxyModel::OnToggled<&BranchWatchProxyModel::m_bcctrl>);
// clang-format on
auto* const group_box = new QGroupBox(tr("Branch Type"), nullptr);
group_box->setLayout(layout);
group_box->setAlignment(Qt::AlignHCenter);
m_act_branch_type_filters = m_control_toolbar->addWidget(group_box);
}
{
// Origin and Destination Filter Options
auto* const layout = new QGridLayout(nullptr);
const auto routine = [this, layout](const QString& placeholder_text, int row, int column,
int width,
void (BranchWatchProxyModel::*slot)(const QString&)) {
auto* const line_edit = new QLineEdit(nullptr);
layout->addWidget(line_edit, row, column, 1, width);
connect(line_edit, &QLineEdit::textChanged, [this, slot](const QString& text) {
(m_table_proxy->*slot)(text);
UpdateStatus();
});
line_edit->setPlaceholderText(placeholder_text);
return line_edit;
};
// clang-format off
routine(tr("Origin Symbol" ), 0, 0, 1, &BranchWatchProxyModel::OnSymbolTextChanged<&BranchWatchProxyModel::m_origin_symbol_name>);
routine(tr("Origin Min" ), 1, 0, 1, &BranchWatchProxyModel::OnAddressTextChanged<&BranchWatchProxyModel::m_origin_min>)->setMaxLength(8);
routine(tr("Origin Max" ), 2, 0, 1, &BranchWatchProxyModel::OnAddressTextChanged<&BranchWatchProxyModel::m_origin_max>)->setMaxLength(8);
routine(tr("Destination Symbol"), 0, 1, 1, &BranchWatchProxyModel::OnSymbolTextChanged<&BranchWatchProxyModel::m_destin_symbol_name>);
routine(tr("Destination Min" ), 1, 1, 1, &BranchWatchProxyModel::OnAddressTextChanged<&BranchWatchProxyModel::m_destin_min>)->setMaxLength(8);
routine(tr("Destination Max" ), 2, 1, 1, &BranchWatchProxyModel::OnAddressTextChanged<&BranchWatchProxyModel::m_destin_max>)->setMaxLength(8);
// clang-format on
auto* const group_box = new QGroupBox(tr("Origin and Destination"), nullptr);
group_box->setLayout(layout);
group_box->setAlignment(Qt::AlignHCenter);
m_act_origin_destin_filters = m_control_toolbar->addWidget(group_box);
}
{
// Condition Filter Options
auto* const layout = new QVBoxLayout(nullptr);
layout->setAlignment(Qt::AlignHCenter);
const auto routine = [this, layout](const QString& text,
void (BranchWatchProxyModel::*slot)(bool)) {
auto* const check_box = new QCheckBox(text, nullptr);
layout->addWidget(check_box);
connect(check_box, &QCheckBox::toggled, [this, slot](bool checked) {
(m_table_proxy->*slot)(checked);
UpdateStatus();
});
check_box->setChecked(true);
return check_box;
};
routine(tr("true"), &BranchWatchProxyModel::OnToggled<&BranchWatchProxyModel::m_cond_true>)
->setToolTip(tr("This will also filter unconditional branches.\n"
"To filter for or against unconditional branches,\n"
"use the Branch Type filter options."));
routine(tr("false"), &BranchWatchProxyModel::OnToggled<&BranchWatchProxyModel::m_cond_false>);
auto* const group_box = new QGroupBox(tr("Condition"), nullptr);
group_box->setLayout(layout);
group_box->setAlignment(Qt::AlignHCenter);
m_act_condition_filters = m_control_toolbar->addWidget(group_box);
}
{
// Misc. Controls
m_btn_was_overwritten = new QPushButton(tr("Branch Was Overwritten"), nullptr);
connect(m_btn_was_overwritten, &QPushButton::clicked, this,
&BranchWatchDialog::OnBranchWasOverwritten);
m_btn_was_overwritten->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding);
m_btn_not_overwritten = new QPushButton(tr("Branch Not Overwritten"), nullptr);
connect(m_btn_not_overwritten, &QPushButton::clicked, this,
&BranchWatchDialog::OnBranchNotOverwritten);
m_btn_not_overwritten->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding);
m_btn_wipe_recent_hits = new QPushButton(tr("Wipe Recent Hits"), nullptr);
connect(m_btn_wipe_recent_hits, &QPushButton::clicked, this,
&BranchWatchDialog::OnWipeRecentHits);
m_btn_wipe_recent_hits->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding);
m_btn_wipe_recent_hits->setEnabled(false);
auto* const layout = new QVBoxLayout(nullptr);
layout->addWidget(m_btn_was_overwritten);
layout->addWidget(m_btn_not_overwritten);
layout->addWidget(m_btn_wipe_recent_hits);
auto* const group_box = new QGroupBox(tr("Misc. Controls"), nullptr);
group_box->setLayout(layout);
group_box->setAlignment(Qt::AlignHCenter);
m_act_misc_controls = m_control_toolbar->addWidget(group_box);
}
// Table Context Menus
auto* const delete_action = new QAction(tr("&Delete"), this);
connect(delete_action, &QAction::triggered, this, &BranchWatchDialog::OnTableDelete);
m_act_invert_condition = new QAction(tr("Invert &Condition"), this);
connect(m_act_invert_condition, &QAction::triggered, this,
&BranchWatchDialog::OnTableInvertCondition);
m_act_invert_decrement_check = new QAction(tr("Invert &Decrement Check"), this);
connect(m_act_invert_decrement_check, &QAction::triggered, this,
&BranchWatchDialog::OnTableInvertDecrementCheck);
m_act_make_unconditional = new QAction(tr("Make &Unconditional"), this);
connect(m_act_make_unconditional, &QAction::triggered, this,
&BranchWatchDialog::OnTableMakeUnconditional);
m_act_copy_address = new QAction(tr("&Copy Address"), this);
connect(m_act_copy_address, &QAction::triggered, this, &BranchWatchDialog::OnTableCopyAddress);
m_act_insert_nop = new QAction(tr("Insert &NOP"), this);
connect(m_act_insert_nop, &QAction::triggered, this, &BranchWatchDialog::OnTableSetNOP);
m_act_insert_blr = new QAction(tr("Insert &BLR"), this);
connect(m_act_insert_blr, &QAction::triggered, this, &BranchWatchDialog::OnTableSetBLR);
m_mnu_set_breakpoint = new QMenu(tr("Set Brea&kpoint"), this);
m_act_break_on_hit = m_mnu_set_breakpoint->addAction(
tr("&Break on Hit"), this, &BranchWatchDialog::OnTableSetBreakpointBreak);
m_act_log_on_hit = m_mnu_set_breakpoint->addAction(tr("&Log on Hit"), this,
&BranchWatchDialog::OnTableSetBreakpointLog);
m_act_both_on_hit = m_mnu_set_breakpoint->addAction(tr("Break &and Log on Hit"), this,
&BranchWatchDialog::OnTableSetBreakpointBoth);
m_mnu_table_context_instruction = new QMenu(this);
m_mnu_table_context_instruction->addActions(
{delete_action, m_act_invert_condition, m_act_invert_decrement_check});
m_mnu_table_context_condition = new QMenu(this);
m_mnu_table_context_condition->addActions({delete_action, m_act_make_unconditional});
m_mnu_table_context_origin = new QMenu(this);
m_mnu_table_context_origin->addActions(
{delete_action, m_act_insert_nop, m_act_copy_address, m_mnu_set_breakpoint->menuAction()});
m_mnu_table_context_destin_or_symbol = new QMenu(this);
m_mnu_table_context_destin_or_symbol->addActions(
{delete_action, m_act_insert_blr, m_act_copy_address, m_mnu_set_breakpoint->menuAction()});
m_mnu_table_context_other = new QMenu(this);
m_mnu_table_context_other->addAction(delete_action);
LoadQSettings();
// Column Visibility Menu
m_mnu_column_visibility = new QMenu(this);
{
static constexpr std::array<const char*, Column::NumberOfColumns> headers = {
QT_TR_NOOP("Instruction"), QT_TR_NOOP("Condition"), QT_TR_NOOP("Origin"),
QT_TR_NOOP("Destination"), QT_TR_NOOP("Recent Hits"), QT_TR_NOOP("Total Hits"),
QT_TR_NOOP("Origin Symbol"), QT_TR_NOOP("Destination Symbol")};
for (int column = 0; column < Column::NumberOfColumns; ++column)
{
auto* const action =
m_mnu_column_visibility->addAction(tr(headers[column]), [this, column](bool enabled) {
m_table_view->setColumnHidden(column, !enabled);
});
action->setChecked(!m_table_view->isColumnHidden(column));
action->setCheckable(true);
}
}
// Toolbar Visibility Menu
auto* const toolbar_visibility_menu = new QMenu(this);
{
const auto routine = [toolbar_visibility_menu](const QString& text, QAction* toolbar_action) {
auto* const menu_action =
toolbar_visibility_menu->addAction(text, toolbar_action, &QAction::setVisible);
menu_action->setChecked(toolbar_action->isVisible());
menu_action->setCheckable(true);
};
routine(tr("&Branch Type"), m_act_branch_type_filters);
routine(tr("&Origin and Destination"), m_act_origin_destin_filters);
routine(tr("&Condition"), m_act_condition_filters);
routine(tr("&Misc. Controls"), m_act_misc_controls);
}
// Menu Bar
auto* const menu_bar = new QMenuBar(nullptr);
menu_bar->setNativeMenuBar(false);
{
auto* const menu = menu_bar->addMenu(tr("&File"));
menu->addAction(tr("&Save Branch Watch"), this, &BranchWatchDialog::OnSave);
menu->addAction(tr("Save Branch Watch &As..."), this, &BranchWatchDialog::OnSaveAs);
menu->addAction(tr("&Load Branch Watch"), this, &BranchWatchDialog::OnLoad);
menu->addAction(tr("Load Branch Watch &From..."), this, &BranchWatchDialog::OnLoadFrom);
m_act_autosave = menu->addAction(tr("A&uto Save"));
m_act_autosave->setCheckable(true);
connect(m_act_autosave, &QAction::toggled, this, &BranchWatchDialog::OnToggleAutoSave);
}
{
auto* const menu = menu_bar->addMenu(tr("&Tool"));
menu->setToolTipsVisible(true);
menu->addAction(tr("Hide &Controls"), this, &BranchWatchDialog::OnHideShowControls)
->setCheckable(true);
auto* const act_ignore_apploader = menu->addAction(tr("Ignore &Apploader Branch Hits"));
act_ignore_apploader->setToolTip(
tr("This only applies to the initial boot of the emulated software."));
act_ignore_apploader->setChecked(m_system.IsBranchWatchIgnoreApploader());
act_ignore_apploader->setCheckable(true);
connect(act_ignore_apploader, &QAction::toggled, this,
&BranchWatchDialog::OnToggleIgnoreApploader);
menu->addMenu(m_mnu_column_visibility)->setText(tr("Column &Visibility"));
menu->addMenu(toolbar_visibility_menu)->setText(tr("&Toolbar Visibility"));
menu->addAction(tr("Wipe &Inspection Data"), this, &BranchWatchDialog::OnWipeInspection);
menu->addAction(tr("&Help"), this, &BranchWatchDialog::OnHelp);
}
connect(m_timer = new QTimer(this), &QTimer::timeout, this, &BranchWatchDialog::OnTimeout);
connect(m_table_proxy, &BranchWatchProxyModel::layoutChanged, this,
&BranchWatchDialog::UpdateStatus);
auto* const main_layout = new QVBoxLayout(nullptr);
main_layout->setMenuBar(menu_bar);
main_layout->addWidget(m_control_toolbar);
main_layout->addWidget(m_table_view);
main_layout->addWidget(m_status_bar);
setLayout(main_layout);
}
BranchWatchDialog::~BranchWatchDialog()
{
SaveQSettings();
}
static constexpr int BRANCH_WATCH_TOOL_TIMER_DELAY_MS = 100;
static bool TimerCondition(const Core::BranchWatch& branch_watch, Core::State state)
{
return branch_watch.GetRecordingActive() && state > Core::State::Paused;
}
void BranchWatchDialog::hideEvent(QHideEvent* event)
{
Hide();
QDialog::hideEvent(event);
}
void BranchWatchDialog::showEvent(QShowEvent* event)
{
Show();
QDialog::showEvent(event);
}
void BranchWatchDialog::OnStartPause(bool checked) const
{
m_branch_watch.SetRecordingActive(Core::CPUThreadGuard{m_system}, checked);
if (checked)
{
m_btn_start_pause->setText(tr("Pause Branch Watch"));
if (Core::GetState(m_system) > Core::State::Paused)
m_timer->start(BRANCH_WATCH_TOOL_TIMER_DELAY_MS);
}
else
{
m_btn_start_pause->setText(tr("Start Branch Watch"));
if (m_timer->isActive())
m_timer->stop();
}
Update();
}
void BranchWatchDialog::OnClearBranchWatch()
{
{
const Core::CPUThreadGuard guard{m_system};
m_table_model->OnClearBranchWatch(guard);
AutoSave(guard);
}
m_btn_wipe_recent_hits->setEnabled(false);
UpdateStatus();
}
static std::string GetSnapshotDefaultFilepath()
{
return fmt::format("{}{}.txt", File::GetUserPath(D_DUMPDEBUG_BRANCHWATCH_IDX),
SConfig::GetInstance().GetGameID());
}
void BranchWatchDialog::OnSave()
{
if (!m_branch_watch.CanSave())
{
ModalMessageBox::warning(this, tr("Error"), tr("There is nothing to save!"));
return;
}
Save(Core::CPUThreadGuard{m_system}, GetSnapshotDefaultFilepath());
}
void BranchWatchDialog::OnSaveAs()
{
if (!m_branch_watch.CanSave())
{
ModalMessageBox::warning(this, tr("Error"), tr("There is nothing to save!"));
return;
}
const QString filepath = DolphinFileDialog::getSaveFileName(
this, tr("Save Branch Watch Snapshot"),
QString::fromStdString(File::GetUserPath(D_DUMPDEBUG_BRANCHWATCH_IDX)),
tr("Text file (*.txt);;All Files (*)"));
if (filepath.isEmpty())
return;
Save(Core::CPUThreadGuard{m_system}, filepath.toStdString());
}
void BranchWatchDialog::OnLoad()
{
Load(Core::CPUThreadGuard{m_system}, GetSnapshotDefaultFilepath());
}
void BranchWatchDialog::OnLoadFrom()
{
const QString filepath = DolphinFileDialog::getOpenFileName(
this, tr("Load Branch Watch Snapshot"),
QString::fromStdString(File::GetUserPath(D_DUMPDEBUG_BRANCHWATCH_IDX)),
tr("Text file (*.txt);;All Files (*)"), nullptr, QFileDialog::Option::ReadOnly);
if (filepath.isEmpty())
return;
Load(Core::CPUThreadGuard{m_system}, filepath.toStdString());
}
void BranchWatchDialog::OnCodePathWasTaken()
{
{
const Core::CPUThreadGuard guard{m_system};
m_table_model->OnCodePathWasTaken(guard);
AutoSave(guard);
}
m_btn_wipe_recent_hits->setEnabled(true);
UpdateStatus();
}
void BranchWatchDialog::OnCodePathNotTaken()
{
{
const Core::CPUThreadGuard guard{m_system};
m_table_model->OnCodePathNotTaken(guard);
AutoSave(guard);
}
UpdateStatus();
}
void BranchWatchDialog::OnBranchWasOverwritten()
{
{
const Core::CPUThreadGuard guard{m_system};
m_table_model->OnBranchWasOverwritten(guard);
AutoSave(guard);
}
UpdateStatus();
}
void BranchWatchDialog::OnBranchNotOverwritten()
{
{
const Core::CPUThreadGuard guard{m_system};
m_table_model->OnBranchNotOverwritten(guard);
AutoSave(guard);
}
UpdateStatus();
}
void BranchWatchDialog::OnWipeRecentHits() const
{
m_table_model->OnWipeRecentHits();
}
void BranchWatchDialog::OnWipeInspection() const
{
m_table_model->OnWipeInspection();
}
void BranchWatchDialog::OnTimeout() const
{
Update();
}
void BranchWatchDialog::OnEmulationStateChanged(Core::State new_state) const
{
m_btn_was_overwritten->setEnabled(new_state != Core::State::Uninitialized);
m_btn_not_overwritten->setEnabled(new_state != Core::State::Uninitialized);
if (TimerCondition(m_branch_watch, new_state))
m_timer->start(BRANCH_WATCH_TOOL_TIMER_DELAY_MS);
else if (m_timer->isActive())
m_timer->stop();
Update();
}
void BranchWatchDialog::OnThemeChanged()
{
UpdateIcons();
}
void BranchWatchDialog::OnHelp()
{
ModalMessageBox::information(
this, tr("Branch Watch Tool Help (1/4)"),
tr("Branch Watch is a code-searching tool that can isolate branches tracked by the emulated "
"CPU by testing candidate branches with simple criteria. If you are familiar with Cheat "
"Engine's Ultimap, Branch Watch is similar to that."
"\n\n"
"Press the \"Start Branch Watch\" button to activate Branch Watch. Branch Watch persists "
"across emulation sessions, and a snapshot of your progress can be saved to and loaded "
"from the User Directory to persist after Dolphin Emulator is closed. \"Save As...\" and "
"\"Load From...\" actions are also available, and auto-saving can be enabled to save a "
"snapshot at every step of a search. The \"Pause Branch Watch\" button will halt Branch "
"Watch from tracking further branch hits until it is told to resume. Press the \"Clear "
"Branch Watch\" button to clear all candidates and return to the blacklist phase."));
ModalMessageBox::information(
this, tr("Branch Watch Tool Help (2/4)"),
tr("Branch Watch starts in the blacklist phase, meaning no candidates have been chosen yet, "
"but candidates found so far can be excluded from the candidacy by pressing the \"Code "
"Path Not Taken\", \"Branch Was Overwritten\", and \"Branch Not Overwritten\" buttons. "
"Once the \"Code Path Was Taken\" button is pressed for the first time, Branch Watch will "
"switch to the reduction phase, and the table will populate with all eligible "
"candidates."));
ModalMessageBox::information(
this, tr("Branch Watch Tool Help (3/4)"),
tr("Once in the reduction phase, it is time to start narrowing down the candidates shown in "
"the table. Further reduce the candidates by checking whether a code path was or was not "
"taken since the last time it was checked. It is also possible to reduce the candidates "
"by determining whether a branch instruction has or has not been overwritten since it was "
"first hit. Filter the candidates by branch kind, branch condition, origin or destination "
"address, and origin or destination symbol name."
"\n\n"
"After enough passes and experimentation, you may be able to find function calls and "
"conditional code paths that are only taken when an action is performed in the emulated "
"software."));
ModalMessageBox::information(
this, tr("Branch Watch Tool Help (4/4)"),
tr("Rows in the table can be left-clicked on the origin, destination, and symbol columns to "
"view the associated address in Code View. Right-clicking the selected row(s) will bring "
"up a context menu."
"\n\n"
"If the origin, destination, or symbol columns are right-clicked, an action copy the "
"relevant address(es) to the clipboard will be available, and an action to set a "
"breakpoint at the relevant address(es) will be available. Note that, for the origin / "
"destination symbol columns, these actions will only be enabled if every row in the "
"selection has a symbol."
"\n\n"
"If the instruction column of a row selection is right-clicked, an action to invert the "
"branch instruction's condition and an action to invert the branch instruction's "
"decrement check will be available, but only if the branch instruction is a conditional "
"one."
"\n\n"
"If the condition column of a row selection is right-clicked, an action to make the "
"branch instruction unconditional will be available, but only if the branch instruction "
"is a conditional one."
"\n\n"
"If the origin column of a row selection is right-clicked, an action to replace the "
"branch instruction at the origin(s) with a NOP instruction (No Operation) will be "
"available."
"\n\n"
"If the destination column of a row selection is right-clicked, an action to replace the "
"instruction at the destination(s) with a BLR instruction (Branch to Link Register) will "
"be available, but will only be enabled if the branch instruction at every origin updates "
"the link register."
"\n\n"
"If the origin / destination symbol column of a row selection is right-clicked, an action "
"to replace the instruction at the start of the symbol(s) with a BLR instruction will be "
"available, but will only be enabled if every row in the selection has a symbol."
"\n\n"
"All context menus have the action to delete the selected row(s) from the candidates."));
}
void BranchWatchDialog::OnToggleAutoSave(bool checked)
{
if (!checked)
return;
const QString filepath = DolphinFileDialog::getSaveFileName(
// i18n: If the user selects a file, Branch Watch will save to that file.
// If the user presses Cancel, Branch Watch will save to a file in the user folder.
this, tr("Select Branch Watch Snapshot Auto-Save File (for user folder location, cancel)"),
QString::fromStdString(File::GetUserPath(D_DUMPDEBUG_BRANCHWATCH_IDX)),
tr("Text file (*.txt);;All Files (*)"));
if (filepath.isEmpty())
m_autosave_filepath = std::nullopt;
else
m_autosave_filepath = filepath.toStdString();
}
void BranchWatchDialog::OnHideShowControls(bool checked) const
{
if (checked)
m_control_toolbar->hide();
else
m_control_toolbar->show();
}
void BranchWatchDialog::OnToggleIgnoreApploader(bool checked) const
{
m_system.SetIsBranchWatchIgnoreApploader(checked);
}
void BranchWatchDialog::OnTableClicked(const QModelIndex& index) const
{
const QVariant v = m_table_proxy->data(index, UserRole::ClickRole);
switch (index.column())
{
case Column::OriginSymbol:
case Column::DestinSymbol:
if (!v.isValid())
return;
[[fallthrough]];
case Column::Origin:
case Column::Destination:
m_code_widget->SetAddress(v.value<u32>(), CodeViewWidget::SetAddressUpdate::WithDetailedUpdate);
return;
}
}
void BranchWatchDialog::OnTableContextMenu(const QPoint& pos) const
{
if (m_table_view->horizontalHeader()->hiddenSectionCount() == Column::NumberOfColumns)
{
m_mnu_column_visibility->exec(m_table_view->viewport()->mapToGlobal(pos));
return;
}
const QModelIndex index = m_table_view->indexAt(pos);
if (!index.isValid())
return;
m_index_list_temp = m_table_view->selectionModel()->selectedRows(index.column());
GetTableContextMenu(index)->exec(m_table_view->viewport()->mapToGlobal(pos));
m_index_list_temp.clear();
m_index_list_temp.shrink_to_fit();
}
void BranchWatchDialog::OnTableHeaderContextMenu(const QPoint& pos) const
{
m_mnu_column_visibility->exec(m_table_view->horizontalHeader()->mapToGlobal(pos));
}
void BranchWatchDialog::OnTableDelete() const
{
std::ranges::transform(
m_index_list_temp, m_index_list_temp.begin(),
[this](const QModelIndex& index) { return m_table_proxy->mapToSource(index); });
std::ranges::sort(m_index_list_temp, std::less{});
for (const auto& index : std::ranges::reverse_view{m_index_list_temp})
{
if (!index.isValid())
continue;
m_table_model->removeRow(index.row());
}
UpdateStatus();
}
void BranchWatchDialog::OnTableDeleteKeypress() const
{
m_index_list_temp = m_table_view->selectionModel()->selectedRows();
OnTableDelete();
m_index_list_temp.clear();
m_index_list_temp.shrink_to_fit();
}
void BranchWatchDialog::OnTableSetBLR() const
{
SetStubPatches(0x4e800020);
}
void BranchWatchDialog::OnTableSetNOP() const
{
SetStubPatches(0x60000000);
}
void BranchWatchDialog::OnTableInvertCondition() const
{
SetEditPatches([](u32 hex) {
UGeckoInstruction inst = hex;
inst.BO ^= 0b01000;
return inst.hex;
});
}
void BranchWatchDialog::OnTableInvertDecrementCheck() const
{
SetEditPatches([](u32 hex) {
UGeckoInstruction inst = hex;
inst.BO ^= 0b00010;
return inst.hex;
});
}
void BranchWatchDialog::OnTableMakeUnconditional() const
{
SetEditPatches([](u32 hex) {
UGeckoInstruction inst = hex;
inst.BO = 0b10100; // 1z1zz - Branch always
return inst.hex;
});
}
void BranchWatchDialog::OnTableCopyAddress() const
{
auto iter = m_index_list_temp.begin();
if (iter == m_index_list_temp.end())
return;
QString text;
text.reserve(m_index_list_temp.size() * 9 - 1);
while (true)
{
text.append(QString::number(m_table_proxy->data(*iter, UserRole::ClickRole).value<u32>(), 16));
if (++iter == m_index_list_temp.end())
break;
text.append(QChar::fromLatin1('\n'));
}
QApplication::clipboard()->setText(text);
}
void BranchWatchDialog::OnTableSetBreakpointBreak() const
{
SetBreakpoints(true, false);
}
void BranchWatchDialog::OnTableSetBreakpointLog() const
{
SetBreakpoints(false, true);
}
void BranchWatchDialog::OnTableSetBreakpointBoth() const
{
SetBreakpoints(true, true);
}
void BranchWatchDialog::ConnectSlots()
{
const auto* const settings = &Settings::Instance();
connect(settings, &Settings::EmulationStateChanged, this,
&BranchWatchDialog::OnEmulationStateChanged);
connect(settings, &Settings::ThemeChanged, this, &BranchWatchDialog::OnThemeChanged);
connect(settings, &Settings::DebugFontChanged, m_table_model, &BranchWatchTableModel::setFont);
const auto* const host = Host::GetInstance();
connect(host, &Host::PPCSymbolsChanged, m_table_model, &BranchWatchTableModel::UpdateSymbols);
}
void BranchWatchDialog::DisconnectSlots()
{
const auto* const settings = &Settings::Instance();
disconnect(settings, &Settings::EmulationStateChanged, this,
&BranchWatchDialog::OnEmulationStateChanged);
disconnect(settings, &Settings::ThemeChanged, this, &BranchWatchDialog::OnThemeChanged);
disconnect(settings, &Settings::DebugFontChanged, m_table_model,
&BranchWatchTableModel::OnDebugFontChanged);
const auto* const host = Host::GetInstance();
disconnect(host, &Host::PPCSymbolsChanged, m_table_model,
&BranchWatchTableModel::OnPPCSymbolsChanged);
}
void BranchWatchDialog::Show()
{
ConnectSlots();
// Hit every slot that may have missed a signal while this widget was hidden.
OnEmulationStateChanged(Core::GetState(m_system));
OnThemeChanged();
m_table_model->OnDebugFontChanged(Settings::Instance().GetDebugFont());
m_table_model->OnPPCSymbolsChanged();
}
void BranchWatchDialog::Hide()
{
DisconnectSlots();
if (m_timer->isActive())
m_timer->stop();
}
void BranchWatchDialog::LoadQSettings()
{
const auto& settings = Settings::GetQSettings();
restoreGeometry(settings.value(QStringLiteral("branchwatchdialog/geometry")).toByteArray());
m_table_view->horizontalHeader()->restoreState( // Restore column visibility state.
settings.value(QStringLiteral("branchwatchdialog/tableheader/state")).toByteArray());
m_act_branch_type_filters->setVisible(
!settings.value(QStringLiteral("branchwatchdialog/toolbar/branch_type_hidden")).toBool());
m_act_origin_destin_filters->setVisible(
!settings.value(QStringLiteral("branchwatchdialog/toolbar/origin_destin_hidden")).toBool());
m_act_condition_filters->setVisible(
!settings.value(QStringLiteral("branchwatchdialog/toolbar/condition_hidden")).toBool());
m_act_misc_controls->setVisible(
!settings.value(QStringLiteral("branchwatchdialog/toolbar/misc_controls_hidden")).toBool());
}
void BranchWatchDialog::SaveQSettings() const
{
auto& settings = Settings::GetQSettings();
settings.setValue(QStringLiteral("branchwatchdialog/geometry"), saveGeometry());
settings.setValue(QStringLiteral("branchwatchdialog/tableheader/state"),
m_table_view->horizontalHeader()->saveState());
settings.setValue(QStringLiteral("branchwatchdialog/toolbar/branch_type_hidden"),
!m_act_branch_type_filters->isVisible());
settings.setValue(QStringLiteral("branchwatchdialog/toolbar/origin_destin_hidden"),
!m_act_origin_destin_filters->isVisible());
settings.setValue(QStringLiteral("branchwatchdialog/toolbar/condition_hidden"),
!m_act_condition_filters->isVisible());
settings.setValue(QStringLiteral("branchwatchdialog/toolbar/misc_controls_hidden"),
!m_act_misc_controls->isVisible());
}
void BranchWatchDialog::Update() const
{
if (m_branch_watch.GetRecordingPhase() == Core::BranchWatch::Phase::Blacklist)
UpdateStatus();
m_table_model->UpdateHits();
}
void BranchWatchDialog::UpdateStatus() const
{
switch (m_branch_watch.GetRecordingPhase())
{
case Core::BranchWatch::Phase::Blacklist:
{
const std::size_t candidate_size = m_branch_watch.GetCollectionSize();
const std::size_t blacklist_size = m_branch_watch.GetBlacklistSize();
if (blacklist_size == 0)
{
m_status_bar->showMessage(tr("Candidates: %1").arg(candidate_size));
return;
}
m_status_bar->showMessage(tr("Candidates: %1 | Excluded: %2 | Remaining: %3")
.arg(candidate_size)
.arg(blacklist_size)
.arg(candidate_size - blacklist_size));
return;
}
case Core::BranchWatch::Phase::Reduction:
{
const std::size_t candidate_size = m_branch_watch.GetSelection().size();
if (candidate_size == 0)
{
m_status_bar->showMessage(tr("Zero candidates remaining."));
return;
}
const std::size_t remaining_size = m_table_proxy->rowCount();
m_status_bar->showMessage(tr("Candidates: %1 | Filtered: %2 | Remaining: %3")
.arg(candidate_size)
.arg(candidate_size - remaining_size)
.arg(remaining_size));
return;
}
}
}
void BranchWatchDialog::UpdateIcons()
{
m_icn_full = Resources::GetThemeIcon("debugger_breakpoint");
m_icn_partial = Resources::GetThemeIcon("stop");
}
void BranchWatchDialog::Save(const Core::CPUThreadGuard& guard, const std::string& filepath)
{
File::IOFile file(filepath, "w");
if (!file.IsOpen())
{
ModalMessageBox::warning(
this, tr("Error"),
tr("Failed to save Branch Watch snapshot \"%1\"").arg(QString::fromStdString(filepath)));
return;
}
m_table_model->Save(guard, file.GetHandle());
}
void BranchWatchDialog::Load(const Core::CPUThreadGuard& guard, const std::string& filepath)
{
File::IOFile file(filepath, "r");
if (!file.IsOpen())
{
ModalMessageBox::warning(
this, tr("Error"),
tr("Failed to open Branch Watch snapshot \"%1\"").arg(QString::fromStdString(filepath)));
return;
}
m_table_model->Load(guard, file.GetHandle());
m_btn_wipe_recent_hits->setEnabled(m_branch_watch.GetRecordingPhase() ==
Core::BranchWatch::Phase::Reduction);
}
void BranchWatchDialog::AutoSave(const Core::CPUThreadGuard& guard)
{
if (!m_act_autosave->isChecked() || !m_branch_watch.CanSave())
return;
Save(guard, m_autosave_filepath ? m_autosave_filepath.value() : GetSnapshotDefaultFilepath());
}
void BranchWatchDialog::SetStubPatches(u32 value) const
{
auto& debug_interface = m_system.GetPowerPC().GetDebugInterface();
for (const Core::CPUThreadGuard guard(m_system); const QModelIndex& index : m_index_list_temp)
{
debug_interface.SetPatch(guard, m_table_proxy->data(index, UserRole::ClickRole).value<u32>(),
value);
m_table_proxy->SetInspected(index);
}
// TODO: This is not ideal. What I need is a signal for when memory has been changed by the GUI,
// but I cannot find one. UpdateDisasmDialog comes close, but does too much in one signal. For
// example, CodeViewWidget will scroll to the current PC when UpdateDisasmDialog is signaled. This
// seems like a pervasive issue. For example, modifying an instruction in the CodeViewWidget will
// not reflect in the MemoryViewWidget, and vice versa. Neither of these widgets changing memory
// will reflect in the JITWidget, either. At the very least, we can make sure the CodeWidget
// is updated in an acceptable way.
m_code_widget->Update();
}
void BranchWatchDialog::SetEditPatches(u32 (*transform)(u32)) const
{
auto& debug_interface = m_system.GetPowerPC().GetDebugInterface();
for (const Core::CPUThreadGuard guard(m_system); const QModelIndex& index : m_index_list_temp)
{
const Core::BranchWatchCollectionKey& k =
m_table_proxy->GetBranchWatchSelection(index).collection_ptr->first;
// This function assumes patches apply to the origin address, unlike SetStubPatches.
debug_interface.SetPatch(guard, k.origin_addr, transform(k.original_inst.hex));
m_table_proxy->SetInspected(index);
}
// TODO: Same issue as SetStubPatches.
m_code_widget->Update();
}
void BranchWatchDialog::SetBreakpoints(bool break_on_hit, bool log_on_hit) const
{
auto& breakpoints = m_system.GetPowerPC().GetBreakPoints();
for (const QModelIndex& index : m_index_list_temp)
{
const u32 address = m_table_proxy->data(index, UserRole::ClickRole).value<u32>();
breakpoints.Add(address, break_on_hit, log_on_hit, {});
}
emit m_code_widget->BreakpointsChanged();
m_code_widget->Update();
}
void BranchWatchDialog::SetBreakpointMenuActionsIcons() const
{
qsizetype bp_break_count = 0, bp_log_count = 0, bp_both_count = 0;
for (auto& breakpoints = m_system.GetPowerPC().GetBreakPoints();
const QModelIndex& index : m_index_list_temp)
{
if (const TBreakPoint* bp = breakpoints.GetRegularBreakpoint(
m_table_proxy->data(index, UserRole::ClickRole).value<u32>()))
{
if (bp->break_on_hit && bp->log_on_hit)
{
bp_both_count += 1;
continue;
}
bp_break_count += bp->break_on_hit;
bp_log_count += bp->log_on_hit;
}
}
const qsizetype selected_row_count = m_index_list_temp.size();
m_act_break_on_hit->setIconVisibleInMenu(bp_break_count != 0);
m_act_break_on_hit->setIcon(bp_break_count == selected_row_count ? m_icn_full : m_icn_partial);
m_act_log_on_hit->setIconVisibleInMenu(bp_log_count != 0);
m_act_log_on_hit->setIcon(bp_log_count == selected_row_count ? m_icn_full : m_icn_partial);
m_act_both_on_hit->setIconVisibleInMenu(bp_both_count != 0);
m_act_both_on_hit->setIcon(bp_both_count == selected_row_count ? m_icn_full : m_icn_partial);
}
QMenu* BranchWatchDialog::GetTableContextMenu(const QModelIndex& index) const
{
const bool core_initialized = Core::GetState(m_system) != Core::State::Uninitialized;
switch (index.column())
{
case Column::Instruction:
return GetTableContextMenu_Instruction(core_initialized);
case Column::Condition:
return GetTableContextMenu_Condition(core_initialized);
case Column::Origin:
return GetTableContextMenu_Origin(core_initialized);
case Column::Destination:
return GetTableContextMenu_Destin(core_initialized);
case Column::RecentHits:
case Column::TotalHits:
return m_mnu_table_context_other;
case Column::OriginSymbol:
case Column::DestinSymbol:
return GetTableContextMenu_Symbol(core_initialized);
}
static_assert(Column::NumberOfColumns == 8);
Common::Unreachable();
}
QMenu* BranchWatchDialog::GetTableContextMenu_Instruction(bool core_initialized) const
{
const bool all_branches_conditional = // Taking advantage of short-circuit evaluation here.
core_initialized && std::ranges::all_of(m_index_list_temp, [this](const QModelIndex& index) {
return BranchIsConditional(
m_table_proxy->GetBranchWatchSelection(index).collection_ptr->first.original_inst);
});
m_act_invert_condition->setEnabled(all_branches_conditional);
m_act_invert_decrement_check->setEnabled(all_branches_conditional);
return m_mnu_table_context_instruction;
}
QMenu* BranchWatchDialog::GetTableContextMenu_Condition(bool core_initialized) const
{
const bool all_branches_conditional = // Taking advantage of short-circuit evaluation here.
core_initialized && std::ranges::all_of(m_index_list_temp, [this](const QModelIndex& index) {
return BranchIsConditional(
m_table_proxy->GetBranchWatchSelection(index).collection_ptr->first.original_inst);
});
m_act_make_unconditional->setEnabled(all_branches_conditional);
return m_mnu_table_context_condition;
}
QMenu* BranchWatchDialog::GetTableContextMenu_Origin(bool core_initialized) const
{
SetBreakpointMenuActionsIcons();
m_act_insert_nop->setEnabled(core_initialized);
m_act_copy_address->setEnabled(true);
m_mnu_set_breakpoint->setEnabled(true);
return m_mnu_table_context_origin;
}
QMenu* BranchWatchDialog::GetTableContextMenu_Destin(bool core_initialized) const
{
SetBreakpointMenuActionsIcons();
const bool all_branches_save_lr = // Taking advantage of short-circuit evaluation here.
core_initialized && std::ranges::all_of(m_index_list_temp, [this](const QModelIndex& index) {
return m_table_proxy->GetBranchWatchSelection(index).collection_ptr->first.original_inst.LK;
});
m_act_insert_blr->setEnabled(all_branches_save_lr);
m_act_copy_address->setEnabled(true);
m_mnu_set_breakpoint->setEnabled(true);
return m_mnu_table_context_destin_or_symbol;
}
QMenu* BranchWatchDialog::GetTableContextMenu_Symbol(bool core_initialized) const
{
SetBreakpointMenuActionsIcons();
const bool all_symbols_valid =
std::ranges::all_of(m_index_list_temp, [this](const QModelIndex& index) {
return m_table_proxy->data(index, UserRole::ClickRole).isValid();
});
m_act_insert_blr->setEnabled(core_initialized && all_symbols_valid);
m_act_copy_address->setEnabled(all_symbols_valid);
m_mnu_set_breakpoint->setEnabled(all_symbols_valid);
return m_mnu_table_context_destin_or_symbol;
}