// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team // SPDX-License-Identifier: GPL-3.0+ #include "BreakpointView.h" #include "QtUtils.h" #include "Debugger/DebuggerSettingsManager.h" #include "BreakpointDialog.h" #include "BreakpointModel.h" #include BreakpointView::BreakpointView(const DebuggerViewParameters& parameters) : DebuggerView(parameters, DISALLOW_MULTIPLE_INSTANCES) , m_model(BreakpointModel::getInstance(cpu())) { m_ui.setupUi(this); m_ui.breakpointList->setContextMenuPolicy(Qt::CustomContextMenu); connect(m_ui.breakpointList, &QTableView::customContextMenuRequested, this, &BreakpointView::openContextMenu); connect(m_ui.breakpointList, &QTableView::doubleClicked, this, &BreakpointView::onDoubleClicked); m_ui.breakpointList->setModel(m_model); m_ui.breakpointList->horizontalHeader()->setSectionsMovable(true); this->resizeColumns(); } void BreakpointView::onDoubleClicked(const QModelIndex& index) { if (index.isValid() && index.column() == BreakpointModel::OFFSET) goToInDisassembler(m_model->data(index, BreakpointModel::DataRole).toUInt(), true); } void BreakpointView::openContextMenu(QPoint pos) { QMenu* menu = new QMenu(m_ui.breakpointList); menu->setAttribute(Qt::WA_DeleteOnClose); if (cpu().isAlive()) { QAction* newAction = menu->addAction(tr("New")); connect(newAction, &QAction::triggered, this, &BreakpointView::contextNew); const QItemSelectionModel* selModel = m_ui.breakpointList->selectionModel(); if (selModel->hasSelection()) { QAction* editAction = menu->addAction(tr("Edit")); connect(editAction, &QAction::triggered, this, &BreakpointView::contextEdit); if (selModel->selectedIndexes().count() == 1) { QAction* copyAction = menu->addAction(tr("Copy")); connect(copyAction, &QAction::triggered, this, &BreakpointView::contextCopy); } QAction* deleteAction = menu->addAction(tr("Delete")); connect(deleteAction, &QAction::triggered, this, &BreakpointView::contextDelete); } } menu->addSeparator(); if (m_model->rowCount() > 0) { QAction* actionExport = menu->addAction(tr("Copy all as CSV")); connect(actionExport, &QAction::triggered, [this]() { // It's important to use the Export Role here to allow pasting to be translation agnostic QGuiApplication::clipboard()->setText( QtUtils::AbstractItemModelToCSV(m_model, BreakpointModel::ExportRole, true)); }); } if (cpu().isAlive()) { QAction* actionImport = menu->addAction(tr("Paste from CSV")); connect(actionImport, &QAction::triggered, this, &BreakpointView::contextPasteCSV); if (cpu().getCpuType() == BREAKPOINT_EE) { QAction* actionLoad = menu->addAction(tr("Load from Settings")); connect(actionLoad, &QAction::triggered, [this]() { m_model->clear(); DebuggerSettingsManager::loadGameSettings(m_model); }); QAction* actionSave = menu->addAction(tr("Save to Settings")); connect(actionSave, &QAction::triggered, this, &BreakpointView::saveBreakpointsToDebuggerSettings); } } menu->popup(m_ui.breakpointList->viewport()->mapToGlobal(pos)); } void BreakpointView::contextCopy() { const QItemSelectionModel* selModel = m_ui.breakpointList->selectionModel(); if (!selModel->hasSelection()) return; QGuiApplication::clipboard()->setText(m_model->data(selModel->currentIndex()).toString()); } void BreakpointView::contextDelete() { const QItemSelectionModel* selModel = m_ui.breakpointList->selectionModel(); if (!selModel->hasSelection()) return; QModelIndexList indices = selModel->selectedIndexes(); std::set rows; for (QModelIndex index : indices) rows.emplace(index.row()); for (auto row = rows.rbegin(); row != rows.rend(); row++) m_model->removeRows(*row, 1); } void BreakpointView::contextNew() { BreakpointDialog* bpDialog = new BreakpointDialog(this, &cpu(), *m_model); connect(bpDialog, &QDialog::accepted, this, &BreakpointView::resizeColumns); bpDialog->setAttribute(Qt::WA_DeleteOnClose); bpDialog->show(); } void BreakpointView::contextEdit() { const QItemSelectionModel* selModel = m_ui.breakpointList->selectionModel(); if (!selModel->hasSelection()) return; const int selectedRow = selModel->selectedIndexes().first().row(); auto bpObject = m_model->at(selectedRow); BreakpointDialog* bpDialog = new BreakpointDialog(this, &cpu(), *m_model, bpObject, selectedRow); connect(bpDialog, &QDialog::accepted, this, &BreakpointView::resizeColumns); bpDialog->setAttribute(Qt::WA_DeleteOnClose); bpDialog->show(); } void BreakpointView::contextPasteCSV() { QString csv = QGuiApplication::clipboard()->text(); // Skip header csv = csv.mid(csv.indexOf('\n') + 1); for (const QString& line : csv.split('\n')) { QStringList fields; // In order to handle text with commas in them we must wrap values in quotes to mark // where a value starts and end so that text commas aren't identified as delimiters. // So matches each quote pair, parse it out, and removes the quotes to get the value. QRegularExpression eachQuotePair(R"("([^"]|\\.)*")"); QRegularExpressionMatchIterator it = eachQuotePair.globalMatch(line); while (it.hasNext()) { QRegularExpressionMatch match = it.next(); QString matchedValue = match.captured(0); fields << matchedValue.mid(1, matchedValue.length() - 2); } m_model->loadBreakpointFromFieldList(fields); } } void BreakpointView::saveBreakpointsToDebuggerSettings() { DebuggerSettingsManager::saveGameSettings(m_model); } void BreakpointView::resizeColumns() { for (std::size_t i = 0; auto mode : BreakpointModel::HeaderResizeModes) { m_ui.breakpointList->horizontalHeader()->setSectionResizeMode(i, mode); i++; } }