Qt: Split Cheat Manager and Memory Scanner

And make them persistent across system invocations.
This commit is contained in:
Stenzek 2024-04-10 13:56:49 +10:00
parent e936e7498a
commit 4598bc789b
No known key found for this signature in database
15 changed files with 2006 additions and 1404 deletions

View file

@ -32,9 +32,9 @@ set(SRCS
cheatcodeeditordialog.cpp
cheatcodeeditordialog.h
cheatcodeeditordialog.ui
cheatmanagerdialog.cpp
cheatmanagerdialog.h
cheatmanagerdialog.ui
cheatmanagerwindow.cpp
cheatmanagerwindow.h
cheatmanagerwindow.ui
collapsiblewidget.cpp
collapsiblewidget.h
colorpickerbutton.cpp
@ -121,6 +121,9 @@ set(SRCS
memorycardeditorwindow.ui
memorycardsettingswidget.cpp
memorycardsettingswidget.h
memoryscannerwindow.cpp
memoryscannerwindow.h
memoryscannerwindow.ui
memoryviewwidget.cpp
memoryviewwidget.h
postprocessingsettingswidget.cpp

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,578 @@
// SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin <stenzek@gmail.com> and contributors.
// SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0)
#include "cheatmanagerwindow.h"
#include "cheatcodeeditordialog.h"
#include "mainwindow.h"
#include "qthost.h"
#include "qtutils.h"
#include "core/bus.h"
#include "core/cpu_core.h"
#include "core/host.h"
#include "core/system.h"
#include "common/assert.h"
#include "common/string_util.h"
#include <QtCore/QFileInfo>
#include <QtGui/QColor>
#include <QtWidgets/QFileDialog>
#include <QtWidgets/QInputDialog>
#include <QtWidgets/QMenu>
#include <QtWidgets/QMessageBox>
#include <QtWidgets/QTreeWidgetItemIterator>
#include <array>
#include <utility>
CheatManagerWindow::CheatManagerWindow() : QWidget()
{
m_ui.setupUi(this);
connectUi();
updateCheatList();
}
CheatManagerWindow::~CheatManagerWindow() = default;
void CheatManagerWindow::connectUi()
{
connect(m_ui.cheatList, &QTreeWidget::currentItemChanged, this, &CheatManagerWindow::cheatListCurrentItemChanged);
connect(m_ui.cheatList, &QTreeWidget::itemActivated, this, &CheatManagerWindow::cheatListItemActivated);
connect(m_ui.cheatList, &QTreeWidget::itemChanged, this, &CheatManagerWindow::cheatListItemChanged);
connect(m_ui.cheatListNewCategory, &QPushButton::clicked, this, &CheatManagerWindow::newCategoryClicked);
connect(m_ui.cheatListAdd, &QPushButton::clicked, this, &CheatManagerWindow::addCodeClicked);
connect(m_ui.cheatListEdit, &QPushButton::clicked, this, &CheatManagerWindow::editCodeClicked);
connect(m_ui.cheatListRemove, &QPushButton::clicked, this, &CheatManagerWindow::deleteCodeClicked);
connect(m_ui.cheatListActivate, &QPushButton::clicked, this, &CheatManagerWindow::activateCodeClicked);
connect(m_ui.cheatListImport, &QPushButton::clicked, this, &CheatManagerWindow::importClicked);
connect(m_ui.cheatListExport, &QPushButton::clicked, this, &CheatManagerWindow::exportClicked);
connect(m_ui.cheatListClear, &QPushButton::clicked, this, &CheatManagerWindow::clearClicked);
connect(m_ui.cheatListReset, &QPushButton::clicked, this, &CheatManagerWindow::resetClicked);
connect(g_emu_thread, &EmuThread::cheatEnabled, this, &CheatManagerWindow::setCheatCheckState);
connect(g_emu_thread, &EmuThread::runningGameChanged, this, &CheatManagerWindow::updateCheatList);
}
void CheatManagerWindow::showEvent(QShowEvent* event)
{
QWidget::showEvent(event);
resizeColumns();
}
void CheatManagerWindow::closeEvent(QCloseEvent* event)
{
QWidget::closeEvent(event);
emit closed();
}
void CheatManagerWindow::resizeEvent(QResizeEvent* event)
{
QWidget::resizeEvent(event);
resizeColumns();
}
void CheatManagerWindow::resizeColumns()
{
QtUtils::ResizeColumnsForTreeView(m_ui.cheatList, {-1, 100, 150, 100});
}
QTreeWidgetItem* CheatManagerWindow::getItemForCheatIndex(u32 index) const
{
QTreeWidgetItemIterator iter(m_ui.cheatList);
while (*iter)
{
QTreeWidgetItem* item = *iter;
const QVariant item_data(item->data(0, Qt::UserRole));
if (item_data.isValid() && item_data.toUInt() == index)
return item;
++iter;
}
return nullptr;
}
QTreeWidgetItem* CheatManagerWindow::getItemForCheatGroup(const QString& group_name) const
{
const int count = m_ui.cheatList->topLevelItemCount();
for (int i = 0; i < count; i++)
{
QTreeWidgetItem* item = m_ui.cheatList->topLevelItem(i);
if (item->text(0) == group_name)
return item;
}
return nullptr;
}
QTreeWidgetItem* CheatManagerWindow::createItemForCheatGroup(const QString& group_name) const
{
QTreeWidgetItem* group = new QTreeWidgetItem();
group->setFlags(group->flags() | Qt::ItemIsUserCheckable);
group->setText(0, group_name);
m_ui.cheatList->addTopLevelItem(group);
return group;
}
QStringList CheatManagerWindow::getCheatGroupNames() const
{
QStringList group_names;
const int count = m_ui.cheatList->topLevelItemCount();
for (int i = 0; i < count; i++)
{
QTreeWidgetItem* item = m_ui.cheatList->topLevelItem(i);
group_names.push_back(item->text(0));
}
return group_names;
}
static int getCheatIndexFromItem(QTreeWidgetItem* item)
{
QVariant item_data(item->data(0, Qt::UserRole));
if (!item_data.isValid())
return -1;
return static_cast<int>(item_data.toUInt());
}
int CheatManagerWindow::getSelectedCheatIndex() const
{
QList<QTreeWidgetItem*> sel = m_ui.cheatList->selectedItems();
if (sel.isEmpty())
return -1;
return static_cast<int>(getCheatIndexFromItem(sel.first()));
}
CheatList* CheatManagerWindow::getCheatList() const
{
return System::IsValid() ? System::GetCheatList() : nullptr;
}
void CheatManagerWindow::updateCheatList()
{
QSignalBlocker sb(m_ui.cheatList);
while (m_ui.cheatList->topLevelItemCount() > 0)
delete m_ui.cheatList->takeTopLevelItem(0);
m_ui.cheatList->setEnabled(false);
m_ui.cheatListAdd->setEnabled(false);
m_ui.cheatListNewCategory->setEnabled(false);
m_ui.cheatListEdit->setEnabled(false);
m_ui.cheatListRemove->setEnabled(false);
m_ui.cheatListActivate->setText(tr("Activate"));
m_ui.cheatListActivate->setEnabled(false);
m_ui.cheatListImport->setEnabled(false);
m_ui.cheatListExport->setEnabled(false);
m_ui.cheatListClear->setEnabled(false);
m_ui.cheatListReset->setEnabled(false);
Host::RunOnCPUThread([]() {
if (!System::IsValid())
return;
CheatList* list = System::GetCheatList();
if (!list)
{
System::LoadCheatList();
list = System::GetCheatList();
}
if (!list)
{
System::LoadCheatListFromDatabase();
list = System::GetCheatList();
}
if (!list)
{
System::SetCheatList(std::make_unique<CheatList>());
list = System::GetCheatList();
}
// still racey...
QtHost::RunOnUIThread([list]() {
if (!QtHost::IsSystemValid())
return;
CheatManagerWindow* cm = g_main_window->getCheatManagerWindow();
if (!cm)
return;
QSignalBlocker sb(cm->m_ui.cheatList);
const std::vector<std::string> groups = list->GetCodeGroups();
for (const std::string& group_name : groups)
{
QTreeWidgetItem* group = cm->createItemForCheatGroup(QString::fromStdString(group_name));
const u32 count = list->GetCodeCount();
bool all_enabled = true;
for (u32 i = 0; i < count; i++)
{
const CheatCode& code = list->GetCode(i);
if (code.group != group_name)
continue;
QTreeWidgetItem* item = new QTreeWidgetItem(group);
cm->fillItemForCheatCode(item, i, code);
all_enabled &= code.enabled;
}
group->setCheckState(0, all_enabled ? Qt::Checked : Qt::Unchecked);
group->setExpanded(true);
}
cm->m_ui.cheatList->setEnabled(true);
cm->m_ui.cheatListAdd->setEnabled(true);
cm->m_ui.cheatListNewCategory->setEnabled(true);
cm->m_ui.cheatListImport->setEnabled(true);
cm->m_ui.cheatListClear->setEnabled(true);
cm->m_ui.cheatListReset->setEnabled(true);
cm->m_ui.cheatListExport->setEnabled(cm->m_ui.cheatList->topLevelItemCount() > 0);
});
});
}
void CheatManagerWindow::fillItemForCheatCode(QTreeWidgetItem* item, u32 index, const CheatCode& code)
{
item->setData(0, Qt::UserRole, QVariant(static_cast<uint>(index)));
if (code.IsManuallyActivated())
{
item->setFlags(item->flags() & ~(Qt::ItemIsUserCheckable));
}
else
{
item->setFlags(item->flags() | Qt::ItemIsUserCheckable);
item->setCheckState(0, code.enabled ? Qt::Checked : Qt::Unchecked);
}
item->setText(0, QString::fromStdString(code.description));
item->setText(1, qApp->translate("Cheats", CheatCode::GetTypeDisplayName(code.type)));
item->setText(2, qApp->translate("Cheats", CheatCode::GetActivationDisplayName(code.activation)));
item->setText(3, QString::number(static_cast<uint>(code.instructions.size())));
}
void CheatManagerWindow::saveCheatList()
{
Host::RunOnCPUThread([]() { System::SaveCheatList(); });
}
void CheatManagerWindow::cheatListCurrentItemChanged(QTreeWidgetItem* current, QTreeWidgetItem* previous)
{
const int cheat_index = current ? getCheatIndexFromItem(current) : -1;
const bool has_current = (cheat_index >= 0);
m_ui.cheatListEdit->setEnabled(has_current);
m_ui.cheatListRemove->setEnabled(has_current);
m_ui.cheatListActivate->setEnabled(has_current);
if (!has_current)
{
m_ui.cheatListActivate->setText(tr("Activate"));
}
else
{
const bool manual_activation = getCheatList()->GetCode(static_cast<u32>(cheat_index)).IsManuallyActivated();
m_ui.cheatListActivate->setText(manual_activation ? tr("Activate") : tr("Toggle"));
}
}
void CheatManagerWindow::cheatListItemActivated(QTreeWidgetItem* item)
{
if (!item)
return;
const int index = getCheatIndexFromItem(item);
if (index >= 0)
activateCheat(static_cast<u32>(index));
}
void CheatManagerWindow::cheatListItemChanged(QTreeWidgetItem* item, int column)
{
if (!item || column != 0)
return;
CheatList* list = getCheatList();
const int index = getCheatIndexFromItem(item);
if (index < 0)
{
// we're probably a parent/group node
const int child_count = item->childCount();
const Qt::CheckState cs = item->checkState(0);
for (int i = 0; i < child_count; i++)
item->child(i)->setCheckState(0, cs);
return;
}
if (static_cast<u32>(index) >= list->GetCodeCount())
return;
CheatCode& cc = list->GetCode(static_cast<u32>(index));
if (cc.IsManuallyActivated())
return;
const bool new_enabled = (item->checkState(0) == Qt::Checked);
if (cc.enabled == new_enabled)
return;
Host::RunOnCPUThread([index, new_enabled]() {
System::GetCheatList()->SetCodeEnabled(static_cast<u32>(index), new_enabled);
System::SaveCheatList();
});
}
void CheatManagerWindow::activateCheat(u32 index)
{
CheatList* list = getCheatList();
if (index >= list->GetCodeCount())
return;
CheatCode& cc = list->GetCode(index);
if (cc.IsManuallyActivated())
{
g_emu_thread->applyCheat(index);
return;
}
const bool new_enabled = !cc.enabled;
setCheatCheckState(index, new_enabled);
Host::RunOnCPUThread([index, new_enabled]() {
System::GetCheatList()->SetCodeEnabled(index, new_enabled);
System::SaveCheatList();
});
}
void CheatManagerWindow::setCheatCheckState(u32 index, bool checked)
{
QTreeWidgetItem* item = getItemForCheatIndex(index);
if (item)
{
QSignalBlocker sb(m_ui.cheatList);
item->setCheckState(0, checked ? Qt::Checked : Qt::Unchecked);
}
}
void CheatManagerWindow::newCategoryClicked()
{
QString group_name = QInputDialog::getText(this, tr("Add Group"), tr("Group Name:"));
if (group_name.isEmpty())
return;
if (getItemForCheatGroup(group_name) != nullptr)
{
QMessageBox::critical(this, tr("Error"), tr("This group name already exists."));
return;
}
createItemForCheatGroup(group_name);
}
void CheatManagerWindow::addCodeClicked()
{
CheatList* list = getCheatList();
CheatCode new_code;
new_code.group = "Ungrouped";
CheatCodeEditorDialog editor(getCheatGroupNames(), &new_code, this);
if (editor.exec() > 0)
{
const QString group_name_qstr(QString::fromStdString(new_code.group));
QTreeWidgetItem* group_item = getItemForCheatGroup(group_name_qstr);
if (!group_item)
group_item = createItemForCheatGroup(group_name_qstr);
QTreeWidgetItem* item = new QTreeWidgetItem(group_item);
fillItemForCheatCode(item, list->GetCodeCount(), new_code);
group_item->setExpanded(true);
Host::RunOnCPUThread(
[&new_code]() {
System::GetCheatList()->AddCode(std::move(new_code));
System::SaveCheatList();
},
true);
}
}
void CheatManagerWindow::editCodeClicked()
{
int index = getSelectedCheatIndex();
if (index < 0)
return;
CheatList* list = getCheatList();
if (static_cast<u32>(index) >= list->GetCodeCount())
return;
CheatCode new_code = list->GetCode(static_cast<u32>(index));
CheatCodeEditorDialog editor(getCheatGroupNames(), &new_code, this);
if (editor.exec() > 0)
{
QTreeWidgetItem* item = getItemForCheatIndex(static_cast<u32>(index));
if (item)
{
if (new_code.group != list->GetCode(static_cast<u32>(index)).group)
{
item = item->parent()->takeChild(item->parent()->indexOfChild(item));
const QString group_name_qstr(QString::fromStdString(new_code.group));
QTreeWidgetItem* group_item = getItemForCheatGroup(group_name_qstr);
if (!group_item)
group_item = createItemForCheatGroup(group_name_qstr);
group_item->addChild(item);
group_item->setExpanded(true);
}
fillItemForCheatCode(item, static_cast<u32>(index), new_code);
}
else
{
// shouldn't happen...
updateCheatList();
}
Host::RunOnCPUThread(
[index, &new_code]() {
System::GetCheatList()->SetCode(static_cast<u32>(index), std::move(new_code));
System::SaveCheatList();
},
true);
}
}
void CheatManagerWindow::deleteCodeClicked()
{
int index = getSelectedCheatIndex();
if (index < 0)
return;
CheatList* list = getCheatList();
if (static_cast<u32>(index) >= list->GetCodeCount())
return;
if (QMessageBox::question(this, tr("Delete Code"),
tr("Are you sure you wish to delete the selected code? This action is not reversible."),
QMessageBox::Yes, QMessageBox::No) != QMessageBox::Yes)
{
return;
}
Host::RunOnCPUThread(
[index]() {
System::GetCheatList()->RemoveCode(static_cast<u32>(index));
System::SaveCheatList();
},
true);
updateCheatList();
}
void CheatManagerWindow::activateCodeClicked()
{
int index = getSelectedCheatIndex();
if (index < 0)
return;
activateCheat(static_cast<u32>(index));
}
void CheatManagerWindow::importClicked()
{
QMenu menu(this);
connect(menu.addAction(tr("From File...")), &QAction::triggered, this, &CheatManagerWindow::importFromFileTriggered);
connect(menu.addAction(tr("From Text...")), &QAction::triggered, this, &CheatManagerWindow::importFromTextTriggered);
menu.exec(QCursor::pos());
}
void CheatManagerWindow::importFromFileTriggered()
{
const QString filter(tr("PCSXR/Libretro Cheat Files (*.cht *.txt);;All Files (*.*)"));
const QString filename =
QDir::toNativeSeparators(QFileDialog::getOpenFileName(this, tr("Import Cheats"), QString(), filter));
if (filename.isEmpty())
return;
CheatList new_cheats;
if (!new_cheats.LoadFromFile(filename.toUtf8().constData(), CheatList::Format::Autodetect))
{
QMessageBox::critical(this, tr("Error"), tr("Failed to parse cheat file. The log may contain more information."));
return;
}
Host::RunOnCPUThread(
[&new_cheats]() {
DebugAssert(System::HasCheatList());
System::GetCheatList()->MergeList(new_cheats);
System::SaveCheatList();
},
true);
updateCheatList();
}
void CheatManagerWindow::importFromTextTriggered()
{
const QString text = QInputDialog::getMultiLineText(this, tr("Import Cheats"), tr("Cheat File Text:"));
if (text.isEmpty())
return;
CheatList new_cheats;
if (!new_cheats.LoadFromString(text.toStdString(), CheatList::Format::Autodetect))
{
QMessageBox::critical(this, tr("Error"), tr("Failed to parse cheat file. The log may contain more information."));
return;
}
Host::RunOnCPUThread(
[&new_cheats]() {
DebugAssert(System::HasCheatList());
System::GetCheatList()->MergeList(new_cheats);
System::SaveCheatList();
},
true);
updateCheatList();
}
void CheatManagerWindow::exportClicked()
{
const QString filter(tr("PCSXR Cheat Files (*.cht);;All Files (*.*)"));
const QString filename =
QDir::toNativeSeparators(QFileDialog::getSaveFileName(this, tr("Export Cheats"), QString(), filter));
if (filename.isEmpty())
return;
if (!getCheatList()->SaveToPCSXRFile(filename.toUtf8().constData()))
QMessageBox::critical(this, tr("Error"), tr("Failed to save cheat file. The log may contain more information."));
}
void CheatManagerWindow::clearClicked()
{
if (QMessageBox::question(this, tr("Confirm Clear"),
tr("Are you sure you want to remove all cheats? This is not reversible.")) !=
QMessageBox::Yes)
{
return;
}
Host::RunOnCPUThread([] { System::ClearCheatList(true); }, true);
updateCheatList();
}
void CheatManagerWindow::resetClicked()
{
if (QMessageBox::question(
this, tr("Confirm Reset"),
tr(
"Are you sure you want to reset the cheat list? Any cheats not in the DuckStation database WILL BE LOST.")) !=
QMessageBox::Yes)
{
return;
}
Host::RunOnCPUThread([] { System::DeleteCheatList(); }, true);
updateCheatList();
}

View file

@ -1,32 +1,38 @@
// SPDX-FileCopyrightText: 2019-2022 Connor McLaughlin <stenzek@gmail.com>
// SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin <stenzek@gmail.com>
// SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0)
#pragma once
#include "ui_cheatmanagerwindow.h"
#include "core/cheats.h"
#include "ui_cheatmanagerdialog.h"
#include <QtCore/QTimer>
#include <QtWidgets/QComboBox>
#include <QtWidgets/QDialog>
#include <QtWidgets/QLabel>
#include <QtWidgets/QPushButton>
#include <QtWidgets/QTableWidget>
#include <QtWidgets/QWidget>
#include <optional>
class CheatManagerDialog : public QDialog
class CheatManagerWindow : public QWidget
{
Q_OBJECT
public:
CheatManagerDialog(QWidget* parent);
~CheatManagerDialog();
CheatManagerWindow();
~CheatManagerWindow();
Q_SIGNALS:
void closed();
protected:
void showEvent(QShowEvent* event);
void closeEvent(QCloseEvent* event);
void resizeEvent(QResizeEvent* event);
private Q_SLOTS:
void resizeColumns();
private Q_SLOTS:
CheatList* getCheatList() const;
void updateCheatList();
void saveCheatList();
@ -47,29 +53,13 @@ private Q_SLOTS:
void clearClicked();
void resetClicked();
void addToWatchClicked();
void addManualWatchAddressClicked();
void removeWatchClicked();
void scanCurrentItemChanged(QTableWidgetItem* current, QTableWidgetItem* previous);
void watchCurrentItemChanged(QTableWidgetItem* current, QTableWidgetItem* previous);
void scanItemChanged(QTableWidgetItem* item);
void watchItemChanged(QTableWidgetItem* item);
void updateScanValue();
void updateScanUi();
private:
enum : int
{
MAX_DISPLAYED_SCAN_RESULTS = 5000
};
void setupAdditionalUi();
void connectUi();
void setUpdateTimerEnabled(bool enabled);
void updateResults();
void updateResultsValues();
void updateWatch();
void updateWatchValues();
void fillItemForCheatCode(QTreeWidgetItem* item, u32 index, const CheatCode& code);
QTreeWidgetItem* getItemForCheatIndex(u32 index) const;
@ -77,15 +67,8 @@ private:
QTreeWidgetItem* createItemForCheatGroup(const QString& group_name) const;
QStringList getCheatGroupNames() const;
int getSelectedCheatIndex() const;
int getSelectedResultIndexFirst() const;
int getSelectedResultIndexLast() const;
int getSelectedWatchIndexFirst() const;
int getSelectedWatchIndexLast() const;
Ui::CheatManagerDialog m_ui;
MemoryScan m_scanner;
MemoryWatchList m_watch;
Ui::CheatManagerWindow m_ui;
QTimer* m_update_timer = nullptr;
};

View file

@ -1,18 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>CheatManagerDialog</class>
<widget class="QDialog" name="CheatManagerDialog">
<class>CheatManagerWindow</class>
<widget class="QWidget" name="CheatManagerWindow">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>1046</width>
<height>778</height>
<width>882</width>
<height>572</height>
</rect>
</property>
<property name="windowTitle">
<string>Cheat Manager</string>
</property>
<property name="windowIcon">
<iconset>
<normaloff>:/icons/duck.png</normaloff>:/icons/duck.png</iconset>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<widget class="QTabWidget" name="tabWidget">
@ -101,7 +105,7 @@
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
@ -116,10 +120,10 @@
<item row="0" column="0">
<widget class="QTreeWidget" name="cheatList">
<property name="selectionMode">
<enum>QAbstractItemView::SingleSelection</enum>
<enum>QAbstractItemView::SelectionMode::SingleSelection</enum>
</property>
<property name="selectionBehavior">
<enum>QAbstractItemView::SelectRows</enum>
<enum>QAbstractItemView::SelectionBehavior::SelectRows</enum>
</property>
<column>
<property name="text">
@ -153,7 +157,7 @@
<item row="0" column="0">
<widget class="QSplitter" name="splitter">
<property name="orientation">
<enum>Qt::Vertical</enum>
<enum>Qt::Orientation::Vertical</enum>
</property>
<widget class="QWidget" name="layoutWidget2">
<layout class="QHBoxLayout" name="horizontalLayout_5">
@ -163,10 +167,10 @@
<bool>true</bool>
</property>
<property name="selectionMode">
<enum>QAbstractItemView::ContiguousSelection</enum>
<enum>QAbstractItemView::SelectionMode::ContiguousSelection</enum>
</property>
<property name="selectionBehavior">
<enum>QAbstractItemView::SelectRows</enum>
<enum>QAbstractItemView::SelectionBehavior::SelectRows</enum>
</property>
<property name="showGrid">
<bool>false</bool>
@ -466,85 +470,85 @@
<item>
<layout class="QHBoxLayout" name="horizontalLayout_6">
<item>
<widget class="QTextEdit" name="scanResults">
<property name="enabled">
<bool>false</bool>
</property>
<property name="maximumSize">
<size>
<width>300</width>
<height>30</height>
</size>
</property>
<property name="inputMethodHints">
<set>Qt::ImhNone</set>
</property>
<property name="frameShape">
<enum>QFrame::NoFrame</enum>
</property>
<property name="verticalScrollBarPolicy">
<enum>Qt::ScrollBarAlwaysOff</enum>
</property>
<property name="horizontalScrollBarPolicy">
<enum>Qt::ScrollBarAlwaysOff</enum>
</property>
<property name="readOnly">
<bool>true</bool>
</property>
<property name="textInteractionFlags">
<set>Qt::NoTextInteraction</set>
</property>
<property name="text">
<string>Number of Results (Display limited to first 5000) : </string>
</property>
</widget>
</item>
<item>
<widget class="QTextEdit" name="scanResultsCount">
<property name="enabled">
<bool>false</bool>
</property>
<property name="maximumSize">
<size>
<width>60</width>
<height>30</height>
</size>
</property>
<property name="inputMethodHints">
<set>Qt::ImhNone</set>
</property>
<property name="frameShape">
<enum>QFrame::NoFrame</enum>
</property>
<property name="verticalScrollBarPolicy">
<enum>Qt::ScrollBarAlwaysOff</enum>
</property>
<property name="horizontalScrollBarPolicy">
<enum>Qt::ScrollBarAlwaysOff</enum>
</property>
<property name="readOnly">
<bool>true</bool>
</property>
<property name="acceptRichText">
<bool>false</bool>
</property>
<property name="textInteractionFlags">
<set>Qt::NoTextInteraction</set>
</property>
<property name="placeholderText">
<string notr="true"/>
</property>
<property name="text">
<string>0</string>
</property>
</widget>
</item>
</layout>
<widget class="QTextEdit" name="scanResults">
<property name="enabled">
<bool>false</bool>
</property>
<property name="maximumSize">
<size>
<width>300</width>
<height>30</height>
</size>
</property>
<property name="inputMethodHints">
<set>Qt::InputMethodHint::ImhNone</set>
</property>
<property name="frameShape">
<enum>QFrame::Shape::NoFrame</enum>
</property>
<property name="verticalScrollBarPolicy">
<enum>Qt::ScrollBarPolicy::ScrollBarAlwaysOff</enum>
</property>
<property name="horizontalScrollBarPolicy">
<enum>Qt::ScrollBarPolicy::ScrollBarAlwaysOff</enum>
</property>
<property name="readOnly">
<bool>true</bool>
</property>
<property name="textInteractionFlags">
<set>Qt::TextInteractionFlag::NoTextInteraction</set>
</property>
<property name="text" stdset="0">
<string>Number of Results (Display limited to first 5000) : </string>
</property>
</widget>
</item>
<item>
<widget class="QTextEdit" name="scanResultsCount">
<property name="enabled">
<bool>false</bool>
</property>
<property name="maximumSize">
<size>
<width>60</width>
<height>30</height>
</size>
</property>
<property name="inputMethodHints">
<set>Qt::InputMethodHint::ImhNone</set>
</property>
<property name="frameShape">
<enum>QFrame::Shape::NoFrame</enum>
</property>
<property name="verticalScrollBarPolicy">
<enum>Qt::ScrollBarPolicy::ScrollBarAlwaysOff</enum>
</property>
<property name="horizontalScrollBarPolicy">
<enum>Qt::ScrollBarPolicy::ScrollBarAlwaysOff</enum>
</property>
<property name="readOnly">
<bool>true</bool>
</property>
<property name="acceptRichText">
<bool>false</bool>
</property>
<property name="textInteractionFlags">
<set>Qt::TextInteractionFlag::NoTextInteraction</set>
</property>
<property name="placeholderText">
<string notr="true"/>
</property>
<property name="text" stdset="0">
<string>0</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
<enum>Qt::Orientation::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
@ -566,10 +570,10 @@
<bool>true</bool>
</property>
<property name="selectionMode">
<enum>QAbstractItemView::ContiguousSelection</enum>
<enum>QAbstractItemView::SelectionMode::ContiguousSelection</enum>
</property>
<property name="selectionBehavior">
<enum>QAbstractItemView::SelectRows</enum>
<enum>QAbstractItemView::SelectionBehavior::SelectRows</enum>
</property>
<property name="showGrid">
<bool>false</bool>
@ -658,7 +662,7 @@
<item>
<spacer name="horizontalSpacer_2">
<property name="orientation">
<enum>Qt::Horizontal</enum>
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>

View file

@ -9,7 +9,7 @@
<ClCompile Include="audiosettingswidget.cpp" />
<ClCompile Include="autoupdaterdialog.cpp" />
<ClCompile Include="biossettingswidget.cpp" />
<ClCompile Include="cheatmanagerdialog.cpp" />
<ClCompile Include="cheatmanagerwindow.cpp" />
<ClCompile Include="cheatcodeeditordialog.cpp" />
<ClCompile Include="collapsiblewidget.cpp" />
<ClCompile Include="colorpickerbutton.cpp" />
@ -30,6 +30,7 @@
<ClCompile Include="inputbindingdialog.cpp" />
<ClCompile Include="inputbindingwidgets.cpp" />
<ClCompile Include="logwindow.cpp" />
<ClCompile Include="memoryscannerwindow.cpp" />
<ClCompile Include="memoryviewwidget.cpp" />
<ClCompile Include="displaywidget.cpp" />
<ClCompile Include="gamelistsettingswidget.cpp" />
@ -58,7 +59,7 @@
<QtMoc Include="aboutdialog.h" />
<QtMoc Include="audiosettingswidget.h" />
<QtMoc Include="biossettingswidget.h" />
<QtMoc Include="cheatmanagerdialog.h" />
<QtMoc Include="cheatmanagerwindow.h" />
<QtMoc Include="cheatcodeeditordialog.h" />
<QtMoc Include="coverdownloaddialog.h" />
<QtMoc Include="memorycardsettingswidget.h" />
@ -86,6 +87,7 @@
<QtMoc Include="memoryviewwidget.h" />
<QtMoc Include="logwindow.h" />
<QtMoc Include="graphicssettingswidget.h" />
<QtMoc Include="memoryscannerwindow.h" />
<ClInclude Include="pch.h" />
<ClInclude Include="resource.h" />
<ClInclude Include="settingwidgetbinder.h" />
@ -141,7 +143,7 @@
<QtUi Include="memorycardeditorwindow.ui">
<FileType>Document</FileType>
</QtUi>
<QtUi Include="cheatmanagerdialog.ui">
<QtUi Include="cheatmanagerwindow.ui">
<FileType>Document</FileType>
</QtUi>
<QtUi Include="cheatcodeeditordialog.ui">
@ -224,7 +226,7 @@
<ClCompile Include="$(IntDir)moc_autoupdaterdialog.cpp" />
<ClCompile Include="$(IntDir)moc_advancedsettingswidget.cpp" />
<ClCompile Include="$(IntDir)moc_biossettingswidget.cpp" />
<ClCompile Include="$(IntDir)moc_cheatmanagerdialog.cpp" />
<ClCompile Include="$(IntDir)moc_cheatmanagerwindow.cpp" />
<ClCompile Include="$(IntDir)moc_cheatcodeeditordialog.cpp" />
<ClCompile Include="$(IntDir)moc_collapsiblewidget.cpp" />
<ClCompile Include="$(IntDir)moc_colorpickerbutton.cpp" />
@ -255,6 +257,7 @@
<ClCompile Include="$(IntDir)moc_mainwindow.cpp" />
<ClCompile Include="$(IntDir)moc_memorycardsettingswidget.cpp" />
<ClCompile Include="$(IntDir)moc_memorycardeditorwindow.cpp" />
<ClCompile Include="$(IntDir)moc_memoryscannerwindow.cpp" />
<ClCompile Include="$(IntDir)moc_memoryviewwidget.cpp" />
<ClCompile Include="$(IntDir)moc_postprocessingsettingswidget.cpp" />
<ClCompile Include="$(IntDir)moc_qthost.cpp" />
@ -329,6 +332,9 @@
<QtUi Include="graphicssettingswidget.ui">
<FileType>Document</FileType>
</QtUi>
<QtUi Include="memoryscannerwindow.ui">
<FileType>Document</FileType>
</QtUi>
<None Include="translations\duckstation-qt_es-es.ts" />
<None Include="translations\duckstation-qt_tr.ts" />
</ItemGroup>

View file

@ -27,7 +27,7 @@
<ClCompile Include="biossettingswidget.cpp" />
<ClCompile Include="memorycardeditorwindow.cpp" />
<ClCompile Include="postprocessingsettingswidget.cpp" />
<ClCompile Include="cheatmanagerdialog.cpp" />
<ClCompile Include="cheatmanagerwindow.cpp" />
<ClCompile Include="cheatcodeeditordialog.cpp" />
<ClCompile Include="debuggerwindow.cpp" />
<ClCompile Include="debuggermodels.cpp" />
@ -73,7 +73,7 @@
<ClCompile Include="$(IntDir)moc_cheatcodeeditordialog.cpp">
<Filter>moc</Filter>
</ClCompile>
<ClCompile Include="$(IntDir)moc_cheatmanagerdialog.cpp">
<ClCompile Include="$(IntDir)moc_cheatmanagerwindow.cpp">
<Filter>moc</Filter>
</ClCompile>
<ClCompile Include="$(IntDir)moc_collapsiblewidget.cpp">
@ -182,6 +182,10 @@
<ClCompile Include="$(IntDir)moc_interfacesettingswidget.cpp">
<Filter>moc</Filter>
</ClCompile>
<ClCompile Include="memoryscannerwindow.cpp" />
<ClCompile Include="$(IntDir)moc_memoryscannerwindow.cpp">
<Filter>moc</Filter>
</ClCompile>
</ItemGroup>
<ItemGroup>
<ClInclude Include="qtutils.h" />
@ -226,7 +230,7 @@
<QtMoc Include="biossettingswidget.h" />
<QtMoc Include="memorycardeditorwindow.h" />
<QtMoc Include="postprocessingsettingswidget.h" />
<QtMoc Include="cheatmanagerdialog.h" />
<QtMoc Include="cheatmanagerwindow.h" />
<QtMoc Include="cheatcodeeditordialog.h" />
<QtMoc Include="debuggermodels.h" />
<QtMoc Include="debuggerwindow.h" />
@ -246,6 +250,7 @@
<QtMoc Include="setupwizarddialog.h" />
<QtMoc Include="logwindow.h" />
<QtMoc Include="graphicssettingswidget.h" />
<QtMoc Include="memoryscannerwindow.h" />
</ItemGroup>
<ItemGroup>
<QtUi Include="consolesettingswidget.ui" />
@ -261,7 +266,7 @@
<QtUi Include="biossettingswidget.ui" />
<QtUi Include="postprocessingsettingswidget.ui" />
<QtUi Include="memorycardeditorwindow.ui" />
<QtUi Include="cheatmanagerdialog.ui" />
<QtUi Include="cheatmanagerwindow.ui" />
<QtUi Include="cheatcodeeditordialog.ui" />
<QtUi Include="emulationsettingswidget.ui" />
<QtUi Include="achievementsettingswidget.ui" />
@ -287,6 +292,7 @@
<QtUi Include="controllerledsettingsdialog.ui" />
<QtUi Include="debuggeraddbreakpointdialog.ui" />
<QtUi Include="graphicssettingswidget.ui" />
<QtUi Include="memoryscannerwindow.ui" />
</ItemGroup>
<ItemGroup>
<Natvis Include="qt5.natvis" />

View file

@ -5,7 +5,7 @@
#include "aboutdialog.h"
#include "achievementlogindialog.h"
#include "autoupdaterdialog.h"
#include "cheatmanagerdialog.h"
#include "cheatmanagerwindow.h"
#include "coverdownloaddialog.h"
#include "debuggerwindow.h"
#include "displaywidget.h"
@ -14,6 +14,7 @@
#include "interfacesettingswidget.h"
#include "logwindow.h"
#include "memorycardeditorwindow.h"
#include "memoryscannerwindow.h"
#include "qthost.h"
#include "qtutils.h"
#include "settingswindow.h"
@ -601,12 +602,6 @@ void MainWindow::onSystemDestroyed()
// reload played time
if (m_game_list_widget->isShowingGameList())
m_game_list_widget->refresh(false);
if (m_cheat_manager_dialog)
{
delete m_cheat_manager_dialog;
m_cheat_manager_dialog = nullptr;
}
}
void MainWindow::onRunningGameChanged(const QString& filename, const QString& game_serial, const QString& game_title)
@ -752,33 +747,12 @@ void MainWindow::recreate()
void MainWindow::destroySubWindows()
{
if (m_debugger_window)
{
m_debugger_window->close();
m_debugger_window->deleteLater();
m_debugger_window = nullptr;
}
if (m_memory_card_editor_window)
{
m_memory_card_editor_window->close();
m_memory_card_editor_window->deleteLater();
m_memory_card_editor_window = nullptr;
}
if (m_controller_settings_window)
{
m_controller_settings_window->close();
m_controller_settings_window->deleteLater();
m_controller_settings_window = nullptr;
}
if (m_settings_window)
{
m_settings_window->close();
m_settings_window->deleteLater();
m_settings_window = nullptr;
}
QtUtils::CloseAndDeleteWindow(m_memory_scanner_window);
QtUtils::CloseAndDeleteWindow(m_debugger_window);
QtUtils::CloseAndDeleteWindow(m_cheat_manager_window);
QtUtils::CloseAndDeleteWindow(m_memory_card_editor_window);
QtUtils::CloseAndDeleteWindow(m_controller_settings_window);
QtUtils::CloseAndDeleteWindow(m_settings_window);
SettingsWindow::closeGamePropertiesDialogs();
@ -1805,6 +1779,7 @@ void MainWindow::updateEmulationActions(bool starting, bool running, bool cheevo
m_ui.menuChangeDisc->setDisabled(starting || !running);
m_ui.menuCheats->setDisabled(starting || !running || cheevos_challenge_mode);
m_ui.actionCPUDebugger->setDisabled(cheevos_challenge_mode);
m_ui.actionMemoryScanner->setDisabled(cheevos_challenge_mode);
m_ui.actionDumpRAM->setDisabled(starting || !running || cheevos_challenge_mode);
m_ui.actionDumpVRAM->setDisabled(starting || !running || cheevos_challenge_mode);
m_ui.actionDumpSPURAM->setDisabled(starting || !running || cheevos_challenge_mode);
@ -2094,7 +2069,8 @@ void MainWindow::connectSignals()
connect(m_ui.actionAboutQt, &QAction::triggered, qApp, &QApplication::aboutQt);
connect(m_ui.actionAbout, &QAction::triggered, this, &MainWindow::onAboutActionTriggered);
connect(m_ui.actionCheckForUpdates, &QAction::triggered, this, &MainWindow::onCheckForUpdatesActionTriggered);
connect(m_ui.actionMemory_Card_Editor, &QAction::triggered, this, &MainWindow::onToolsMemoryCardEditorTriggered);
connect(m_ui.actionMemoryCardEditor, &QAction::triggered, this, &MainWindow::onToolsMemoryCardEditorTriggered);
connect(m_ui.actionMemoryScanner, &QAction::triggered, this, &MainWindow::onToolsMemoryScannerTriggered);
connect(m_ui.actionCoverDownloader, &QAction::triggered, this, &MainWindow::onToolsCoverDownloaderTriggered);
connect(m_ui.actionCPUDebugger, &QAction::triggered, this, &MainWindow::openCPUDebugger);
SettingWidgetBinder::BindWidgetToBoolSetting(nullptr, m_ui.actionEnableGDBServer, "Debug", "EnableGDBServer", false);
@ -2571,17 +2547,7 @@ SettingsWindow* MainWindow::getSettingsDialog()
void MainWindow::doSettings(const char* category /* = nullptr */)
{
SettingsWindow* dlg = getSettingsDialog();
if (!dlg->isVisible())
{
dlg->show();
}
else
{
dlg->raise();
dlg->activateWindow();
dlg->setFocus();
}
QtUtils::ShowOrRaiseWindow(dlg);
if (category)
dlg->setCategory(category);
}
@ -2592,17 +2558,7 @@ void MainWindow::doControllerSettings(
if (!m_controller_settings_window)
m_controller_settings_window = new ControllerSettingsWindow();
if (!m_controller_settings_window->isVisible())
{
m_controller_settings_window->show();
}
else
{
m_controller_settings_window->raise();
m_controller_settings_window->activateWindow();
m_controller_settings_window->setFocus();
}
QtUtils::ShowOrRaiseWindow(m_controller_settings_window);
if (category != ControllerSettingsWindow::Category::Count)
m_controller_settings_window->setCategory(category);
}
@ -2933,16 +2889,7 @@ void MainWindow::openMemoryCardEditor(const QString& card_a_path, const QString&
if (!m_memory_card_editor_window)
m_memory_card_editor_window = new MemoryCardEditorWindow();
if (!m_memory_card_editor_window->isVisible())
{
m_memory_card_editor_window->show();
}
else
{
m_memory_card_editor_window->raise();
m_memory_card_editor_window->activateWindow();
m_memory_card_editor_window->setFocus();
}
QtUtils::ShowOrRaiseWindow(m_memory_card_editor_window);
if (!card_a_path.isEmpty())
{
@ -2987,19 +2934,9 @@ void MainWindow::onAchievementsChallengeModeChanged(bool enabled)
{
if (enabled)
{
if (m_cheat_manager_dialog)
{
m_cheat_manager_dialog->close();
delete m_cheat_manager_dialog;
m_cheat_manager_dialog = nullptr;
}
if (m_debugger_window)
{
m_debugger_window->close();
delete m_debugger_window;
m_debugger_window = nullptr;
}
QtUtils::CloseAndDeleteWindow(m_cheat_manager_window);
QtUtils::CloseAndDeleteWindow(m_debugger_window);
QtUtils::CloseAndDeleteWindow(m_memory_scanner_window);
}
updateEmulationActions(false, System::IsValid(), enabled);
@ -3019,44 +2956,52 @@ void MainWindow::onToolsCoverDownloaderTriggered()
dlg.exec();
}
void MainWindow::openCheatManager()
void MainWindow::onToolsMemoryScannerTriggered()
{
if (!m_cheat_manager_dialog)
m_cheat_manager_dialog = new CheatManagerDialog(this);
if (Achievements::IsHardcoreModeActive())
return;
if (!m_cheat_manager_dialog->isVisible())
{
m_cheat_manager_dialog->show();
}
else
if (!m_memory_scanner_window)
{
m_cheat_manager_dialog->raise();
m_cheat_manager_dialog->activateWindow();
m_cheat_manager_dialog->setFocus();
m_memory_scanner_window = new MemoryScannerWindow();
connect(m_memory_scanner_window, &MemoryScannerWindow::closed, this, [this]() {
m_memory_scanner_window->deleteLater();
m_memory_scanner_window = nullptr;
});
}
QtUtils::ShowOrRaiseWindow(m_memory_scanner_window);
}
void MainWindow::openCPUDebugger()
void MainWindow::openCheatManager()
{
if (m_debugger_window)
{
m_debugger_window->raise();
m_debugger_window->activateWindow();
m_debugger_window->setFocus();
if (Achievements::IsHardcoreModeActive())
return;
if (!m_cheat_manager_window)
{
m_cheat_manager_window = new CheatManagerWindow();
connect(m_cheat_manager_window, &CheatManagerWindow::closed, this, [this]() {
m_cheat_manager_window->deleteLater();
m_cheat_manager_window = nullptr;
});
}
Assert(!m_debugger_window);
m_debugger_window = new DebuggerWindow();
connect(m_debugger_window, &DebuggerWindow::closed, this, &MainWindow::onCPUDebuggerClosed);
m_debugger_window->show();
QtUtils::ShowOrRaiseWindow(m_cheat_manager_window);
}
void MainWindow::onCPUDebuggerClosed()
void MainWindow::openCPUDebugger()
{
Assert(m_debugger_window);
m_debugger_window->deleteLater();
m_debugger_window = nullptr;
if (!m_debugger_window)
{
m_debugger_window = new DebuggerWindow();
connect(m_debugger_window, &DebuggerWindow::closed, this, [this]() {
m_debugger_window->deleteLater();
m_debugger_window = nullptr;
});
}
QtUtils::ShowOrRaiseWindow(m_debugger_window);
}
void MainWindow::onToolsOpenDataDirectoryTriggered()
@ -3067,13 +3012,9 @@ void MainWindow::onToolsOpenDataDirectoryTriggered()
void MainWindow::onSettingsTriggeredFromToolbar()
{
if (s_system_valid)
{
m_settings_toolbar_menu->exec(QCursor::pos());
}
else
{
doSettings();
}
}
void MainWindow::checkForUpdates(bool display_message)

View file

@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: 2019-2022 Connor McLaughlin <stenzek@gmail.com>
// SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin <stenzek@gmail.com>
// SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0)
#pragma once
@ -24,13 +24,14 @@ class QLabel;
class QThread;
class QProgressBar;
class MainWindow;
class GameListWidget;
class EmuThread;
class AutoUpdaterDialog;
class MemoryCardEditorWindow;
class CheatManagerDialog;
class CheatManagerWindow;
class DebuggerWindow;
class MainWindow;
class MemoryScannerWindow;
class GPUDevice;
namespace Achievements {
@ -96,6 +97,9 @@ public:
ALWAYS_INLINE QLabel* getStatusFPSWidget() const { return m_status_fps_widget; }
ALWAYS_INLINE QLabel* getStatusVPSWidget() const { return m_status_vps_widget; }
/// Accessors for child windows.
CheatManagerWindow* getCheatManagerWindow() const { return m_cheat_manager_window; }
public Q_SLOTS:
/// Updates debug menu visibility (hides if disabled).
void updateDebugMenuVisibility();
@ -166,6 +170,7 @@ private Q_SLOTS:
void onAboutActionTriggered();
void onCheckForUpdatesActionTriggered();
void onToolsMemoryCardEditorTriggered();
void onToolsMemoryScannerTriggered();
void onToolsCoverDownloaderTriggered();
void onToolsOpenDataDirectoryTriggered();
void onSettingsTriggeredFromToolbar();
@ -180,7 +185,6 @@ private Q_SLOTS:
void openCheatManager();
void openCPUDebugger();
void onCPUDebuggerClosed();
protected:
void showEvent(QShowEvent* event) override;
@ -290,8 +294,9 @@ private:
AutoUpdaterDialog* m_auto_updater_dialog = nullptr;
MemoryCardEditorWindow* m_memory_card_editor_window = nullptr;
CheatManagerDialog* m_cheat_manager_dialog = nullptr;
CheatManagerWindow* m_cheat_manager_window = nullptr;
DebuggerWindow* m_debugger_window = nullptr;
MemoryScannerWindow* m_memory_scanner_window = nullptr;
bool m_was_paused_by_focus_loss = false;
bool m_open_debugger_on_start = false;

View file

@ -30,7 +30,7 @@
<x>0</x>
<y>0</y>
<width>800</width>
<height>21</height>
<height>33</height>
</rect>
</property>
<widget class="QMenu" name="menuSystem">
@ -42,8 +42,7 @@
<string>Change Disc</string>
</property>
<property name="icon">
<iconset theme="disc-line">
<normaloff>.</normaloff>.</iconset>
<iconset theme="disc-line"/>
</property>
<actiongroup name="actionGroupChangeDiscSubImages"/>
<addaction name="actionChangeDiscFromFile"/>
@ -57,8 +56,7 @@
<string>Cheats</string>
</property>
<property name="icon">
<iconset theme="cheats-line">
<normaloff>.</normaloff>.</iconset>
<iconset theme="cheats-line"/>
</property>
</widget>
<widget class="QMenu" name="menuLoadState">
@ -66,8 +64,7 @@
<string>Load State</string>
</property>
<property name="icon">
<iconset theme="folder-open-line">
<normaloff>.</normaloff>.</iconset>
<iconset theme="folder-open-line"/>
</property>
</widget>
<widget class="QMenu" name="menuSaveState">
@ -75,8 +72,7 @@
<string>Save State</string>
</property>
<property name="icon">
<iconset theme="save-3-line">
<normaloff>.</normaloff>.</iconset>
<iconset theme="save-3-line"/>
</property>
</widget>
<addaction name="actionStartFile"/>
@ -109,8 +105,7 @@
<string>Theme</string>
</property>
<property name="icon">
<iconset theme="paint-brush-line">
<normaloff>.</normaloff>.</iconset>
<iconset theme="paint-brush-line"/>
</property>
</widget>
<widget class="QMenu" name="menuSettingsLanguage">
@ -118,8 +113,7 @@
<string>Language</string>
</property>
<property name="icon">
<iconset theme="global-line">
<normaloff>.</normaloff>.</iconset>
<iconset theme="global-line"/>
</property>
</widget>
<addaction name="actionViewGameProperties"/>
@ -157,7 +151,7 @@
<addaction name="separator"/>
<addaction name="actionCheckForUpdates"/>
<addaction name="separator"/>
<addaction name="actionViewThirdPartyNotices" />
<addaction name="actionViewThirdPartyNotices"/>
<addaction name="actionAboutQt"/>
<addaction name="actionAbout"/>
</widget>
@ -235,9 +229,11 @@
<property name="title">
<string>&amp;Tools</string>
</property>
<addaction name="actionMemory_Card_Editor"/>
<addaction name="actionMemoryCardEditor"/>
<addaction name="actionCoverDownloader"/>
<addaction name="separator"/>
<addaction name="actionMemoryScanner"/>
<addaction name="separator"/>
<addaction name="actionOpenDataDirectory"/>
</widget>
<addaction name="menuSystem"/>
@ -258,7 +254,7 @@
</size>
</property>
<property name="toolButtonStyle">
<enum>Qt::ToolButtonTextUnderIcon</enum>
<enum>Qt::ToolButtonStyle::ToolButtonTextUnderIcon</enum>
</property>
<attribute name="toolBarArea">
<enum>TopToolBarArea</enum>
@ -287,8 +283,7 @@
<widget class="QStatusBar" name="statusBar"/>
<action name="actionStartFile">
<property name="icon">
<iconset theme="file-line">
<normaloff>.</normaloff>.</iconset>
<iconset theme="file-line"/>
</property>
<property name="text">
<string>Start &amp;File...</string>
@ -296,8 +291,7 @@
</action>
<action name="actionStartDisc">
<property name="icon">
<iconset theme="disc-line">
<normaloff>.</normaloff>.</iconset>
<iconset theme="disc-line"/>
</property>
<property name="text">
<string>Start &amp;Disc...</string>
@ -305,8 +299,7 @@
</action>
<action name="actionStartBios">
<property name="icon">
<iconset theme="chip-line">
<normaloff>.</normaloff>.</iconset>
<iconset theme="chip-line"/>
</property>
<property name="text">
<string>Start &amp;BIOS</string>
@ -314,8 +307,7 @@
</action>
<action name="actionScanForNewGames">
<property name="icon">
<iconset theme="file-search-line">
<normaloff>.</normaloff>.</iconset>
<iconset theme="file-search-line"/>
</property>
<property name="text">
<string>&amp;Scan For New Games</string>
@ -323,8 +315,7 @@
</action>
<action name="actionRescanAllGames">
<property name="icon">
<iconset theme="refresh-line">
<normaloff>.</normaloff>.</iconset>
<iconset theme="refresh-line"/>
</property>
<property name="text">
<string>&amp;Rescan All Games</string>
@ -332,8 +323,7 @@
</action>
<action name="actionPowerOff">
<property name="icon">
<iconset theme="shut-down-line">
<normaloff>.</normaloff>.</iconset>
<iconset theme="shut-down-line"/>
</property>
<property name="text">
<string>Power &amp;Off</string>
@ -341,8 +331,7 @@
</action>
<action name="actionReset">
<property name="icon">
<iconset theme="restart-line">
<normaloff>.</normaloff>.</iconset>
<iconset theme="restart-line"/>
</property>
<property name="text">
<string>&amp;Reset</string>
@ -353,8 +342,7 @@
<bool>true</bool>
</property>
<property name="icon">
<iconset theme="pause-line">
<normaloff>.</normaloff>.</iconset>
<iconset theme="pause-line"/>
</property>
<property name="text">
<string>&amp;Pause</string>
@ -362,8 +350,7 @@
</action>
<action name="actionLoadState">
<property name="icon">
<iconset theme="folder-open-line">
<normaloff>.</normaloff>.</iconset>
<iconset theme="folder-open-line"/>
</property>
<property name="text">
<string>&amp;Load State</string>
@ -371,8 +358,7 @@
</action>
<action name="actionSaveState">
<property name="icon">
<iconset theme="save-3-line">
<normaloff>.</normaloff>.</iconset>
<iconset theme="save-3-line"/>
</property>
<property name="text">
<string>&amp;Save State</string>
@ -380,8 +366,7 @@
</action>
<action name="actionExit">
<property name="icon">
<iconset theme="door-open-line">
<normaloff>.</normaloff>.</iconset>
<iconset theme="door-open-line"/>
</property>
<property name="text">
<string>E&amp;xit</string>
@ -389,8 +374,7 @@
</action>
<action name="actionBIOSSettings">
<property name="icon">
<iconset theme="chip-line">
<normaloff>.</normaloff>.</iconset>
<iconset theme="chip-line"/>
</property>
<property name="text">
<string>B&amp;IOS</string>
@ -398,8 +382,7 @@
</action>
<action name="actionConsoleSettings">
<property name="icon">
<iconset theme="chip-2-line">
<normaloff>.</normaloff>.</iconset>
<iconset theme="chip-2-line"/>
</property>
<property name="text">
<string>C&amp;onsole</string>
@ -407,8 +390,7 @@
</action>
<action name="actionEmulationSettings">
<property name="icon">
<iconset theme="emulation-line">
<normaloff>.</normaloff>.</iconset>
<iconset theme="emulation-line"/>
</property>
<property name="text">
<string>E&amp;mulation</string>
@ -416,8 +398,7 @@
</action>
<action name="actionControllerSettings">
<property name="icon">
<iconset theme="controller-digital-line">
<normaloff>.</normaloff>.</iconset>
<iconset theme="controller-digital-line"/>
</property>
<property name="text">
<string>&amp;Controllers</string>
@ -425,8 +406,7 @@
</action>
<action name="actionHotkeySettings">
<property name="icon">
<iconset theme="keyboard-line">
<normaloff>.</normaloff>.</iconset>
<iconset theme="keyboard-line"/>
</property>
<property name="text">
<string>&amp;Hotkeys</string>
@ -434,8 +414,7 @@
</action>
<action name="actionGraphicsSettings">
<property name="icon">
<iconset theme="image-fill">
<normaloff>.</normaloff>.</iconset>
<iconset theme="image-fill"/>
</property>
<property name="text">
<string>&amp;Graphics</string>
@ -443,8 +422,7 @@
</action>
<action name="actionPostProcessingSettings">
<property name="icon">
<iconset theme="sun-fill">
<normaloff>.</normaloff>.</iconset>
<iconset theme="sun-fill"/>
</property>
<property name="text">
<string>&amp;Post-Processing</string>
@ -452,8 +430,7 @@
</action>
<action name="actionFullscreen">
<property name="icon">
<iconset theme="fullscreen-line">
<normaloff>.</normaloff>.</iconset>
<iconset theme="fullscreen-line"/>
</property>
<property name="text">
<string>Fullscreen</string>
@ -493,8 +470,7 @@
</action>
<action name="actionCheckForUpdates">
<property name="icon">
<iconset theme="download-2-line">
<normaloff>.</normaloff>.</iconset>
<iconset theme="download-2-line"/>
</property>
<property name="text">
<string>Check for &amp;Updates...</string>
@ -525,8 +501,7 @@
</action>
<action name="actionChangeDisc">
<property name="icon">
<iconset theme="disc-line">
<normaloff>.</normaloff>.</iconset>
<iconset theme="disc-line"/>
</property>
<property name="text">
<string>Change Disc...</string>
@ -534,8 +509,7 @@
</action>
<action name="actionCheats">
<property name="icon">
<iconset theme="cheats-line">
<normaloff>.</normaloff>.</iconset>
<iconset theme="cheats-line"/>
</property>
<property name="text">
<string>Cheats...</string>
@ -543,8 +517,7 @@
</action>
<action name="actionCheatsToolbar">
<property name="icon">
<iconset theme="cheats-line">
<normaloff>.</normaloff>.</iconset>
<iconset theme="cheats-line"/>
</property>
<property name="text">
<string>Cheats</string>
@ -552,8 +525,7 @@
</action>
<action name="actionAudioSettings">
<property name="icon">
<iconset theme="volume-up-line">
<normaloff>.</normaloff>.</iconset>
<iconset theme="volume-up-line"/>
</property>
<property name="text">
<string>Audio</string>
@ -561,8 +533,7 @@
</action>
<action name="actionAchievementSettings">
<property name="icon">
<iconset theme="trophy-line">
<normaloff>.</normaloff>.</iconset>
<iconset theme="trophy-line"/>
</property>
<property name="text">
<string>Achievements</string>
@ -570,8 +541,7 @@
</action>
<action name="actionFolderSettings">
<property name="icon">
<iconset theme="folder-open-line">
<normaloff>.</normaloff>.</iconset>
<iconset theme="folder-open-line"/>
</property>
<property name="text">
<string>Folders</string>
@ -579,8 +549,7 @@
</action>
<action name="actionGameListSettings">
<property name="icon">
<iconset theme="folder-open-line">
<normaloff>.</normaloff>.</iconset>
<iconset theme="folder-open-line"/>
</property>
<property name="text">
<string>Game List</string>
@ -588,8 +557,7 @@
</action>
<action name="actionInterfaceSettings">
<property name="icon">
<iconset theme="settings-3-line">
<normaloff>.</normaloff>.</iconset>
<iconset theme="settings-3-line"/>
</property>
<property name="text">
<string>&amp;Interface</string>
@ -597,8 +565,7 @@
</action>
<action name="actionAdvancedSettings">
<property name="icon">
<iconset theme="alert-line">
<normaloff>.</normaloff>.</iconset>
<iconset theme="alert-line"/>
</property>
<property name="text">
<string>Advanced</string>
@ -606,8 +573,7 @@
</action>
<action name="actionAddGameDirectory">
<property name="icon">
<iconset theme="folder-add-line">
<normaloff>.</normaloff>.</iconset>
<iconset theme="folder-add-line"/>
</property>
<property name="text">
<string>Add Game Directory...</string>
@ -615,26 +581,24 @@
</action>
<action name="actionSettings">
<property name="icon">
<iconset theme="settings-3-line">
<normaloff>.</normaloff>.</iconset>
<iconset theme="settings-3-line"/>
</property>
<property name="text">
<string>&amp;Settings</string>
</property>
<property name="menuRole">
<enum>QAction::PreferencesRole</enum>
<enum>QAction::MenuRole::PreferencesRole</enum>
</property>
</action>
<action name="actionSettings2">
<property name="icon">
<iconset theme="settings-3-line">
<normaloff>.</normaloff>.</iconset>
<iconset theme="settings-3-line"/>
</property>
<property name="text">
<string>&amp;Settings</string>
</property>
<property name="menuRole">
<enum>QAction::PreferencesRole</enum>
<enum>QAction::MenuRole::PreferencesRole</enum>
</property>
</action>
<action name="actionChangeDiscFromFile">
@ -788,8 +752,7 @@
</action>
<action name="actionScreenshot">
<property name="icon">
<iconset theme="screenshot-2-line">
<normaloff>.</normaloff>.</iconset>
<iconset theme="screenshot-2-line"/>
</property>
<property name="text">
<string>&amp;Screenshot</string>
@ -797,8 +760,7 @@
</action>
<action name="actionMemoryCardSettings">
<property name="icon">
<iconset theme="memcard-line">
<normaloff>.</normaloff>.</iconset>
<iconset theme="memcard-line"/>
</property>
<property name="text">
<string>&amp;Memory Cards</string>
@ -806,8 +768,7 @@
</action>
<action name="actionResumeLastState">
<property name="icon">
<iconset theme="play-circle-line">
<normaloff>.</normaloff>.</iconset>
<iconset theme="play-circle-line"/>
</property>
<property name="text">
<string>Resume</string>
@ -851,8 +812,7 @@
</action>
<action name="actionViewGameList">
<property name="icon">
<iconset theme="list-check">
<normaloff>.</normaloff>.</iconset>
<iconset theme="list-check"/>
</property>
<property name="text">
<string>Game &amp;List</string>
@ -871,14 +831,13 @@
<bool>false</bool>
</property>
<property name="icon">
<iconset theme="file-settings-line">
<normaloff>.</normaloff>.</iconset>
<iconset theme="file-settings-line"/>
</property>
<property name="text">
<string>Game &amp;Properties</string>
</property>
</action>
<action name="actionMemory_Card_Editor">
<action name="actionMemoryCardEditor">
<property name="text">
<string>Memory &amp;Card Editor</string>
</property>
@ -898,8 +857,7 @@
</action>
<action name="actionViewGameGrid">
<property name="icon">
<iconset theme="function-line">
<normaloff>.</normaloff>.</iconset>
<iconset theme="function-line"/>
</property>
<property name="text">
<string>Game &amp;Grid</string>
@ -949,8 +907,7 @@
</action>
<action name="actionPowerOffWithoutSaving">
<property name="icon">
<iconset theme="close-line">
<normaloff>.</normaloff>.</iconset>
<iconset theme="close-line"/>
</property>
<property name="text">
<string>Power Off &amp;Without Saving</string>
@ -958,8 +915,7 @@
</action>
<action name="actionStartFullscreenUI">
<property name="icon">
<iconset theme="tv-2-line">
<normaloff>.</normaloff>.</iconset>
<iconset theme="tv-2-line"/>
</property>
<property name="text">
<string>Start Big Picture Mode</string>
@ -967,8 +923,7 @@
</action>
<action name="actionStartFullscreenUI2">
<property name="icon">
<iconset theme="tv-2-line">
<normaloff>.</normaloff>.</iconset>
<iconset theme="tv-2-line"/>
</property>
<property name="text">
<string>Big Picture</string>
@ -979,6 +934,11 @@
<string>Cover Downloader</string>
</property>
</action>
<action name="actionMemoryScanner">
<property name="text">
<string>Memory &amp;Scanner</string>
</property>
</action>
</widget>
<resources>
<include location="resources/resources.qrc"/>

View file

@ -0,0 +1,592 @@
// SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin <stenzek@gmail.com> and contributors.
// SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0)
#include "memoryscannerwindow.h"
#include "cheatcodeeditordialog.h"
#include "common/assert.h"
#include "common/string_util.h"
#include "core/bus.h"
#include "core/cpu_core.h"
#include "core/host.h"
#include "core/system.h"
#include "qthost.h"
#include "qtutils.h"
#include <QtCore/QFileInfo>
#include <QtGui/QColor>
#include <QtWidgets/QFileDialog>
#include <QtWidgets/QInputDialog>
#include <QtWidgets/QMenu>
#include <QtWidgets/QMessageBox>
#include <QtWidgets/QTreeWidgetItemIterator>
#include <array>
#include <utility>
static constexpr std::array<const char*, 6> s_size_strings = {
{TRANSLATE_NOOP("MemoryScannerWindow", "Byte"), TRANSLATE_NOOP("MemoryScannerWindow", "Halfword"),
TRANSLATE_NOOP("MemoryScannerWindow", "Word"), TRANSLATE_NOOP("MemoryScannerWindow", "Signed Byte"),
TRANSLATE_NOOP("MemoryScannerWindow", "Signed Halfword"), TRANSLATE_NOOP("MemoryScannerWindow", "Signed Word")}};
static QString formatHexValue(u32 value, u8 size)
{
return QStringLiteral("0x%1").arg(static_cast<uint>(value), size, 16, QChar('0'));
}
static QString formatHexAndDecValue(u32 value, u8 size, bool is_signed)
{
if (is_signed)
{
u32 value_raw = value;
if (size == 2)
value_raw &= 0xFF;
else if (size == 4)
value_raw &= 0xFFFF;
return QStringLiteral("0x%1 (%2)")
.arg(static_cast<u32>(value_raw), size, 16, QChar('0'))
.arg(static_cast<int>(value));
}
else
return QStringLiteral("0x%1 (%2)").arg(static_cast<u32>(value), size, 16, QChar('0')).arg(static_cast<uint>(value));
}
static QString formatCheatCode(u32 address, u32 value, const MemoryAccessSize size)
{
if (size == MemoryAccessSize::Byte && address <= 0x00200000)
return QStringLiteral("CHEAT CODE: %1 %2")
.arg(static_cast<u32>(address) + 0x30000000, 8, 16, QChar('0'))
.toUpper()
.arg(static_cast<u16>(value), 4, 16, QChar('0'))
.toUpper();
else if (size == MemoryAccessSize::HalfWord && address <= 0x001FFFFE)
return QStringLiteral("CHEAT CODE: %1 %2")
.arg(static_cast<u32>(address) + 0x80000000, 8, 16, QChar('0'))
.toUpper()
.arg(static_cast<u16>(value), 4, 16, QChar('0'))
.toUpper();
else if (size == MemoryAccessSize::Word && address <= 0x001FFFFC)
return QStringLiteral("CHEAT CODE: %1 %2")
.arg(static_cast<u32>(address) + 0x90000000, 8, 16, QChar('0'))
.toUpper()
.arg(static_cast<u32>(value), 8, 16, QChar('0'))
.toUpper();
else
return QStringLiteral("OUTSIDE RAM RANGE. POKE %1 with %2")
.arg(static_cast<u32>(address), 8, 16, QChar('0'))
.toUpper()
.arg(static_cast<u16>(value), 8, 16, QChar('0'))
.toUpper();
}
static QString formatValue(u32 value, bool is_signed)
{
if (is_signed)
return QString::number(static_cast<int>(value));
else
return QString::number(static_cast<uint>(value));
}
MemoryScannerWindow::MemoryScannerWindow() : QWidget()
{
m_ui.setupUi(this);
connectUi();
}
MemoryScannerWindow::~MemoryScannerWindow() = default;
void MemoryScannerWindow::connectUi()
{
m_ui.scanStartAddress->setText(formatHexValue(m_scanner.GetStartAddress(), 8));
m_ui.scanEndAddress->setText(formatHexValue(m_scanner.GetEndAddress(), 8));
connect(m_ui.scanValue, &QLineEdit::textChanged, this, &MemoryScannerWindow::updateScanValue);
connect(m_ui.scanValueBase, QOverload<int>::of(&QComboBox::currentIndexChanged),
[this](int index) { updateScanValue(); });
connect(m_ui.scanSize, QOverload<int>::of(&QComboBox::currentIndexChanged), [this](int index) {
m_scanner.SetSize(static_cast<MemoryAccessSize>(index));
m_scanner.ResetSearch();
updateResults();
});
connect(m_ui.scanValueSigned, QOverload<int>::of(&QComboBox::currentIndexChanged), [this](int index) {
m_scanner.SetValueSigned(index == 0);
m_scanner.ResetSearch();
updateResults();
});
connect(m_ui.scanOperator, QOverload<int>::of(&QComboBox::currentIndexChanged),
[this](int index) { m_scanner.SetOperator(static_cast<MemoryScan::Operator>(index)); });
connect(m_ui.scanStartAddress, &QLineEdit::textChanged, [this](const QString& value) {
uint address;
if (value.startsWith(QStringLiteral("0x")) && value.length() > 2)
address = value.mid(2).toUInt(nullptr, 16);
else
address = value.toUInt(nullptr, 16);
m_scanner.SetStartAddress(static_cast<PhysicalMemoryAddress>(address));
});
connect(m_ui.scanEndAddress, &QLineEdit::textChanged, [this](const QString& value) {
uint address;
if (value.startsWith(QStringLiteral("0x")) && value.length() > 2)
address = value.mid(2).toUInt(nullptr, 16);
else
address = value.toUInt(nullptr, 16);
m_scanner.SetEndAddress(static_cast<PhysicalMemoryAddress>(address));
});
connect(m_ui.scanPresetRange, QOverload<int>::of(&QComboBox::currentIndexChanged), [this](int index) {
if (index == 0)
{
m_ui.scanStartAddress->setText(formatHexValue(0, 8));
m_ui.scanEndAddress->setText(formatHexValue(Bus::g_ram_size, 8));
}
else if (index == 1)
{
m_ui.scanStartAddress->setText(formatHexValue(CPU::SCRATCHPAD_ADDR, 8));
m_ui.scanEndAddress->setText(formatHexValue(CPU::SCRATCHPAD_ADDR + CPU::SCRATCHPAD_SIZE, 8));
}
else
{
m_ui.scanStartAddress->setText(formatHexValue(Bus::BIOS_BASE, 8));
m_ui.scanEndAddress->setText(formatHexValue(Bus::BIOS_BASE + Bus::BIOS_SIZE, 8));
}
});
connect(m_ui.scanNewSearch, &QPushButton::clicked, [this]() {
m_scanner.Search();
updateResults();
});
connect(m_ui.scanSearchAgain, &QPushButton::clicked, [this]() {
m_scanner.SearchAgain();
updateResults();
});
connect(m_ui.scanResetSearch, &QPushButton::clicked, [this]() {
m_scanner.ResetSearch();
updateResults();
});
connect(m_ui.scanAddWatch, &QPushButton::clicked, this, &MemoryScannerWindow::addToWatchClicked);
connect(m_ui.scanAddManualAddress, &QPushButton::clicked, this, &MemoryScannerWindow::addManualWatchAddressClicked);
connect(m_ui.scanRemoveWatch, &QPushButton::clicked, this, &MemoryScannerWindow::removeWatchClicked);
connect(m_ui.scanTable, &QTableWidget::currentItemChanged, this, &MemoryScannerWindow::scanCurrentItemChanged);
connect(m_ui.watchTable, &QTableWidget::currentItemChanged, this, &MemoryScannerWindow::watchCurrentItemChanged);
connect(m_ui.scanTable, &QTableWidget::itemChanged, this, &MemoryScannerWindow::scanItemChanged);
connect(m_ui.watchTable, &QTableWidget::itemChanged, this, &MemoryScannerWindow::watchItemChanged);
m_update_timer = new QTimer(this);
connect(m_update_timer, &QTimer::timeout, this, &MemoryScannerWindow::updateScanUi);
connect(g_emu_thread, &EmuThread::systemStarted, this, &MemoryScannerWindow::onSystemStarted);
connect(g_emu_thread, &EmuThread::systemDestroyed, this, &MemoryScannerWindow::onSystemDestroyed);
if (QtHost::IsSystemValid())
onSystemStarted();
else
enableUi(false);
}
void MemoryScannerWindow::enableUi(bool enabled)
{
const bool has_results = (m_scanner.GetResultCount() > 0);
m_ui.scanValue->setEnabled(enabled);
m_ui.scanValueBase->setEnabled(enabled);
m_ui.scanValueSigned->setEnabled(enabled);
m_ui.scanSize->setEnabled(enabled);
m_ui.scanOperator->setEnabled(enabled);
m_ui.scanStartAddress->setEnabled(enabled);
m_ui.scanEndAddress->setEnabled(enabled);
m_ui.scanPresetRange->setEnabled(enabled);
m_ui.scanResultCount->setEnabled(enabled);
m_ui.scanNewSearch->setEnabled(enabled);
m_ui.scanSearchAgain->setEnabled(enabled && has_results);
m_ui.scanResetSearch->setEnabled(enabled && has_results);
m_ui.scanAddWatch->setEnabled(enabled && !m_ui.scanTable->selectedItems().empty());
m_ui.watchTable->setEnabled(enabled);
m_ui.scanAddManualAddress->setEnabled(enabled);
m_ui.scanRemoveWatch->setEnabled(enabled && !m_ui.watchTable->selectedItems().empty());
}
void MemoryScannerWindow::showEvent(QShowEvent* event)
{
QWidget::showEvent(event);
resizeColumns();
}
void MemoryScannerWindow::closeEvent(QCloseEvent* event)
{
QWidget::closeEvent(event);
emit closed();
}
void MemoryScannerWindow::resizeEvent(QResizeEvent* event)
{
QWidget::resizeEvent(event);
resizeColumns();
}
void MemoryScannerWindow::resizeColumns()
{
QtUtils::ResizeColumnsForTableView(m_ui.scanTable, {-1, 130, 130});
QtUtils::ResizeColumnsForTableView(m_ui.watchTable, {-1, 100, 100, 100, 40});
}
int MemoryScannerWindow::getSelectedResultIndexFirst() const
{
QList<QTableWidgetSelectionRange> sel = m_ui.scanTable->selectedRanges();
if (sel.isEmpty())
return -1;
return sel.front().topRow();
}
int MemoryScannerWindow::getSelectedResultIndexLast() const
{
QList<QTableWidgetSelectionRange> sel = m_ui.scanTable->selectedRanges();
if (sel.isEmpty())
return -1;
return sel.front().bottomRow();
}
int MemoryScannerWindow::getSelectedWatchIndexFirst() const
{
QList<QTableWidgetSelectionRange> sel = m_ui.watchTable->selectedRanges();
if (sel.isEmpty())
return -1;
return sel.front().topRow();
}
int MemoryScannerWindow::getSelectedWatchIndexLast() const
{
QList<QTableWidgetSelectionRange> sel = m_ui.watchTable->selectedRanges();
if (sel.isEmpty())
return -1;
return sel.front().bottomRow();
}
void MemoryScannerWindow::onSystemStarted()
{
if (!m_update_timer->isActive())
m_update_timer->start(SCAN_INTERVAL);
enableUi(true);
}
void MemoryScannerWindow::onSystemDestroyed()
{
if (m_update_timer->isActive())
m_update_timer->stop();
enableUi(false);
}
void MemoryScannerWindow::addToWatchClicked()
{
const int indexFirst = getSelectedResultIndexFirst();
const int indexLast = getSelectedResultIndexLast();
if (indexFirst < 0)
return;
for (int index = indexFirst; index <= indexLast; index++)
{
const MemoryScan::Result& res = m_scanner.GetResults()[static_cast<u32>(index)];
m_watch.AddEntry(fmt::format("0x{:08x}", res.address), res.address, m_scanner.GetSize(), m_scanner.GetValueSigned(),
false);
updateWatch();
}
}
void MemoryScannerWindow::addManualWatchAddressClicked()
{
std::optional<unsigned> address = QtUtils::PromptForAddress(this, windowTitle(), tr("Enter manual address:"), false);
if (!address.has_value())
return;
QStringList items;
for (const char* title : s_size_strings)
items.append(tr(title));
bool ok = false;
QString selected_item(QInputDialog::getItem(this, windowTitle(), tr("Select data size:"), items, 0, false, &ok));
int index = items.indexOf(selected_item);
if (index < 0 || !ok)
return;
if (index == 1 || index == 4)
address.value() &= 0xFFFFFFFE;
else if (index == 2 || index == 5)
address.value() &= 0xFFFFFFFC;
m_watch.AddEntry(fmt::format("0x{:08x}", address.value()), address.value(), static_cast<MemoryAccessSize>(index % 3),
(index > 3), false);
updateWatch();
}
void MemoryScannerWindow::removeWatchClicked()
{
const int indexFirst = getSelectedWatchIndexFirst();
const int indexLast = getSelectedWatchIndexLast();
if (indexFirst < 0)
return;
for (int index = indexLast; index >= indexFirst; index--)
{
m_watch.RemoveEntry(static_cast<u32>(index));
updateWatch();
}
}
void MemoryScannerWindow::scanCurrentItemChanged(QTableWidgetItem* current, QTableWidgetItem* previous)
{
m_ui.scanAddWatch->setEnabled((current != nullptr));
}
void MemoryScannerWindow::watchCurrentItemChanged(QTableWidgetItem* current, QTableWidgetItem* previous)
{
m_ui.scanRemoveWatch->setEnabled((current != nullptr));
}
void MemoryScannerWindow::scanItemChanged(QTableWidgetItem* item)
{
const u32 index = static_cast<u32>(item->row());
switch (item->column())
{
case 1:
{
bool value_ok = false;
if (m_scanner.GetValueSigned())
{
int value = item->text().toInt(&value_ok);
if (value_ok)
m_scanner.SetResultValue(index, static_cast<u32>(value));
}
else
{
uint value = item->text().toUInt(&value_ok);
if (value_ok)
m_scanner.SetResultValue(index, static_cast<u32>(value));
}
}
break;
default:
break;
}
}
void MemoryScannerWindow::watchItemChanged(QTableWidgetItem* item)
{
const u32 index = static_cast<u32>(item->row());
if (index >= m_watch.GetEntryCount())
return;
switch (item->column())
{
case 4:
{
m_watch.SetEntryFreeze(index, (item->checkState() == Qt::Checked));
}
break;
case 0:
{
m_watch.SetEntryDescription(index, item->text().toStdString());
}
break;
case 3:
{
const MemoryWatchList::Entry& entry = m_watch.GetEntry(index);
bool value_ok = false;
if (entry.is_signed)
{
int value = item->text().toInt(&value_ok);
if (value_ok)
m_watch.SetEntryValue(index, static_cast<u32>(value));
}
else
{
uint value;
if (item->text()[1] == 'x' || item->text()[1] == 'X')
value = item->text().toUInt(&value_ok, 16);
else
value = item->text().toUInt(&value_ok);
if (value_ok)
m_watch.SetEntryValue(index, static_cast<u32>(value));
}
}
break;
default:
break;
}
}
void MemoryScannerWindow::updateScanValue()
{
QString value = m_ui.scanValue->text();
if (value.startsWith(QStringLiteral("0x")))
value.remove(0, 2);
bool ok = false;
uint uint_value = value.toUInt(&ok, (m_ui.scanValueBase->currentIndex() > 0) ? 16 : 10);
if (ok)
m_scanner.SetValue(uint_value);
}
void MemoryScannerWindow::updateResults()
{
QSignalBlocker sb(m_ui.scanTable);
m_ui.scanTable->setRowCount(0);
int row = 0;
const MemoryScan::ResultVector& results = m_scanner.GetResults();
for (const MemoryScan::Result& res : results)
{
if (row == MAX_DISPLAYED_SCAN_RESULTS)
break;
m_ui.scanTable->insertRow(row);
QTableWidgetItem* address_item = new QTableWidgetItem(formatHexValue(res.address, 8));
address_item->setFlags(address_item->flags() & ~(Qt::ItemIsEditable));
m_ui.scanTable->setItem(row, 0, address_item);
QTableWidgetItem* value_item;
if (m_ui.scanValueBase->currentIndex() == 0)
value_item = new QTableWidgetItem(formatValue(res.value, m_scanner.GetValueSigned()));
else if (m_scanner.GetSize() == MemoryAccessSize::Byte)
value_item = new QTableWidgetItem(formatHexValue(res.value, 2));
else if (m_scanner.GetSize() == MemoryAccessSize::HalfWord)
value_item = new QTableWidgetItem(formatHexValue(res.value, 4));
else
value_item = new QTableWidgetItem(formatHexValue(res.value, 8));
m_ui.scanTable->setItem(row, 1, value_item);
QTableWidgetItem* previous_item;
if (m_ui.scanValueBase->currentIndex() == 0)
previous_item = new QTableWidgetItem(formatValue(res.last_value, m_scanner.GetValueSigned()));
else if (m_scanner.GetSize() == MemoryAccessSize::Byte)
previous_item = new QTableWidgetItem(formatHexValue(res.last_value, 2));
else if (m_scanner.GetSize() == MemoryAccessSize::HalfWord)
previous_item = new QTableWidgetItem(formatHexValue(res.last_value, 4));
else
previous_item = new QTableWidgetItem(formatHexValue(res.last_value, 8));
previous_item->setFlags(address_item->flags() & ~(Qt::ItemIsEditable));
m_ui.scanTable->setItem(row, 2, previous_item);
row++;
}
m_ui.scanResultCount->setText((row < static_cast<int>(results.size())) ?
tr("%1 (only showing first %2)").arg(results.size()).arg(row) :
QString::number(m_scanner.GetResultCount()));
m_ui.scanResetSearch->setEnabled(!results.empty());
m_ui.scanSearchAgain->setEnabled(!results.empty());
m_ui.scanAddWatch->setEnabled(false);
}
void MemoryScannerWindow::updateResultsValues()
{
QSignalBlocker sb(m_ui.scanTable);
int row = 0;
for (const MemoryScan::Result& res : m_scanner.GetResults())
{
if (res.value_changed)
{
QTableWidgetItem* item = m_ui.scanTable->item(row, 1);
if (m_ui.scanValueBase->currentIndex() == 0)
item->setText(formatValue(res.value, m_scanner.GetValueSigned()));
else if (m_scanner.GetSize() == MemoryAccessSize::Byte)
item->setText(formatHexValue(res.value, 2));
else if (m_scanner.GetSize() == MemoryAccessSize::HalfWord)
item->setText(formatHexValue(res.value, 4));
else
item->setText(formatHexValue(res.value, 8));
item->setForeground(Qt::red);
}
row++;
if (row == MAX_DISPLAYED_SCAN_RESULTS)
break;
}
}
void MemoryScannerWindow::updateWatch()
{
m_watch.UpdateValues();
QSignalBlocker sb(m_ui.watchTable);
m_ui.watchTable->setRowCount(0);
const MemoryWatchList::EntryVector& entries = m_watch.GetEntries();
if (!entries.empty())
{
int row = 0;
for (const MemoryWatchList::Entry& res : entries)
{
m_ui.watchTable->insertRow(row);
QTableWidgetItem* description_item = new QTableWidgetItem(formatCheatCode(res.address, res.value, res.size));
m_ui.watchTable->setItem(row, 0, description_item);
QTableWidgetItem* address_item = new QTableWidgetItem(formatHexValue(res.address, 8));
address_item->setFlags(address_item->flags() & ~(Qt::ItemIsEditable));
m_ui.watchTable->setItem(row, 1, address_item);
QTableWidgetItem* size_item =
new QTableWidgetItem(tr(s_size_strings[static_cast<u32>(res.size) + (res.is_signed ? 3 : 0)]));
size_item->setFlags(address_item->flags() & ~(Qt::ItemIsEditable));
m_ui.watchTable->setItem(row, 2, size_item);
QTableWidgetItem* value_item;
if (res.size == MemoryAccessSize::Byte)
value_item = new QTableWidgetItem(formatHexAndDecValue(res.value, 2, res.is_signed));
else if (res.size == MemoryAccessSize::HalfWord)
value_item = new QTableWidgetItem(formatHexAndDecValue(res.value, 4, res.is_signed));
else
value_item = new QTableWidgetItem(formatHexAndDecValue(res.value, 8, res.is_signed));
m_ui.watchTable->setItem(row, 3, value_item);
QTableWidgetItem* freeze_item = new QTableWidgetItem();
freeze_item->setFlags(freeze_item->flags() | (Qt::ItemIsEditable | Qt::ItemIsUserCheckable));
freeze_item->setCheckState(res.freeze ? Qt::Checked : Qt::Unchecked);
m_ui.watchTable->setItem(row, 4, freeze_item);
row++;
}
}
m_ui.scanSaveWatch->setEnabled(!entries.empty());
m_ui.scanRemoveWatch->setEnabled(false);
}
void MemoryScannerWindow::updateWatchValues()
{
QSignalBlocker sb(m_ui.watchTable);
int row = 0;
for (const MemoryWatchList::Entry& res : m_watch.GetEntries())
{
if (res.changed)
{
if (m_ui.scanValueBase->currentIndex() == 0)
m_ui.watchTable->item(row, 3)->setText(formatValue(res.value, res.is_signed));
else if (m_scanner.GetSize() == MemoryAccessSize::Byte)
m_ui.watchTable->item(row, 3)->setText(formatHexValue(res.value, 2));
else if (m_scanner.GetSize() == MemoryAccessSize::HalfWord)
m_ui.watchTable->item(row, 3)->setText(formatHexValue(res.value, 4));
else
m_ui.watchTable->item(row, 3)->setText(formatHexValue(res.value, 8));
}
row++;
}
}
void MemoryScannerWindow::updateScanUi()
{
m_scanner.UpdateResultsValues();
m_watch.UpdateValues();
updateResultsValues();
updateWatchValues();
}

View file

@ -0,0 +1,74 @@
// SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin <stenzek@gmail.com>
// SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0)
#pragma once
#include "ui_memoryscannerwindow.h"
#include "core/cheats.h"
#include <QtCore/QTimer>
#include <QtWidgets/QComboBox>
#include <QtWidgets/QLabel>
#include <QtWidgets/QPushButton>
#include <QtWidgets/QTableWidget>
#include <QtWidgets/QWidget>
#include <optional>
class MemoryScannerWindow : public QWidget
{
Q_OBJECT
public:
MemoryScannerWindow();
~MemoryScannerWindow();
Q_SIGNALS:
void closed();
protected:
void showEvent(QShowEvent* event);
void closeEvent(QCloseEvent* event);
void resizeEvent(QResizeEvent* event);
private Q_SLOTS:
void onSystemStarted();
void onSystemDestroyed();
void addToWatchClicked();
void addManualWatchAddressClicked();
void removeWatchClicked();
void scanCurrentItemChanged(QTableWidgetItem* current, QTableWidgetItem* previous);
void watchCurrentItemChanged(QTableWidgetItem* current, QTableWidgetItem* previous);
void scanItemChanged(QTableWidgetItem* item);
void watchItemChanged(QTableWidgetItem* item);
void updateScanValue();
void updateScanUi();
private:
enum : int
{
MAX_DISPLAYED_SCAN_RESULTS = 5000,
SCAN_INTERVAL = 100,
};
void connectUi();
void enableUi(bool enabled);
void resizeColumns();
void updateResults();
void updateResultsValues();
void updateWatch();
void updateWatchValues();
int getSelectedResultIndexFirst() const;
int getSelectedResultIndexLast() const;
int getSelectedWatchIndexFirst() const;
int getSelectedWatchIndexLast() const;
Ui::MemoryScannerWindow m_ui;
MemoryScan m_scanner;
MemoryWatchList m_watch;
QTimer* m_update_timer = nullptr;
};

View file

@ -0,0 +1,470 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>MemoryScannerWindow</class>
<widget class="QWidget" name="MemoryScannerWindow">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>833</width>
<height>610</height>
</rect>
</property>
<property name="windowTitle">
<string>Memory Scanner</string>
</property>
<property name="windowIcon">
<iconset>
<normaloff>:/icons/duck.png</normaloff>:/icons/duck.png</iconset>
</property>
<layout class="QGridLayout" name="gridLayout_3">
<item row="0" column="0">
<widget class="QTableWidget" name="scanTable">
<property name="alternatingRowColors">
<bool>true</bool>
</property>
<property name="selectionMode">
<enum>QAbstractItemView::SelectionMode::ContiguousSelection</enum>
</property>
<property name="selectionBehavior">
<enum>QAbstractItemView::SelectionBehavior::SelectRows</enum>
</property>
<property name="showGrid">
<bool>false</bool>
</property>
<property name="sortingEnabled">
<bool>true</bool>
</property>
<attribute name="horizontalHeaderHighlightSections">
<bool>false</bool>
</attribute>
<attribute name="verticalHeaderVisible">
<bool>false</bool>
</attribute>
<column>
<property name="text">
<string>Address</string>
</property>
</column>
<column>
<property name="text">
<string>Value</string>
</property>
</column>
<column>
<property name="text">
<string>Previous Value</string>
</property>
</column>
</widget>
</item>
<item row="0" column="1">
<widget class="QGroupBox" name="groupBox_2">
<property name="title">
<string>Search Parameters</string>
</property>
<layout class="QFormLayout" name="formLayout">
<item row="0" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>Value:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QLineEdit" name="scanValue"/>
</item>
<item>
<widget class="QComboBox" name="scanValueSigned">
<property name="currentIndex">
<number>1</number>
</property>
<item>
<property name="text">
<string>Signed</string>
</property>
</item>
<item>
<property name="text">
<string>Unsigned</string>
</property>
</item>
</widget>
</item>
<item>
<widget class="QComboBox" name="scanValueBase">
<property name="currentIndex">
<number>1</number>
</property>
<item>
<property name="text">
<string>Decimal</string>
</property>
</item>
<item>
<property name="text">
<string>Hex</string>
</property>
</item>
</widget>
</item>
</layout>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_3">
<property name="text">
<string>Data Size:</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QComboBox" name="scanSize">
<property name="currentIndex">
<number>1</number>
</property>
<item>
<property name="text">
<string>Byte (1 byte)</string>
</property>
</item>
<item>
<property name="text">
<string>Halfword (2 bytes)</string>
</property>
</item>
<item>
<property name="text">
<string>Word (4 bytes)</string>
</property>
</item>
</widget>
</item>
<item row="4" column="0">
<widget class="QLabel" name="label_2">
<property name="text">
<string>Operator:</string>
</property>
</widget>
</item>
<item row="4" column="1">
<widget class="QComboBox" name="scanOperator">
<item>
<property name="text">
<string>Equal to...</string>
</property>
</item>
<item>
<property name="text">
<string>Not Equal to...</string>
</property>
</item>
<item>
<property name="text">
<string>Greater Than...</string>
</property>
</item>
<item>
<property name="text">
<string>Greater or Equal...</string>
</property>
</item>
<item>
<property name="text">
<string>Less Than...</string>
</property>
</item>
<item>
<property name="text">
<string>Less or Equal...</string>
</property>
</item>
<item>
<property name="text">
<string>Increased By...</string>
</property>
</item>
<item>
<property name="text">
<string>Decreased By...</string>
</property>
</item>
<item>
<property name="text">
<string>Changed By...</string>
</property>
</item>
<item>
<property name="text">
<string>Equal to Previous (Unchanged Value)</string>
</property>
</item>
<item>
<property name="text">
<string>Not Equal to Previous (Changed Value)</string>
</property>
</item>
<item>
<property name="text">
<string>Greater Than Previous</string>
</property>
</item>
<item>
<property name="text">
<string>Greater or Equal to Previous</string>
</property>
</item>
<item>
<property name="text">
<string>Less Than Previous</string>
</property>
</item>
<item>
<property name="text">
<string>Less or Equal to Previous</string>
</property>
</item>
<item>
<property name="text">
<string>Any Value</string>
</property>
</item>
</widget>
</item>
<item row="5" column="0">
<widget class="QLabel" name="label_4">
<property name="text">
<string>Start Address:</string>
</property>
</widget>
</item>
<item row="5" column="1">
<widget class="QLineEdit" name="scanStartAddress"/>
</item>
<item row="6" column="0">
<widget class="QLabel" name="label_5">
<property name="text">
<string>End Address:</string>
</property>
</widget>
</item>
<item row="6" column="1">
<widget class="QLineEdit" name="scanEndAddress"/>
</item>
<item row="7" column="0">
<widget class="QLabel" name="label_6">
<property name="text">
<string>Preset Range:</string>
</property>
</widget>
</item>
<item row="7" column="1">
<widget class="QComboBox" name="scanPresetRange">
<item>
<property name="text">
<string>RAM</string>
</property>
</item>
<item>
<property name="text">
<string>Scratchpad</string>
</property>
</item>
<item>
<property name="text">
<string>BIOS</string>
</property>
</item>
</widget>
</item>
<item row="8" column="0">
<widget class="QLabel" name="label_7">
<property name="text">
<string>Result Count:</string>
</property>
</widget>
</item>
<item row="8" column="1">
<widget class="QLabel" name="scanResultCount">
<property name="text">
<string>0</string>
</property>
</widget>
</item>
<item row="9" column="0" colspan="2">
<layout class="QHBoxLayout" name="horizontalLayout_3">
<item>
<widget class="QPushButton" name="scanNewSearch">
<property name="text">
<string>New Search</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="scanSearchAgain">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>Search Again</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="scanResetSearch">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>Clear Results</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="10" column="0" colspan="2">
<widget class="QPushButton" name="scanAddWatch">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>Add Selected Results To Watch List</string>
</property>
</widget>
</item>
<item row="11" column="0" colspan="2">
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Orientation::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
</item>
<item row="1" column="0" colspan="2">
<layout class="QGridLayout" name="gridLayout_4">
<item row="0" column="0">
<widget class="QTableWidget" name="watchTable">
<property name="alternatingRowColors">
<bool>true</bool>
</property>
<property name="selectionMode">
<enum>QAbstractItemView::SelectionMode::ContiguousSelection</enum>
</property>
<property name="selectionBehavior">
<enum>QAbstractItemView::SelectionBehavior::SelectRows</enum>
</property>
<property name="showGrid">
<bool>false</bool>
</property>
<property name="sortingEnabled">
<bool>true</bool>
</property>
<attribute name="horizontalHeaderHighlightSections">
<bool>false</bool>
</attribute>
<attribute name="verticalHeaderVisible">
<bool>false</bool>
</attribute>
<column>
<property name="text">
<string>Simple Cheat Code or Description</string>
</property>
</column>
<column>
<property name="text">
<string>Address</string>
</property>
</column>
<column>
<property name="text">
<string>Type</string>
</property>
</column>
<column>
<property name="text">
<string>Value</string>
</property>
</column>
<column>
<property name="text">
<string>Freeze</string>
</property>
</column>
</widget>
</item>
<item row="1" column="0">
<layout class="QHBoxLayout" name="horizontalLayout_4">
<item>
<widget class="QPushButton" name="scanLoadWatch">
<property name="enabled">
<bool>false</bool>
</property>
<property name="visible">
<bool>false</bool>
</property>
<property name="text">
<string>Load Watch</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="scanSaveWatch">
<property name="enabled">
<bool>false</bool>
</property>
<property name="visible">
<bool>false</bool>
</property>
<property name="text">
<string>Save Watch</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="scanAddManualAddress">
<property name="text">
<string>Add Manual Address</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="scanRemoveWatch">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>Remove Selected Entries from Watch List</string>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_2">
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
</layout>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View file

@ -64,6 +64,23 @@ QWidget* GetRootWidget(QWidget* widget, bool stop_at_window_or_dialog)
return widget;
}
void ShowOrRaiseWindow(QWidget* window)
{
if (!window)
return;
if (!window->isVisible())
{
window->show();
}
else
{
window->raise();
window->activateWindow();
window->setFocus();
}
}
template<typename T>
ALWAYS_INLINE_RELEASE static void ResizeColumnsForView(T* view, const std::initializer_list<int>& widths)
{

View file

@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: 2019-2022 Connor McLaughlin <stenzek@gmail.com>
// SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin <stenzek@gmail.com>
// SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0)
#pragma once
@ -11,6 +11,7 @@
#include <QtCore/QMetaType>
#include <QtCore/QString>
#include <QtGui/QIcon>
#include <QtWidgets/QWidget>
#include <functional>
#include <initializer_list>
#include <optional>
@ -46,6 +47,25 @@ QFrame* CreateHorizontalLine(QWidget* parent);
/// Returns the greatest parent of a widget, i.e. its dialog/window.
QWidget* GetRootWidget(QWidget* widget, bool stop_at_window_or_dialog = true);
/// Shows or raises a window (brings it to the front).
void ShowOrRaiseWindow(QWidget* window);
/// Closes and deletes a window later, outside of this event pump.
template<typename T>
[[maybe_unused]] static void CloseAndDeleteWindow(T*& window)
{
if (!window)
return;
window->close();
// Some windows delete themselves.
if (window)
window->deleteLater();
window = nullptr;
}
/// Resizes columns of the table view to at the specified widths. A negative width will stretch the column to use the
/// remaining space.
void ResizeColumnsForTableView(QTableView* view, const std::initializer_list<int>& widths);