| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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; | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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; | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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); | ||
| }; |