640 changes: 640 additions & 0 deletions Source/Core/Core/CheatSearch.cpp

Large diffs are not rendered by default.

219 changes: 219 additions & 0 deletions Source/Core/Core/CheatSearch.h
@@ -0,0 +1,219 @@
// Copyright 2021 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later

#pragma once

#include <functional>
#include <memory>
#include <optional>
#include <string>
#include <variant>
#include <vector>

#include "Common/CommonTypes.h"
#include "Common/Result.h"
#include "Core/PowerPC/MMU.h"

namespace Cheats
{
enum class CompareType
{
Equal,
NotEqual,
Less,
LessOrEqual,
Greater,
GreaterOrEqual,
};

enum class FilterType
{
CompareAgainstSpecificValue,
CompareAgainstLastValue,
DoNotFilter,
};

enum class DataType
{
U8,
U16,
U32,
U64,
S8,
S16,
S32,
S64,
F32,
F64,
};

struct SearchValue
{
std::variant<u8, u16, u32, u64, s8, s16, s32, s64, float, double> m_value;
};

enum class SearchResultValueState : u8
{
ValueFromPhysicalMemory,
ValueFromVirtualMemory,
AddressNotAccessible,
};

template <typename T>
struct SearchResult
{
T m_value;
SearchResultValueState m_value_state;
u32 m_address;

bool IsValueValid() const
{
return m_value_state == SearchResultValueState::ValueFromPhysicalMemory ||
m_value_state == SearchResultValueState::ValueFromVirtualMemory;
}
};

struct MemoryRange
{
u32 m_start;
u64 m_length;

MemoryRange(u32 start, u64 length) : m_start(start), m_length(length) {}
};

enum class SearchErrorCode
{
Success,

// No emulation is currently active.
NoEmulationActive,

// The parameter set given to the search function is bogus.
InvalidParameters,

// This is returned if PowerPC::RequestedAddressSpace::Virtual is given but the MSR.DR flag is
// currently off in the emulated game.
VirtualAddressesCurrentlyNotAccessible,
};

// Returns the corresponding DataType enum for the value currently held by the given SearchValue.
DataType GetDataType(const SearchValue& value);

// Converts the given value to a std::vector<u8>, regardless of the actual stored value type.
// Returned array is in big endian byte order, so it can be copied directly into emulated memory for
// patches or action replay codes.
std::vector<u8> GetValueAsByteVector(const SearchValue& value);

// Do a new search across the given memory region in the given address space, only keeping values
// for which the given validator returns true.
template <typename T>
Common::Result<SearchErrorCode, std::vector<SearchResult<T>>>
NewSearch(const std::vector<MemoryRange>& memory_ranges,
PowerPC::RequestedAddressSpace address_space, bool aligned,
const std::function<bool(const T& value)>& validator);

// Refresh the values for the given results in the given address space, only keeping values for
// which the given validator returns true.
template <typename T>
Common::Result<SearchErrorCode, std::vector<SearchResult<T>>>
NextSearch(const std::vector<SearchResult<T>>& previous_results,
PowerPC::RequestedAddressSpace address_space,
const std::function<bool(const T& new_value, const T& old_value)>& validator);

class CheatSearchSessionBase
{
public:
virtual ~CheatSearchSessionBase();

// Set the value comparison operation used by subsequent searches.
virtual void SetCompareType(CompareType compare_type) = 0;

// Set the filtering value target used by subsequent searches.
virtual void SetFilterType(FilterType filter_type) = 0;

// Set the value of the CompareAgainstSpecificValue filter used by subsequent searches.
virtual bool SetValueFromString(const std::string& value_as_string) = 0;

// Resets the search results, causing the next search to act as a new search.
virtual void ResetResults() = 0;

// Run either a new search or a next search based on the current state of this session.
virtual SearchErrorCode RunSearch() = 0;

virtual size_t GetMemoryRangeCount() = 0;
virtual MemoryRange GetMemoryRange(size_t index) = 0;
virtual PowerPC::RequestedAddressSpace GetAddressSpace() = 0;
virtual DataType GetDataType() = 0;
virtual bool GetAligned() = 0;

virtual size_t GetResultCount() const = 0;
virtual size_t GetValidValueCount() const = 0;
virtual u32 GetResultAddress(size_t index) const = 0;
virtual SearchValue GetResultValueAsSearchValue(size_t index) const = 0;
virtual std::string GetResultValueAsString(size_t index, bool hex) const = 0;
virtual SearchResultValueState GetResultValueState(size_t index) const = 0;
virtual bool WasFirstSearchDone() const = 0;

// Create a complete copy of this search session.
virtual std::unique_ptr<CheatSearchSessionBase> Clone() const = 0;

// Create a partial copy of this search session. Only the results with the passed indices are
// copied. This is useful if you want to run a next search on only partial result data of a
// previous search.
virtual std::unique_ptr<CheatSearchSessionBase>
ClonePartial(const std::vector<size_t>& result_indices) const = 0;
};

template <typename T>
class CheatSearchSession : public CheatSearchSessionBase
{
public:
CheatSearchSession(std::vector<MemoryRange> memory_ranges,
PowerPC::RequestedAddressSpace address_space, bool aligned);
CheatSearchSession(const CheatSearchSession& session);
CheatSearchSession(CheatSearchSession&& session);
CheatSearchSession& operator=(const CheatSearchSession& session);
CheatSearchSession& operator=(CheatSearchSession&& session);
~CheatSearchSession() override;

void SetCompareType(CompareType compare_type) override;
void SetFilterType(FilterType filter_type) override;
bool SetValueFromString(const std::string& value_as_string) override;

void ResetResults() override;
SearchErrorCode RunSearch() override;

size_t GetMemoryRangeCount() override;
MemoryRange GetMemoryRange(size_t index) override;
PowerPC::RequestedAddressSpace GetAddressSpace() override;
DataType GetDataType() override;
bool GetAligned() override;

size_t GetResultCount() const override;
size_t GetValidValueCount() const override;
u32 GetResultAddress(size_t index) const override;
T GetResultValue(size_t index) const;
SearchValue GetResultValueAsSearchValue(size_t index) const override;
std::string GetResultValueAsString(size_t index, bool hex) const override;
SearchResultValueState GetResultValueState(size_t index) const override;
bool WasFirstSearchDone() const override;

std::unique_ptr<CheatSearchSessionBase> Clone() const override;
std::unique_ptr<CheatSearchSessionBase>
ClonePartial(const std::vector<size_t>& result_indices) const override;

private:
std::vector<SearchResult<T>> m_search_results;
std::vector<MemoryRange> m_memory_ranges;
PowerPC::RequestedAddressSpace m_address_space;
CompareType m_compare_type = CompareType::Equal;
FilterType m_filter_type = FilterType::DoNotFilter;
std::optional<T> m_value = std::nullopt;
bool m_aligned;
bool m_first_search_done = false;
};

std::unique_ptr<CheatSearchSessionBase> MakeSession(std::vector<MemoryRange> memory_ranges,
PowerPC::RequestedAddressSpace address_space,
bool aligned, DataType data_type);
} // namespace Cheats
4 changes: 4 additions & 0 deletions Source/Core/DolphinLib.props
Expand Up @@ -163,6 +163,8 @@
<ClInclude Include="Core\Boot\ElfTypes.h" />
<ClInclude Include="Core\BootManager.h" />
<ClInclude Include="Core\CheatCodes.h" />
<ClInclude Include="Core\CheatGeneration.h" />
<ClInclude Include="Core\CheatSearch.h" />
<ClInclude Include="Core\CommonTitles.h" />
<ClInclude Include="Core\Config\DefaultLocale.h" />
<ClInclude Include="Core\Config\FreeLookSettings.h" />
Expand Down Expand Up @@ -743,6 +745,8 @@
<ClCompile Include="Core\Boot\DolReader.cpp" />
<ClCompile Include="Core\Boot\ElfReader.cpp" />
<ClCompile Include="Core\BootManager.cpp" />
<ClCompile Include="Core\CheatGeneration.cpp" />
<ClCompile Include="Core\CheatSearch.cpp" />
<ClCompile Include="Core\Config\DefaultLocale.cpp" />
<ClCompile Include="Core\Config\FreeLookSettings.cpp" />
<ClCompile Include="Core\Config\GraphicsSettings.cpp" />
Expand Down
6 changes: 6 additions & 0 deletions Source/Core/DolphinQt/CMakeLists.txt
Expand Up @@ -16,6 +16,10 @@ set(CMAKE_AUTOMOC ON)
add_executable(dolphin-emu
AboutDialog.cpp
AboutDialog.h
CheatSearchFactoryWidget.cpp
CheatSearchFactoryWidget.h
CheatSearchWidget.cpp
CheatSearchWidget.h
CheatsManager.cpp
CheatsManager.h
ConvertDialog.cpp
Expand Down Expand Up @@ -269,6 +273,8 @@ add_executable(dolphin-emu
QtUtils/ModalMessageBox.cpp
QtUtils/ModalMessageBox.h
QtUtils/ParallelProgressDialog.h
QtUtils/PartiallyClosableTabWidget.cpp
QtUtils/PartiallyClosableTabWidget.h
QtUtils/ImageConverter.cpp
QtUtils/ImageConverter.h
QtUtils/UTF8CodePointCountValidator.cpp
Expand Down
187 changes: 187 additions & 0 deletions Source/Core/DolphinQt/CheatSearchFactoryWidget.cpp
@@ -0,0 +1,187 @@
// Copyright 2021 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later

#include "DolphinQt/CheatSearchFactoryWidget.h"

#include <string>
#include <vector>

#include <QButtonGroup>
#include <QCheckBox>
#include <QComboBox>
#include <QGroupBox>
#include <QLabel>
#include <QLineEdit>
#include <QPushButton>
#include <QRadioButton>
#include <QVBoxLayout>

#include "Common/StringUtil.h"
#include "Core/CheatSearch.h"
#include "Core/Config/MainSettings.h"
#include "Core/ConfigManager.h"
#include "Core/HW/Memmap.h"
#include "Core/PowerPC/MMU.h"

CheatSearchFactoryWidget::CheatSearchFactoryWidget()
{
CreateWidgets();
ConnectWidgets();
RefreshGui();
}

CheatSearchFactoryWidget::~CheatSearchFactoryWidget() = default;

Q_DECLARE_METATYPE(Cheats::DataType);

void CheatSearchFactoryWidget::CreateWidgets()
{
auto* layout = new QVBoxLayout();

auto* address_space_group = new QGroupBox(tr("Address Space"));
auto* address_space_layout = new QVBoxLayout();
address_space_group->setLayout(address_space_layout);

m_standard_address_space = new QRadioButton(tr("Typical GameCube/Wii Address Space"));
m_standard_address_space->setChecked(true);
m_custom_address_space = new QRadioButton(tr("Custom Address Space"));

QLabel* label_standard_address_space =
new QLabel(tr("Sets up the search using standard MEM1 and (on Wii) MEM2 mappings in virtual "
"address space. This will work for the vast majority of games."));
label_standard_address_space->setWordWrap(true);

auto* custom_address_space_layout = new QVBoxLayout();
custom_address_space_layout->setMargin(6);
auto* custom_address_space_button_group = new QButtonGroup();
m_custom_virtual_address_space = new QRadioButton(tr("Use virtual addresses when possible"));
m_custom_virtual_address_space->setChecked(true);
m_custom_physical_address_space = new QRadioButton(tr("Use physical addresses"));
m_custom_effective_address_space =
new QRadioButton(tr("Use memory mapper configuration at time of scan"));
custom_address_space_button_group->addButton(m_custom_virtual_address_space);
custom_address_space_button_group->addButton(m_custom_physical_address_space);
custom_address_space_button_group->addButton(m_custom_effective_address_space);
custom_address_space_layout->addWidget(m_custom_virtual_address_space);
custom_address_space_layout->addWidget(m_custom_physical_address_space);
custom_address_space_layout->addWidget(m_custom_effective_address_space);

QLabel* label_range_start = new QLabel(tr("Range Start: "));
m_custom_address_start = new QLineEdit(QStringLiteral("0x80000000"));
QLabel* label_range_end = new QLabel(tr("Range End: "));
m_custom_address_end = new QLineEdit(QStringLiteral("0x81800000"));
custom_address_space_layout->addWidget(label_range_start);
custom_address_space_layout->addWidget(m_custom_address_start);
custom_address_space_layout->addWidget(label_range_end);
custom_address_space_layout->addWidget(m_custom_address_end);

address_space_layout->addWidget(m_standard_address_space);
address_space_layout->addWidget(label_standard_address_space);
address_space_layout->addWidget(m_custom_address_space);
address_space_layout->addLayout(custom_address_space_layout);

layout->addWidget(address_space_group);

auto* data_type_group = new QGroupBox(tr("Data Type"));
auto* data_type_layout = new QVBoxLayout();
data_type_group->setLayout(data_type_layout);

m_data_type_dropdown = new QComboBox();
m_data_type_dropdown->addItem(tr("8-bit Unsigned Integer"),
QVariant::fromValue(Cheats::DataType::U8));
m_data_type_dropdown->addItem(tr("16-bit Unsigned Integer"),
QVariant::fromValue(Cheats::DataType::U16));
m_data_type_dropdown->addItem(tr("32-bit Unsigned Integer"),
QVariant::fromValue(Cheats::DataType::U32));
m_data_type_dropdown->addItem(tr("64-bit Unsigned Integer"),
QVariant::fromValue(Cheats::DataType::U64));
m_data_type_dropdown->addItem(tr("8-bit Signed Integer"),
QVariant::fromValue(Cheats::DataType::S8));
m_data_type_dropdown->addItem(tr("16-bit Signed Integer"),
QVariant::fromValue(Cheats::DataType::S16));
m_data_type_dropdown->addItem(tr("32-bit Signed Integer"),
QVariant::fromValue(Cheats::DataType::S32));
m_data_type_dropdown->addItem(tr("64-bit Signed Integer"),
QVariant::fromValue(Cheats::DataType::S64));
m_data_type_dropdown->addItem(tr("32-bit Float"), QVariant::fromValue(Cheats::DataType::F32));
m_data_type_dropdown->addItem(tr("64-bit Float"), QVariant::fromValue(Cheats::DataType::F64));
m_data_type_dropdown->setCurrentIndex(6); // select 32bit signed int by default

data_type_layout->addWidget(m_data_type_dropdown);

m_data_type_aligned = new QCheckBox(tr("Aligned to data type length"));
m_data_type_aligned->setChecked(true);

data_type_layout->addWidget(m_data_type_aligned);

layout->addWidget(data_type_group);

m_new_search = new QPushButton(tr("New Search"));
layout->addWidget(m_new_search);

setLayout(layout);
}

void CheatSearchFactoryWidget::ConnectWidgets()
{
connect(m_new_search, &QPushButton::clicked, this, &CheatSearchFactoryWidget::OnNewSearchClicked);
connect(m_standard_address_space, &QPushButton::toggled, this,
&CheatSearchFactoryWidget::OnAddressSpaceRadioChanged);
connect(m_custom_address_space, &QRadioButton::toggled, this,
&CheatSearchFactoryWidget::OnAddressSpaceRadioChanged);
}

void CheatSearchFactoryWidget::RefreshGui()
{
bool enable_custom = m_custom_address_space->isChecked();
m_custom_virtual_address_space->setEnabled(enable_custom);
m_custom_physical_address_space->setEnabled(enable_custom);
m_custom_effective_address_space->setEnabled(enable_custom);
m_custom_address_start->setEnabled(enable_custom);
m_custom_address_end->setEnabled(enable_custom);
}

void CheatSearchFactoryWidget::OnAddressSpaceRadioChanged()
{
RefreshGui();
}

void CheatSearchFactoryWidget::OnNewSearchClicked()
{
std::vector<Cheats::MemoryRange> memory_ranges;
PowerPC::RequestedAddressSpace address_space;
if (m_standard_address_space->isChecked())
{
memory_ranges.emplace_back(0x80000000, Memory::GetRamSizeReal());
if (SConfig::GetInstance().bWii)
memory_ranges.emplace_back(0x90000000, Memory::GetExRamSizeReal());
address_space = PowerPC::RequestedAddressSpace::Virtual;
}
else
{
const std::string address_start_str = m_custom_address_start->text().toStdString();
const std::string address_end_str = m_custom_address_end->text().toStdString();

u64 address_start;
u64 address_end;
if (!TryParse(address_start_str, &address_start) || !TryParse(address_end_str, &address_end))
return;
if (address_end <= address_start || address_end > 0x1'0000'0000)
return;

memory_ranges.emplace_back(static_cast<u32>(address_start), address_end - address_start);

if (m_custom_virtual_address_space->isChecked())
address_space = PowerPC::RequestedAddressSpace::Virtual;
else if (m_custom_physical_address_space->isChecked())
address_space = PowerPC::RequestedAddressSpace::Physical;
else
address_space = PowerPC::RequestedAddressSpace::Effective;
}

bool aligned = m_data_type_aligned->isChecked();
auto data_type = m_data_type_dropdown->currentData().value<Cheats::DataType>();
auto session = Cheats::MakeSession(std::move(memory_ranges), address_space, aligned, data_type);
if (session)
emit NewSessionCreated(*session);
}
51 changes: 51 additions & 0 deletions Source/Core/DolphinQt/CheatSearchFactoryWidget.h
@@ -0,0 +1,51 @@
// Copyright 2021 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later

#pragma once

#include <vector>

#include <QWidget>

#include "Core/CheatSearch.h"

class QCheckBox;
class QComboBox;
class QLineEdit;
class QPushButton;
class QRadioButton;

class CheatSearchFactoryWidget : public QWidget
{
Q_OBJECT
public:
explicit CheatSearchFactoryWidget();
~CheatSearchFactoryWidget() override;

signals:
void NewSessionCreated(const Cheats::CheatSearchSessionBase& session);

private:
void CreateWidgets();
void ConnectWidgets();

void RefreshGui();

void OnAddressSpaceRadioChanged();
void OnNewSearchClicked();

QRadioButton* m_standard_address_space;
QRadioButton* m_custom_address_space;

QRadioButton* m_custom_virtual_address_space;
QRadioButton* m_custom_physical_address_space;
QRadioButton* m_custom_effective_address_space;

QLineEdit* m_custom_address_start;
QLineEdit* m_custom_address_end;

QComboBox* m_data_type_dropdown;
QCheckBox* m_data_type_aligned;

QPushButton* m_new_search;
};
529 changes: 529 additions & 0 deletions Source/Core/DolphinQt/CheatSearchWidget.cpp

Large diffs are not rendered by default.

80 changes: 80 additions & 0 deletions Source/Core/DolphinQt/CheatSearchWidget.h
@@ -0,0 +1,80 @@
// Copyright 2021 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later

#pragma once

#include <QWidget>

#include <functional>
#include <optional>
#include <string>
#include <unordered_map>
#include <vector>

#include "Common/CommonTypes.h"
#include "Core/CheatSearch.h"

namespace ActionReplay
{
struct ARCode;
}

class QCheckBox;
class QComboBox;
class QLabel;
class QLineEdit;
class QPushButton;
class QTableWidget;
class QTableWidgetItem;

struct CheatSearchTableUserData
{
std::string m_description;
};

class CheatSearchWidget : public QWidget
{
Q_OBJECT
public:
explicit CheatSearchWidget(std::unique_ptr<Cheats::CheatSearchSessionBase> session);
~CheatSearchWidget() override;

signals:
void ActionReplayCodeGenerated(const ActionReplay::ARCode& ar_code);

private:
void CreateWidgets();
void ConnectWidgets();

void OnNextScanClicked();
void OnRefreshClicked();
void OnResetClicked();
void OnAddressTableItemChanged(QTableWidgetItem* item);
void OnAddressTableContextMenu();
void OnValueSourceChanged();
void OnHexCheckboxStateChanged();

bool RefreshValues();
void UpdateGuiTable();
void GenerateARCode();

std::unique_ptr<Cheats::CheatSearchSessionBase> m_session;

// storage for the 'Current Value' column's data
std::unordered_map<u32, std::string> m_address_table_current_values;

// storage for user-entered metadata such as text descriptions for memory addresses
// this is intentionally NOT cleared when updating values or resetting or similar
std::unordered_map<u32, CheatSearchTableUserData> m_address_table_user_data;

QComboBox* m_compare_type_dropdown;
QComboBox* m_value_source_dropdown;
QLineEdit* m_given_value_text;
QLabel* m_info_label_1;
QLabel* m_info_label_2;
QPushButton* m_next_scan_button;
QPushButton* m_refresh_values_button;
QPushButton* m_reset_button;
QCheckBox* m_display_values_in_hex_checkbox;
QTableWidget* m_address_table;
};
698 changes: 45 additions & 653 deletions Source/Core/DolphinQt/CheatsManager.cpp

Large diffs are not rendered by default.

62 changes: 11 additions & 51 deletions Source/Core/DolphinQt/CheatsManager.h
Expand Up @@ -5,36 +5,27 @@

#include <functional>
#include <memory>
#include <optional>
#include <vector>

#include <QDialog>

#include "Common/CommonTypes.h"
#include "DolphinQt/GameList/GameListModel.h"

#include "Core/CheatSearch.h"

class ARCodeWidget;
class QComboBox;
class GeckoCodeWidget;
class CheatSearchFactoryWidget;
class QDialogButtonBox;
class QLabel;
class QLineEdit;
class QPushButton;
class QRadioButton;
class QSplitter;
class QTabWidget;
class QTableWidget;
class QTableWidgetItem;
struct Result;
class PartiallyClosableTabWidget;

namespace Core
{
enum class State;
}

namespace UICommon
{
class GameFile;
}

class CheatsManager : public QDialog
{
Q_OBJECT
Expand All @@ -43,51 +34,20 @@ class CheatsManager : public QDialog
~CheatsManager();

private:
QWidget* CreateCheatSearch();
void CreateWidgets();
void ConnectWidgets();
void OnStateChanged(Core::State state);

size_t GetTypeSize() const;
std::function<bool(u32)> CreateMatchFunction();

void Reset();
void NewSearch();
void NextSearch();
void Update();
void GenerateARCode();

void OnWatchContextMenu();
void OnMatchContextMenu();
void OnWatchItemChanged(QTableWidgetItem* item);
void OnNewSessionCreated(const Cheats::CheatSearchSessionBase& session);
void OnTabCloseRequested(int index);

std::string m_game_id;
std::string m_game_tdb_id;
u16 m_revision = 0;

std::vector<Result> m_results;
std::vector<Result> m_watch;
QDialogButtonBox* m_button_box;
QTabWidget* m_tab_widget = nullptr;
PartiallyClosableTabWidget* m_tab_widget = nullptr;

QWidget* m_cheat_search;
ARCodeWidget* m_ar_code = nullptr;

QLabel* m_result_label;
QTableWidget* m_match_table;
QTableWidget* m_watch_table;
QSplitter* m_option_splitter;
QSplitter* m_table_splitter;
QComboBox* m_match_length;
QComboBox* m_match_operation;
QLineEdit* m_match_value;
QPushButton* m_match_new;
QPushButton* m_match_next;
QPushButton* m_match_refresh;
QPushButton* m_match_reset;

QRadioButton* m_match_decimal;
QRadioButton* m_match_hexadecimal;
QRadioButton* m_match_octal;
bool m_updating = false;
GeckoCodeWidget* m_gecko_code = nullptr;
CheatSearchFactoryWidget* m_cheat_search_new = nullptr;
};
6 changes: 6 additions & 0 deletions Source/Core/DolphinQt/DolphinQt.vcxproj
Expand Up @@ -45,6 +45,8 @@
</ItemDefinitionGroup>
<ItemGroup>
<ClCompile Include="AboutDialog.cpp" />
<ClCompile Include="CheatSearchFactoryWidget.cpp" />
<ClCompile Include="CheatSearchWidget.cpp" />
<ClCompile Include="CheatsManager.cpp" />
<ClCompile Include="Config\ARCodeWidget.cpp" />
<ClCompile Include="Config\CheatCodeEditor.cpp" />
Expand Down Expand Up @@ -168,6 +170,7 @@
<ClCompile Include="QtUtils\FlowLayout.cpp" />
<ClCompile Include="QtUtils\ImageConverter.cpp" />
<ClCompile Include="QtUtils\ModalMessageBox.cpp" />
<ClCompile Include="QtUtils\PartiallyClosableTabWidget.cpp" />
<ClCompile Include="QtUtils\UTF8CodePointCountValidator.cpp" />
<ClCompile Include="QtUtils\WindowActivationEventFilter.cpp" />
<ClCompile Include="QtUtils\WinIconHelper.cpp" />
Expand Down Expand Up @@ -227,6 +230,8 @@
<ClInclude Include="Translation.h" />
<ClInclude Include="WiiUpdate.h" />
<QtMoc Include="AboutDialog.h" />
<QtMoc Include="CheatSearchFactoryWidget.h" />
<QtMoc Include="CheatSearchWidget.h" />
<QtMoc Include="CheatsManager.h" />
<QtMoc Include="Config\ARCodeWidget.h" />
<QtMoc Include="Config\CheatWarningWidget.h" />
Expand Down Expand Up @@ -340,6 +345,7 @@
<QtMoc Include="QtUtils\ElidedButton.h" />
<QtMoc Include="QtUtils\FileOpenEventFilter.h" />
<QtMoc Include="QtUtils\ParallelProgressDialog.h" />
<QtMoc Include="QtUtils\PartiallyClosableTabWidget.h" />
<QtMoc Include="QtUtils\UTF8CodePointCountValidator.h" />
<QtMoc Include="QtUtils\WindowActivationEventFilter.h" />
<QtMoc Include="RenderWidget.h" />
Expand Down
19 changes: 19 additions & 0 deletions Source/Core/DolphinQt/QtUtils/PartiallyClosableTabWidget.cpp
@@ -0,0 +1,19 @@
// Copyright 2021 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later

#include "DolphinQt/QtUtils/PartiallyClosableTabWidget.h"

#include <QStyle>
#include <QTabBar>

PartiallyClosableTabWidget::PartiallyClosableTabWidget(QWidget* parent) : QTabWidget(parent)
{
setTabsClosable(true);
}

void PartiallyClosableTabWidget::setTabUnclosable(int index)
{
QTabBar::ButtonPosition closeSide = (QTabBar::ButtonPosition)style()->styleHint(
QStyle::SH_TabBar_CloseButtonPosition, nullptr, this);
tabBar()->setTabButton(index, closeSide, nullptr);
}
17 changes: 17 additions & 0 deletions Source/Core/DolphinQt/QtUtils/PartiallyClosableTabWidget.h
@@ -0,0 +1,17 @@
// Copyright 2021 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later

#pragma once

#include <QTabWidget>

class QEvent;

class PartiallyClosableTabWidget : public QTabWidget
{
Q_OBJECT
public:
PartiallyClosableTabWidget(QWidget* parent = nullptr);

void setTabUnclosable(int index);
};