dolphin/Source/Core/DolphinQt/Debugger/AssemblerWidget.cpp
JosJuice 6ca2da53e8 Partially revert "Revert "Audit uses of IsRunning and GetState""
This reverts the revert commit bc67fc97c39628c76a4dbca411b0e8a9bfaf726a,
except for the changes in BaseConfigLoader.cpp, which caused the bug
that made us revert 72cf2bdb87f09deff22e1085de3290126aa4ad05. PR 12917
contains an improved change to BaseConfigLoader.cpp, which can be merged
(or rejected) independently.

A few changes have also been made based on review comments.
2024-10-04 18:35:41 +02:00

958 lines
26 KiB
C++

// Copyright 2023 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "DolphinQt/Debugger/AssemblerWidget.h"
#include <QAction>
#include <QApplication>
#include <QClipboard>
#include <QComboBox>
#include <QFont>
#include <QFontDatabase>
#include <QGridLayout>
#include <QGroupBox>
#include <QLabel>
#include <QLineEdit>
#include <QMenu>
#include <QPlainTextEdit>
#include <QPushButton>
#include <QScrollBar>
#include <QShortcut>
#include <QStyle>
#include <QTabWidget>
#include <QTextBlock>
#include <QTextEdit>
#include <QToolBar>
#include <QToolButton>
#include <filesystem>
#include <fmt/format.h>
#include "Common/Assert.h"
#include "Common/FileUtil.h"
#include "Core/Core.h"
#include "Core/PowerPC/MMU.h"
#include "Core/PowerPC/PowerPC.h"
#include "Core/System.h"
#include "DolphinQt/Debugger/AssemblyEditor.h"
#include "DolphinQt/QtUtils/DolphinFileDialog.h"
#include "DolphinQt/QtUtils/ModalMessageBox.h"
#include "DolphinQt/Resources.h"
#include "DolphinQt/Settings.h"
namespace
{
using namespace Common::GekkoAssembler;
QString HtmlFormatErrorLoc(const AssemblerError& err)
{
return QObject::tr("<span style=\"color: red; font-weight: bold\">Error</span> on line %1 col %2")
.arg(err.line + 1)
.arg(err.col + 1);
}
QString HtmlFormatErrorLine(const AssemblerError& err)
{
const QString line_pre_error =
QString::fromStdString(std::string(err.error_line.substr(0, err.col))).toHtmlEscaped();
const QString line_error =
QString::fromStdString(std::string(err.error_line.substr(err.col, err.len))).toHtmlEscaped();
const QString line_post_error =
QString::fromStdString(std::string(err.error_line.substr(err.col + err.len))).toHtmlEscaped();
return QStringLiteral("<span style=\"font-family:'monospace';font-size:16px\">"
"<pre>%1<u><span style=\"color:red;font-weight:bold\">%2</span></u>%3</pre>"
"</span>")
.arg(line_pre_error)
.arg(line_error)
.arg(line_post_error);
}
QString HtmlFormatMessage(const AssemblerError& err)
{
return QStringLiteral("<span>%1</span>").arg(QString::fromStdString(err.message).toHtmlEscaped());
}
void DeserializeBlock(const CodeBlock& blk, std::ostringstream& out_str, bool pad4)
{
size_t i = 0;
for (; i < blk.instructions.size(); i++)
{
out_str << fmt::format("{:02x}", blk.instructions[i]);
if (i % 8 == 7)
{
out_str << '\n';
}
else if (i % 4 == 3)
{
out_str << ' ';
}
}
if (pad4)
{
bool did_pad = false;
for (; i % 4 != 0; i++)
{
out_str << "00";
did_pad = true;
}
if (did_pad)
{
out_str << (i % 8 == 0 ? '\n' : ' ');
}
}
else if (i % 8 != 7)
{
out_str << '\n';
}
}
void DeserializeToRaw(const std::vector<CodeBlock>& blocks, std::ostringstream& out_str)
{
for (const auto& blk : blocks)
{
if (blk.instructions.empty())
{
continue;
}
out_str << fmt::format("# Block {:08x}\n", blk.block_address);
DeserializeBlock(blk, out_str, false);
}
}
void DeserializeToAr(const std::vector<CodeBlock>& blocks, std::ostringstream& out_str)
{
for (const auto& blk : blocks)
{
if (blk.instructions.empty())
{
continue;
}
size_t i = 0;
for (; i < blk.instructions.size() - 3; i += 4)
{
// type=NormalCode, subtype=SUB_RAM_WRITE, size=32bit
const u32 ar_addr = ((blk.block_address + i) & 0x1ffffff) | 0x04000000;
out_str << fmt::format("{:08x} {:02x}{:02x}{:02x}{:02x}\n", ar_addr, blk.instructions[i],
blk.instructions[i + 1], blk.instructions[i + 2],
blk.instructions[i + 3]);
}
for (; i < blk.instructions.size(); i++)
{
// type=NormalCode, subtype=SUB_RAM_WRITE, size=8bit
const u32 ar_addr = ((blk.block_address + i) & 0x1ffffff);
out_str << fmt::format("{:08x} 000000{:02x}\n", ar_addr, blk.instructions[i]);
}
}
}
void DeserializeToGecko(const std::vector<CodeBlock>& blocks, std::ostringstream& out_str)
{
DeserializeToAr(blocks, out_str);
}
void DeserializeToGeckoExec(const std::vector<CodeBlock>& blocks, std::ostringstream& out_str)
{
for (const auto& blk : blocks)
{
if (blk.instructions.empty())
{
continue;
}
u32 nlines = 1 + static_cast<u32>((blk.instructions.size() - 1) / 8);
bool ret_on_newline = false;
if (blk.instructions.size() % 8 == 0 || blk.instructions.size() % 8 > 4)
{
// Append extra line for blr
nlines++;
ret_on_newline = true;
}
out_str << fmt::format("c0000000 {:08x}\n", nlines);
DeserializeBlock(blk, out_str, true);
if (ret_on_newline)
{
out_str << "4e800020 00000000\n";
}
else
{
out_str << "4e800020\n";
}
}
}
void DeserializeToGeckoTramp(const std::vector<CodeBlock>& blocks, std::ostringstream& out_str)
{
for (const auto& blk : blocks)
{
if (blk.instructions.empty())
{
continue;
}
const u32 inject_addr = (blk.block_address & 0x1ffffff) | 0x02000000;
u32 nlines = 1 + static_cast<u32>((blk.instructions.size() - 1) / 8);
bool padding_on_newline = false;
if (blk.instructions.size() % 8 == 0 || blk.instructions.size() % 8 > 4)
{
// Append extra line for nop+branchback
nlines++;
padding_on_newline = true;
}
out_str << fmt::format("c{:07x} {:08x}\n", inject_addr, nlines);
DeserializeBlock(blk, out_str, true);
if (padding_on_newline)
{
out_str << "60000000 00000000\n";
}
else
{
out_str << "00000000\n";
}
}
}
} // namespace
AssemblerWidget::AssemblerWidget(QWidget* parent)
: QDockWidget(parent), m_system(Core::System::GetInstance()), m_unnamed_editor_count(0),
m_net_zoom_delta(0)
{
{
QPalette base_palette;
m_dark_scheme = base_palette.color(QPalette::WindowText).value() >
base_palette.color(QPalette::Window).value();
}
setWindowTitle(tr("Assembler"));
setObjectName(QStringLiteral("assemblerwidget"));
setHidden(!Settings::Instance().IsAssemblerVisible() ||
!Settings::Instance().IsDebugModeEnabled());
this->setVisible(true);
CreateWidgets();
restoreGeometry(
Settings::GetQSettings().value(QStringLiteral("assemblerwidget/geometry")).toByteArray());
setFloating(Settings::GetQSettings().value(QStringLiteral("assemblerwidget/floating")).toBool());
connect(&Settings::Instance(), &Settings::AssemblerVisibilityChanged, this,
[this](bool visible) { setHidden(!visible); });
connect(&Settings::Instance(), &Settings::DebugModeToggled, this, [this](bool enabled) {
setHidden(!enabled || !Settings::Instance().IsAssemblerVisible());
});
connect(&Settings::Instance(), &Settings::EmulationStateChanged, this,
&AssemblerWidget::OnEmulationStateChanged);
connect(&Settings::Instance(), &Settings::ThemeChanged, this, &AssemblerWidget::UpdateIcons);
connect(m_asm_tabs, &QTabWidget::tabCloseRequested, this, &AssemblerWidget::OnTabClose);
auto* save_shortcut = new QShortcut(QKeySequence::Save, this);
// Save should only activate if the active tab is in focus
save_shortcut->connect(save_shortcut, &QShortcut::activated, this, [this] {
if (m_asm_tabs->currentIndex() != -1 && m_asm_tabs->currentWidget()->hasFocus())
{
OnSave();
}
});
auto* zoom_in_shortcut = new QShortcut(QKeySequence::ZoomIn, this);
zoom_in_shortcut->setContext(Qt::WidgetWithChildrenShortcut);
connect(zoom_in_shortcut, &QShortcut::activated, this, &AssemblerWidget::OnZoomIn);
auto* zoom_out_shortcut = new QShortcut(QKeySequence::ZoomOut, this);
zoom_out_shortcut->setContext(Qt::WidgetWithChildrenShortcut);
connect(zoom_out_shortcut, &QShortcut::activated, this, &AssemblerWidget::OnZoomOut);
auto* zoom_in_alternate = new QShortcut(QKeySequence(Qt::CTRL | Qt::Key_Equal), this);
zoom_in_alternate->setContext(Qt::WidgetWithChildrenShortcut);
connect(zoom_in_alternate, &QShortcut::activated, this, &AssemblerWidget::OnZoomIn);
auto* zoom_out_alternate = new QShortcut(QKeySequence(Qt::CTRL | Qt::Key_Underscore), this);
zoom_out_alternate->setContext(Qt::WidgetWithChildrenShortcut);
connect(zoom_out_alternate, &QShortcut::activated, this, &AssemblerWidget::OnZoomOut);
auto* zoom_reset = new QShortcut(QKeySequence(Qt::CTRL | Qt::Key_0), this);
zoom_reset->setContext(Qt::WidgetWithChildrenShortcut);
connect(zoom_reset, &QShortcut::activated, this, &AssemblerWidget::OnZoomReset);
ConnectWidgets();
UpdateIcons();
}
void AssemblerWidget::closeEvent(QCloseEvent*)
{
Settings::Instance().SetAssemblerVisible(false);
}
bool AssemblerWidget::ApplicationCloseRequest()
{
int num_unsaved = 0;
for (int i = 0; i < m_asm_tabs->count(); i++)
{
if (GetEditor(i)->IsDirty())
{
num_unsaved++;
}
}
if (num_unsaved > 0)
{
const int result = ModalMessageBox::question(
this, tr("Unsaved Changes"),
tr("You have %1 unsaved assembly tabs open\n\n"
"Do you want to save all and exit?")
.arg(num_unsaved),
QMessageBox::YesToAll | QMessageBox::NoToAll | QMessageBox::Cancel, QMessageBox::Cancel);
switch (result)
{
case QMessageBox::YesToAll:
for (int i = 0; i < m_asm_tabs->count(); i++)
{
AsmEditor* editor = GetEditor(i);
if (editor->IsDirty())
{
if (!SaveEditor(editor))
{
return false;
}
}
}
return true;
case QMessageBox::NoToAll:
return true;
case QMessageBox::Cancel:
return false;
}
}
return true;
}
AssemblerWidget::~AssemblerWidget()
{
auto& settings = Settings::GetQSettings();
settings.setValue(QStringLiteral("assemblerwidget/geometry"), saveGeometry());
settings.setValue(QStringLiteral("assemblerwidget/floating"), isFloating());
}
void AssemblerWidget::CreateWidgets()
{
m_asm_tabs = new QTabWidget;
m_toolbar = new QToolBar;
m_output_type = new QComboBox;
m_output_box = new QPlainTextEdit;
m_error_box = new QTextEdit;
m_address_line = new QLineEdit;
m_copy_output_button = new QPushButton;
m_asm_tabs->setTabsClosable(true);
// Initialize toolbar and actions
// m_toolbar->setIconSize(QSize(32, 32));
m_toolbar->setContentsMargins(0, 0, 0, 0);
m_toolbar->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
m_open = m_toolbar->addAction(tr("Open"), this, &AssemblerWidget::OnOpen);
m_new = m_toolbar->addAction(tr("New"), this, &AssemblerWidget::OnNew);
m_assemble = m_toolbar->addAction(tr("Assemble"), this, [this] {
std::vector<CodeBlock> unused;
OnAssemble(&unused);
});
m_inject = m_toolbar->addAction(tr("Inject"), this, &AssemblerWidget::OnInject);
m_save = m_toolbar->addAction(tr("Save"), this, &AssemblerWidget::OnSave);
m_inject->setEnabled(false);
m_save->setEnabled(false);
m_assemble->setEnabled(false);
// Initialize input, output, error text areas
auto palette = m_output_box->palette();
if (m_dark_scheme)
{
palette.setColor(QPalette::Base, QColor::fromRgb(76, 76, 76));
}
else
{
palette.setColor(QPalette::Base, QColor::fromRgb(180, 180, 180));
}
m_output_box->setPalette(palette);
m_error_box->setPalette(palette);
QFont mono_font(QFontDatabase::systemFont(QFontDatabase::FixedFont).family());
QFont error_font(QFontDatabase::systemFont(QFontDatabase::GeneralFont).family());
mono_font.setPointSize(12);
error_font.setPointSize(12);
QFontMetrics mono_metrics(mono_font);
QFontMetrics err_metrics(mono_font);
m_output_box->setFont(mono_font);
m_error_box->setFont(error_font);
m_output_box->setReadOnly(true);
m_error_box->setReadOnly(true);
const int output_area_width = mono_metrics.horizontalAdvance(QLatin1Char('0')) * OUTPUT_BOX_WIDTH;
m_error_box->setVerticalScrollBarPolicy(Qt::ScrollBarPolicy::ScrollBarAlwaysOff);
m_error_box->setFixedHeight(err_metrics.height() * 3 + mono_metrics.height());
m_output_box->setFixedWidth(output_area_width);
m_error_box->setVerticalScrollBarPolicy(Qt::ScrollBarPolicy::ScrollBarAlwaysOff);
// Initialize output format selection box
m_output_type->addItem(tr("Raw"));
m_output_type->addItem(tr("AR Code"));
m_output_type->addItem(tr("Gecko (04)"));
m_output_type->addItem(tr("Gecko (C0)"));
m_output_type->addItem(tr("Gecko (C2)"));
// Setup layouts
auto* addr_input_layout = new QHBoxLayout;
addr_input_layout->addWidget(new QLabel(tr("Base Address")));
addr_input_layout->addWidget(m_address_line);
auto* output_extra_layout = new QHBoxLayout;
output_extra_layout->addWidget(m_output_type);
output_extra_layout->addWidget(m_copy_output_button);
QWidget* address_input_box = new QWidget();
address_input_box->setLayout(addr_input_layout);
addr_input_layout->setContentsMargins(0, 0, 0, 0);
QWidget* output_extra_box = new QWidget();
output_extra_box->setFixedWidth(output_area_width);
output_extra_box->setLayout(output_extra_layout);
output_extra_layout->setContentsMargins(0, 0, 0, 0);
auto* assembler_layout = new QGridLayout;
assembler_layout->setSpacing(0);
assembler_layout->setContentsMargins(5, 0, 5, 5);
assembler_layout->addWidget(m_toolbar, 0, 0, 1, 2);
{
auto* input_group = new QGroupBox(tr("Input"));
auto* layout = new QVBoxLayout;
input_group->setLayout(layout);
layout->addWidget(m_asm_tabs);
layout->addWidget(address_input_box);
assembler_layout->addWidget(input_group, 1, 0, 1, 1);
}
{
auto* output_group = new QGroupBox(tr("Output"));
auto* layout = new QGridLayout;
output_group->setLayout(layout);
layout->addWidget(m_output_box, 0, 0);
layout->addWidget(output_extra_box, 1, 0);
assembler_layout->addWidget(output_group, 1, 1, 1, 1);
output_group->setSizePolicy(
QSizePolicy(QSizePolicy::Policy::Fixed, QSizePolicy::Policy::Expanding));
}
{
auto* error_group = new QGroupBox(tr("Error Log"));
auto* layout = new QHBoxLayout;
error_group->setLayout(layout);
layout->addWidget(m_error_box);
assembler_layout->addWidget(error_group, 2, 0, 1, 2);
error_group->setSizePolicy(
QSizePolicy(QSizePolicy::Policy::Expanding, QSizePolicy::Policy::Fixed));
}
QWidget* widget = new QWidget;
widget->setLayout(assembler_layout);
setWidget(widget);
}
void AssemblerWidget::ConnectWidgets()
{
m_output_box->connect(m_output_box, &QPlainTextEdit::updateRequest, this, [this] {
if (m_output_box->verticalScrollBar()->isVisible())
{
m_output_box->setFixedWidth(m_output_box->fontMetrics().horizontalAdvance(QLatin1Char('0')) *
OUTPUT_BOX_WIDTH +
m_output_box->style()->pixelMetric(QStyle::PM_ScrollBarExtent));
}
else
{
m_output_box->setFixedWidth(m_output_box->fontMetrics().horizontalAdvance(QLatin1Char('0')) *
OUTPUT_BOX_WIDTH);
}
});
m_copy_output_button->connect(m_copy_output_button, &QPushButton::released, this,
&AssemblerWidget::OnCopyOutput);
m_address_line->connect(m_address_line, &QLineEdit::textChanged, this,
&AssemblerWidget::OnBaseAddressChanged);
m_asm_tabs->connect(m_asm_tabs, &QTabWidget::currentChanged, this, &AssemblerWidget::OnTabChange);
}
void AssemblerWidget::OnAssemble(std::vector<CodeBlock>* asm_out)
{
if (m_asm_tabs->currentIndex() == -1)
{
return;
}
AsmEditor* active_editor = GetEditor(m_asm_tabs->currentIndex());
AsmKind kind = AsmKind::Raw;
m_error_box->clear();
m_output_box->clear();
switch (m_output_type->currentIndex())
{
case 0:
kind = AsmKind::Raw;
break;
case 1:
kind = AsmKind::ActionReplay;
break;
case 2:
kind = AsmKind::Gecko;
break;
case 3:
kind = AsmKind::GeckoExec;
break;
case 4:
kind = AsmKind::GeckoTrampoline;
break;
}
bool good;
u32 base_address = m_address_line->text().toUInt(&good, 16);
if (!good)
{
base_address = 0;
m_error_box->append(
tr("<span style=\"color:#ffcc00\">Warning</span> invalid base address, defaulting to 0"));
}
const std::string contents = active_editor->toPlainText().toStdString();
auto result = Assemble(contents, base_address);
if (IsFailure(result))
{
m_error_box->clear();
asm_out->clear();
const AssemblerError& error = GetFailure(result);
m_error_box->append(HtmlFormatErrorLoc(error));
m_error_box->append(HtmlFormatErrorLine(error));
m_error_box->append(HtmlFormatMessage(error));
asm_out->clear();
return;
}
auto& blocks = GetT(result);
std::ostringstream str_contents;
switch (kind)
{
case AsmKind::Raw:
DeserializeToRaw(blocks, str_contents);
break;
case AsmKind::ActionReplay:
DeserializeToAr(blocks, str_contents);
break;
case AsmKind::Gecko:
DeserializeToGecko(blocks, str_contents);
break;
case AsmKind::GeckoExec:
DeserializeToGeckoExec(blocks, str_contents);
break;
case AsmKind::GeckoTrampoline:
DeserializeToGeckoTramp(blocks, str_contents);
break;
}
m_output_box->appendPlainText(QString::fromStdString(str_contents.str()));
m_output_box->moveCursor(QTextCursor::MoveOperation::Start);
m_output_box->ensureCursorVisible();
*asm_out = std::move(GetT(result));
}
void AssemblerWidget::OnCopyOutput()
{
QApplication::clipboard()->setText(m_output_box->toPlainText());
}
void AssemblerWidget::OnOpen()
{
const std::string default_dir = File::GetUserPath(D_ASM_ROOT_IDX);
const QStringList paths = DolphinFileDialog::getOpenFileNames(
this, tr("Select a File"), QString::fromStdString(default_dir),
QStringLiteral("%1 (*.s *.S *.asm);;%2 (*)")
.arg(tr("All Assembly files"))
.arg(tr("All Files")));
if (paths.isEmpty())
{
return;
}
std::optional<int> show_index;
for (auto path : paths)
{
show_index = std::nullopt;
for (int i = 0; i < m_asm_tabs->count(); i++)
{
AsmEditor* editor = GetEditor(i);
if (editor->PathsMatch(path))
{
show_index = i;
break;
}
}
if (!show_index)
{
NewEditor(path);
}
}
if (show_index)
{
m_asm_tabs->setCurrentIndex(*show_index);
}
}
void AssemblerWidget::OnNew()
{
NewEditor();
}
void AssemblerWidget::OnInject()
{
Core::CPUThreadGuard guard(m_system);
std::vector<CodeBlock> asm_result;
OnAssemble(&asm_result);
for (const auto& blk : asm_result)
{
if (!PowerPC::MMU::HostIsRAMAddress(guard, blk.block_address) || blk.instructions.empty())
{
continue;
}
m_system.GetPowerPC().GetDebugInterface().SetPatch(guard, blk.block_address, blk.instructions);
}
}
void AssemblerWidget::OnSave()
{
if (m_asm_tabs->currentIndex() == -1)
{
return;
}
AsmEditor* active_editor = GetEditor(m_asm_tabs->currentIndex());
SaveEditor(active_editor);
}
void AssemblerWidget::OnZoomIn()
{
if (m_asm_tabs->currentIndex() != -1)
{
ZoomAllEditors(2);
}
}
void AssemblerWidget::OnZoomOut()
{
if (m_asm_tabs->currentIndex() != -1)
{
ZoomAllEditors(-2);
}
}
void AssemblerWidget::OnZoomReset()
{
if (m_asm_tabs->currentIndex() != -1)
{
ZoomAllEditors(-m_net_zoom_delta);
}
}
void AssemblerWidget::OnBaseAddressChanged()
{
if (m_asm_tabs->currentIndex() == -1)
{
return;
}
AsmEditor* active_editor = GetEditor(m_asm_tabs->currentIndex());
active_editor->SetBaseAddress(m_address_line->text());
}
void AssemblerWidget::OnTabChange(int index)
{
if (index == -1)
{
m_address_line->clear();
return;
}
AsmEditor* active_editor = GetEditor(index);
m_address_line->setText(active_editor->BaseAddress());
}
QString AssemblerWidget::TabTextForEditor(AsmEditor* editor, bool with_dirty)
{
ASSERT(editor != nullptr);
QString result;
if (!editor->Path().isEmpty())
result = editor->EditorTitle();
else if (editor->EditorNum() == 0)
result = tr("New File");
else
result = tr("New File (%1)").arg(editor->EditorNum() + 1);
if (with_dirty && editor->IsDirty())
{
// i18n: This asterisk is added to the title of an editor to indicate that it has unsaved
// changes
result = tr("%1 *").arg(result);
}
return result;
}
AsmEditor* AssemblerWidget::GetEditor(int idx)
{
return qobject_cast<AsmEditor*>(m_asm_tabs->widget(idx));
}
void AssemblerWidget::NewEditor(const QString& path)
{
AsmEditor* new_editor =
new AsmEditor(path, path.isEmpty() ? AllocateTabNum() : INVALID_EDITOR_NUM, m_dark_scheme);
if (!path.isEmpty() && !new_editor->LoadFromPath())
{
ModalMessageBox::warning(this, tr("Failed to open file"),
tr("Failed to read the contents of file:\n%1").arg(path));
delete new_editor;
return;
}
const int tab_idx = m_asm_tabs->addTab(new_editor, QStringLiteral());
new_editor->connect(new_editor, &AsmEditor::PathChanged, this, [this] {
AsmEditor* updated_tab = qobject_cast<AsmEditor*>(sender());
DisambiguateTabTitles(updated_tab);
UpdateTabText(updated_tab);
});
new_editor->connect(new_editor, &AsmEditor::DirtyChanged, this,
[this] { UpdateTabText(qobject_cast<AsmEditor*>(sender())); });
new_editor->connect(new_editor, &AsmEditor::ZoomRequested, this,
&AssemblerWidget::ZoomAllEditors);
new_editor->Zoom(m_net_zoom_delta);
DisambiguateTabTitles(new_editor);
m_asm_tabs->setTabText(tab_idx, TabTextForEditor(new_editor, true));
if (m_save && m_assemble)
{
m_save->setEnabled(true);
m_assemble->setEnabled(true);
}
m_asm_tabs->setCurrentIndex(tab_idx);
}
bool AssemblerWidget::SaveEditor(AsmEditor* editor)
{
QString save_path = editor->Path();
if (save_path.isEmpty())
{
const std::string default_dir = File::GetUserPath(D_ASM_ROOT_IDX);
const QString asm_filter = QStringLiteral("%1 (*.S)").arg(tr("Assembly File"));
const QString all_filter = QStringLiteral("%2 (*)").arg(tr("All Files"));
QString selected_filter;
save_path = DolphinFileDialog::getSaveFileName(
this, tr("Save File To"), QString::fromStdString(default_dir),
QStringLiteral("%1;;%2").arg(asm_filter).arg(all_filter), &selected_filter);
if (save_path.isEmpty())
{
return false;
}
if (selected_filter == asm_filter &&
std::filesystem::path(save_path.toStdString()).extension().empty())
{
save_path.append(QStringLiteral(".S"));
}
}
editor->SaveFile(save_path);
return true;
}
void AssemblerWidget::OnEmulationStateChanged(Core::State state)
{
m_inject->setEnabled(state == Core::State::Running || state == Core::State::Paused);
}
void AssemblerWidget::OnTabClose(int index)
{
ASSERT(index < m_asm_tabs->count());
AsmEditor* editor = GetEditor(index);
if (editor->IsDirty())
{
const int result = ModalMessageBox::question(
this, tr("Unsaved Changes"),
tr("There are unsaved changes in \"%1\".\n\n"
"Do you want to save before closing?")
.arg(TabTextForEditor(editor, false)),
QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel, QMessageBox::Cancel);
switch (result)
{
case QMessageBox::Yes:
if (editor->IsDirty())
{
if (!SaveEditor(editor))
{
return;
}
}
break;
case QMessageBox::No:
break;
case QMessageBox::Cancel:
return;
}
}
CloseTab(index, editor);
}
void AssemblerWidget::CloseTab(int index, AsmEditor* editor)
{
FreeTabNum(editor->EditorNum());
m_asm_tabs->removeTab(index);
editor->deleteLater();
DisambiguateTabTitles(nullptr);
if (m_asm_tabs->count() == 0 && m_save && m_assemble)
{
m_save->setEnabled(false);
m_assemble->setEnabled(false);
}
}
int AssemblerWidget::AllocateTabNum()
{
auto min_it = std::min_element(m_free_editor_nums.begin(), m_free_editor_nums.end());
if (min_it == m_free_editor_nums.end())
{
return m_unnamed_editor_count++;
}
const int min = *min_it;
m_free_editor_nums.erase(min_it);
return min;
}
void AssemblerWidget::FreeTabNum(int num)
{
if (num != INVALID_EDITOR_NUM)
{
m_free_editor_nums.push_back(num);
}
}
void AssemblerWidget::UpdateTabText(AsmEditor* editor)
{
int tab_idx = 0;
for (; tab_idx < m_asm_tabs->count(); tab_idx++)
{
if (m_asm_tabs->widget(tab_idx) == editor)
{
break;
}
}
ASSERT(tab_idx < m_asm_tabs->count());
m_asm_tabs->setTabText(tab_idx, TabTextForEditor(editor, true));
}
void AssemblerWidget::DisambiguateTabTitles(AsmEditor* new_tab)
{
for (int i = 0; i < m_asm_tabs->count(); i++)
{
AsmEditor* check = GetEditor(i);
if (check->IsAmbiguous())
{
// Could group all editors with matching titles in a linked list
// but tracking that nicely without dangling pointers feels messy
bool still_ambiguous = false;
for (int j = 0; j < m_asm_tabs->count(); j++)
{
AsmEditor* against = GetEditor(j);
if (j != i && check->FileName() == against->FileName())
{
if (!against->IsAmbiguous())
{
against->SetAmbiguous(true);
UpdateTabText(against);
}
still_ambiguous = true;
}
}
if (!still_ambiguous)
{
check->SetAmbiguous(false);
UpdateTabText(check);
}
}
}
if (new_tab != nullptr)
{
bool is_ambiguous = false;
for (int i = 0; i < m_asm_tabs->count(); i++)
{
AsmEditor* against = GetEditor(i);
if (new_tab != against && against->FileName() == new_tab->FileName())
{
against->SetAmbiguous(true);
UpdateTabText(against);
is_ambiguous = true;
}
}
if (is_ambiguous)
{
new_tab->SetAmbiguous(true);
UpdateTabText(new_tab);
}
}
}
void AssemblerWidget::UpdateIcons()
{
m_new->setIcon(Resources::GetThemeIcon("assembler_new"));
m_open->setIcon(Resources::GetThemeIcon("assembler_openasm"));
m_save->setIcon(Resources::GetThemeIcon("assembler_save"));
m_assemble->setIcon(Resources::GetThemeIcon("assembler_assemble"));
m_inject->setIcon(Resources::GetThemeIcon("assembler_inject"));
m_copy_output_button->setIcon(Resources::GetThemeIcon("assembler_clipboard"));
}
void AssemblerWidget::ZoomAllEditors(int amount)
{
if (amount != 0)
{
m_net_zoom_delta += amount;
for (int i = 0; i < m_asm_tabs->count(); i++)
{
GetEditor(i)->Zoom(amount);
}
}
}