// Copyright 2018 Dolphin Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #include "DolphinQt/Debugger/JITWidget.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "Common/CommonFuncs.h" #include "Common/GekkoDisassembler.h" #include "Core/Core.h" #include "Core/PowerPC/JitCommon/JitCache.h" #include "Core/PowerPC/JitInterface.h" #include "Core/PowerPC/MMU.h" #include "Core/PowerPC/PPCSymbolDB.h" #include "Core/System.h" #include "DolphinQt/Debugger/JitBlockTableModel.h" #include "DolphinQt/Host.h" #include "DolphinQt/QtUtils/ClickableStatusBar.h" #include "DolphinQt/QtUtils/FromStdString.h" #include "DolphinQt/QtUtils/ModalMessageBox.h" #include "DolphinQt/Settings.h" #include "UICommon/UICommon.h" class JitBlockProxyModel final : public QSortFilterProxyModel { friend JITWidget; public: explicit JitBlockProxyModel(QObject* parent = nullptr); ~JitBlockProxyModel() override; JitBlockProxyModel(const JitBlockProxyModel&) = delete; JitBlockProxyModel(JitBlockProxyModel&&) = delete; JitBlockProxyModel& operator=(const JitBlockProxyModel&) = delete; JitBlockProxyModel& operator=(JitBlockProxyModel&&) = delete; bool filterAcceptsRow(int source_row, const QModelIndex& source_parent) const override; [[noreturn]] void setSourceModel(QAbstractItemModel* source_model) override; void setSourceModel(JitBlockTableModel* source_model); JitBlockTableModel* sourceModel() const; const JitBlock& GetJitBlock(const QModelIndex& index); // Always connected slots (external signals) void OnSymbolTextChanged(const QString& text); template JitBlockProxyModel::*member> void OnAddressTextChanged(const QString& text); private: std::optional m_em_address_min, m_em_address_max, m_pm_address_covered; QString m_symbol_name = {}; }; const JitBlock& JitBlockProxyModel::GetJitBlock(const QModelIndex& index) { return sourceModel()->GetJitBlock(mapToSource(index)); } void JitBlockProxyModel::OnSymbolTextChanged(const QString& text) { m_symbol_name = text; invalidateRowsFilter(); } template JitBlockProxyModel::*member> void JitBlockProxyModel::OnAddressTextChanged(const QString& text) { bool ok = false; if (const u32 value = text.toUInt(&ok, 16); ok) this->*member = value; else this->*member = std::nullopt; invalidateRowsFilter(); } bool JitBlockProxyModel::filterAcceptsRow(int source_row, const QModelIndex& source_parent) const { if (source_parent.isValid()) [[unlikely]] return false; if (!m_symbol_name.isEmpty()) { if (const QVariant& symbol_name_v = *sourceModel()->GetSymbolList()[source_row]; !symbol_name_v.isValid() || !static_cast(symbol_name_v.data()) ->contains(m_symbol_name, Qt::CaseInsensitive)) { return false; } } const JitBlock& block = sourceModel()->GetJitBlockRefs()[source_row]; if (m_em_address_min.has_value()) { if (block.effectiveAddress < m_em_address_min.value()) return false; } if (m_em_address_max.has_value()) { if (block.effectiveAddress > m_em_address_max.value()) return false; } if (m_pm_address_covered.has_value()) { if (!block.physical_addresses.contains(m_pm_address_covered.value())) return false; } return true; } // Virtual setSourceModel is forbidden for type-safety reasons. void JitBlockProxyModel::setSourceModel(QAbstractItemModel* source_model) { Crash(); } void JitBlockProxyModel::setSourceModel(JitBlockTableModel* source_model) { QSortFilterProxyModel::setSourceModel(source_model); } JitBlockTableModel* JitBlockProxyModel::sourceModel() const { return static_cast(QSortFilterProxyModel::sourceModel()); } JitBlockProxyModel::JitBlockProxyModel(QObject* parent) : QSortFilterProxyModel(parent) { } JitBlockProxyModel::~JitBlockProxyModel() = default; void JITWidget::UpdateProfilingButton() { const QSignalBlocker blocker(m_toggle_profiling_button); const bool enabled = Config::Get(Config::MAIN_DEBUG_JIT_ENABLE_PROFILING); m_toggle_profiling_button->setText(enabled ? tr("Stop Profiling") : tr("Start Profiling")); m_toggle_profiling_button->setChecked(enabled); } void JITWidget::UpdateOtherButtons(Core::State state) { const bool jit_exists = m_system.GetJitInterface().GetCore() != nullptr; m_clear_cache_button->setEnabled(jit_exists); m_wipe_profiling_button->setEnabled(jit_exists); } void JITWidget::UpdateDebugFont(const QFont& font) { m_table_view->setFont(font); m_ppc_asm_widget->setFont(font); m_host_near_asm_widget->setFont(font); m_host_far_asm_widget->setFont(font); } void JITWidget::ClearDisassembly() { m_ppc_asm_widget->clear(); m_host_near_asm_widget->clear(); m_host_far_asm_widget->clear(); m_status_bar->clearMessage(); } void JITWidget::ShowFreeMemoryStatus() { const std::vector memory_stats = m_system.GetJitInterface().GetMemoryStats(); QString message = tr("Free memory:"); for (const auto& [name, stats] : memory_stats) { const auto& [free_size, fragmentation_ratio] = stats; // i18n: Of each memory region, %1 is its remaining size displayed in an appropriate scale // of bytes (e.g. MiB), %2 is its untranslated name, and %3 is its fragmentation percentage. message.append(tr(" %1 %2 (%3% fragmented)") .arg(QString::fromStdString(UICommon::FormatSize(free_size, 2))) .arg(QtUtils::FromStdString(name)) .arg(fragmentation_ratio * 100.0, 0, 'f', 2)); } m_status_bar->showMessage(message); } void JITWidget::UpdateContent(Core::State state) { ClearDisassembly(); if (state == Core::State::Paused) ShowFreeMemoryStatus(); } static void DisassembleCodeBuffer(const JitBlock& block, PPCSymbolDB& ppc_symbol_db, std::ostream& stream) { // Instructions are 4 byte aligned, so next_address = 1 will never produce a false-negative. for (u32 next_address = 1; const auto& [address, inst] : block.original_buffer) { if (address != next_address) { stream << ppc_symbol_db.GetDescription(address) << '\n'; next_address = address; } fmt::print(stream, "0x{:08x}\t{}\n", address, Common::GekkoDisassembler::Disassemble(inst.hex, address)); next_address += sizeof(UGeckoInstruction); } } void JITWidget::CrossDisassemble(const JitBlock& block) { // TODO C++20: std::ostringstream::view() + QtUtils::FromStdString + std::ostream::seekp(0) would // save a lot of wasted allocation here, but compiler support for the first thing isn't here yet. std::ostringstream stream; DisassembleCodeBuffer(block, m_system.GetPPCSymbolDB(), stream); m_ppc_asm_widget->setPlainText(QString::fromStdString(std::move(stream).str())); auto& jit_interface = m_system.GetJitInterface(); const auto host_near_instruction_count = jit_interface.DisassembleNearCode(block, stream); m_host_near_asm_widget->setPlainText(QString::fromStdString(std::move(stream).str())); const auto host_far_instruction_count = jit_interface.DisassembleFarCode(block, stream); m_host_far_asm_widget->setPlainText(QString::fromStdString(std::move(stream).str())); // i18n: "near" and "far" refer to the near code cache and far code cache of Dolphin's JITs. // %1 and %2 are instruction counts from the near and far code caches, respectively. %3 is a // percentage calculated from how inefficient (in other words, "blown-up") a given JIT block's // recompilation was when considering the host instruction count vs the PPC instruction count. m_status_bar->showMessage(tr("Host instruction count: %1 near %2 far (%3% blowup)") .arg(host_near_instruction_count) .arg(host_far_instruction_count) .arg(static_cast(100 * (host_near_instruction_count + host_far_instruction_count)) / block.originalSize - 100.0, 0, 'f', 2)); } void JITWidget::CrossDisassemble(const QModelIndex& index) { if (index.isValid()) { CrossDisassemble(m_table_proxy->GetJitBlock(index)); return; } UpdateContent(Core::GetState(m_system)); } void JITWidget::CrossDisassemble() { CrossDisassemble(m_table_view->currentIndex()); } void JITWidget::TableEraseBlocks() { auto* const selection_model = m_table_view->selectionModel(); QModelIndexList index_list = selection_model->selectedRows(); selection_model->clear(); // Side effect: currentChanged will be emitted (this is intended). std::ranges::transform(index_list, index_list.begin(), [this](const QModelIndex& index) { return m_table_proxy->mapToSource(index); }); std::ranges::sort(index_list, std::less{}); // QModelIndex is incompatible with std::ranges::less for (const QModelIndex& index : std::ranges::reverse_view{index_list}) { if (!index.isValid()) continue; m_table_model->removeRow(index.row()); } } void JITWidget::LoadQSettings() { auto& settings = Settings::GetQSettings(); restoreGeometry(settings.value(QStringLiteral("jitwidget/geometry")).toByteArray()); setHidden(!Settings::Instance().IsJITVisible() || !Settings::Instance().IsDebugModeEnabled()); // macOS: setFloating() needs to be after setHidden() for proper window presentation // according to Settings setFloating(settings.value(QStringLiteral("jitwidget/floating")).toBool()); m_table_view->horizontalHeader()->restoreState( settings.value(QStringLiteral("jitwidget/tableheader/state")).toByteArray()); m_table_splitter->restoreState( settings.value(QStringLiteral("jitwidget/tablesplitter")).toByteArray()); m_disasm_splitter->restoreState( settings.value(QStringLiteral("jitwidget/disasmsplitter")).toByteArray()); } void JITWidget::SaveQSettings() const { auto& settings = Settings::GetQSettings(); settings.setValue(QStringLiteral("jitwidget/geometry"), saveGeometry()); settings.setValue(QStringLiteral("jitwidget/floating"), isFloating()); settings.setValue(QStringLiteral("jitwidget/tableheader/state"), m_table_view->horizontalHeader()->saveState()); settings.setValue(QStringLiteral("jitwidget/tablesplitter"), m_table_splitter->saveState()); settings.setValue(QStringLiteral("jitwidget/disasmsplitter"), m_disasm_splitter->saveState()); } void JITWidget::ConnectSlots() { auto* const host = Host::GetInstance(); connect(host, &Host::JitCacheInvalidation, this, &JITWidget::OnJitCacheInvalidation); connect(host, &Host::UpdateDisasmDialog, this, &JITWidget::OnUpdateDisasmDialog); connect(host, &Host::PPCSymbolsChanged, this, &JITWidget::OnPPCSymbolsUpdated); connect(host, &Host::PPCBreakpointsChanged, this, &JITWidget::OnPPCBreakpointsChanged); auto* const settings = &Settings::Instance(); connect(settings, &Settings::ConfigChanged, this, &JITWidget::OnConfigChanged); connect(settings, &Settings::DebugFontChanged, this, &JITWidget::OnDebugFontChanged); connect(settings, &Settings::EmulationStateChanged, this, &JITWidget::OnEmulationStateChanged); } void JITWidget::DisconnectSlots() { auto* const host = Host::GetInstance(); disconnect(host, &Host::JitCacheInvalidation, this, &JITWidget::OnJitCacheInvalidation); disconnect(host, &Host::UpdateDisasmDialog, this, &JITWidget::OnUpdateDisasmDialog); disconnect(host, &Host::PPCSymbolsChanged, this, &JITWidget::OnPPCSymbolsUpdated); disconnect(host, &Host::PPCBreakpointsChanged, this, &JITWidget::OnPPCBreakpointsChanged); auto* const settings = &Settings::Instance(); disconnect(settings, &Settings::ConfigChanged, this, &JITWidget::OnConfigChanged); disconnect(settings, &Settings::DebugFontChanged, this, &JITWidget::OnDebugFontChanged); disconnect(settings, &Settings::EmulationStateChanged, this, &JITWidget::OnEmulationStateChanged); } void JITWidget::Show() { ConnectSlots(); // Handle every slot that may have missed a signal while this widget was hidden. // OnJitCacheInvalidation() can be skipped. // OnUpdateDisasmDialog() can be skipped. // OnPPCSymbolsUpdated() can be skipped. // OnPPCBreakpointsChanged() can be skipped. OnConfigChanged(); OnDebugFontChanged(Settings::Instance().GetDebugFont()); OnEmulationStateChanged(Core::GetState(m_system)); } void JITWidget::Hide() { DisconnectSlots(); ClearDisassembly(); } void JITWidget::OnRequestPPCComparison(u32 address, bool translate_address) { Settings::Instance().SetJITVisible(true); raise(); if (translate_address) { const std::optional pm_address = m_system.GetMMU().GetTranslatedAddress(address); if (!pm_address.has_value()) { ModalMessageBox::warning( this, tr("Error"), tr("Effective address %1 has no physical address translation.").arg(address, 0, 16)); return; } address = pm_address.value(); } m_pm_address_covered_line_edit->setText(QString::number(address, 16)); } void JITWidget::OnVisibilityToggled(bool visible) { setHidden(!visible); } void JITWidget::OnDebugModeToggled(bool enabled) { setHidden(!enabled || !Settings::Instance().IsJITVisible()); } void JITWidget::OnToggleProfiling(bool enabled) { Config::SetBaseOrCurrent(Config::MAIN_DEBUG_JIT_ENABLE_PROFILING, enabled); } void JITWidget::OnClearCache() { m_system.GetJitInterface().ClearCache(Core::CPUThreadGuard{m_system}); } void JITWidget::OnWipeProfiling() { m_system.GetJitInterface().WipeBlockProfilingData(Core::CPUThreadGuard{m_system}); } void JITWidget::OnTableCurrentChanged(const QModelIndex& current, const QModelIndex& previous) { CrossDisassemble(current); } void JITWidget::OnTableDoubleClicked(const QModelIndex& index) { emit SetCodeAddress(m_table_proxy->GetJitBlock(index).effectiveAddress); } void JITWidget::OnTableContextMenu(const QPoint& pos) { // There needs to be an option somewhere for a user to recover from hiding every column. if (m_table_view->horizontalHeader()->hiddenSectionCount() == Column::NumberOfColumns) { m_column_visibility_menu->exec(m_table_view->viewport()->mapToGlobal(pos)); return; } m_table_context_menu->exec(m_table_view->viewport()->mapToGlobal(pos)); } void JITWidget::OnTableHeaderContextMenu(const QPoint& pos) { m_column_visibility_menu->exec(m_table_view->horizontalHeader()->mapToGlobal(pos)); } void JITWidget::OnTableMenuViewCode() { // TODO: CodeWidget doesn't support it yet, but eventually signal if the address should be // translated with ((block.feature_flags & CPUEmuFeatureFlags::FEATURE_FLAG_MSR_IR) != 0). if (const QModelIndex& index = m_table_view->currentIndex(); index.isValid()) emit SetCodeAddress(m_table_proxy->GetJitBlock(index).effectiveAddress); } void JITWidget::OnTableMenuEraseBlocks() { TableEraseBlocks(); // Side effect: currentChanged will be emitted (this is intended). // Because currentChanged has been emitted, OnTableCurrentChanged has already handled the rest. } void JITWidget::OnStatusBarPressed() { if (Core::GetState(m_system) == Core::State::Paused) ShowFreeMemoryStatus(); } void JITWidget::OnJitCacheInvalidation() { if (Core::GetState(m_system) != Core::State::Paused) return; ClearDisassembly(); ShowFreeMemoryStatus(); } void JITWidget::OnUpdateDisasmDialog() { if (Core::GetState(m_system) != Core::State::Paused) return; CrossDisassemble(); } void JITWidget::OnPPCSymbolsUpdated() { if (Core::GetState(m_system) != Core::State::Paused) return; CrossDisassemble(); } void JITWidget::OnPPCBreakpointsChanged() { // Whatever row(s) might have been selected could no longer exist, because adding or removing // breakpoints can invalidate JIT blocks. We must clear the selection to avoid stale indices. auto* const selection_model = m_table_view->selectionModel(); selection_model->clear(); // Side effect: currentChanged will be emitted (this is intended). // Because currentChanged has been emitted, OnTableCurrentChanged has already handled the rest. } void JITWidget::OnConfigChanged() { UpdateProfilingButton(); } void JITWidget::OnDebugFontChanged(const QFont& font) { UpdateDebugFont(font); } void JITWidget::OnEmulationStateChanged(Core::State state) { UpdateOtherButtons(state); UpdateContent(state); } void JITWidget::closeEvent(QCloseEvent*) { Settings::Instance().SetJITVisible(false); } void JITWidget::showEvent(QShowEvent*) { emit ShowSignal(); Show(); } void JITWidget::hideEvent(QHideEvent*) { emit HideSignal(); Hide(); } JITWidget::JITWidget(Core::System& system, QWidget* parent) : QDockWidget(parent), m_system(system) { setWindowTitle(tr("JIT Blocks")); setObjectName(QStringLiteral("jitwidget")); setAllowedAreas(Qt::AllDockWidgetAreas); auto* const settings = &Settings::Instance(); connect(settings, &Settings::JITVisibilityChanged, this, &JITWidget::OnVisibilityToggled); connect(settings, &Settings::DebugModeToggled, this, &JITWidget::OnDebugModeToggled); m_table_view = new QTableView(nullptr); m_table_proxy = new JitBlockProxyModel(m_table_view); m_table_model = new JitBlockTableModel(m_system, m_system.GetJitInterface(), m_system.GetPPCSymbolDB(), m_table_proxy); connect(this, &JITWidget::HideSignal, m_table_model, &JitBlockTableModel::OnHideSignal); connect(this, &JITWidget::ShowSignal, m_table_model, &JitBlockTableModel::OnShowSignal); m_table_proxy->setSourceModel(m_table_model); m_table_proxy->setSortRole(UserRole::SortRole); m_table_proxy->setSortCaseSensitivity(Qt::CaseInsensitive); m_table_view->setModel(m_table_proxy); m_table_view->setSortingEnabled(true); m_table_view->sortByColumn(Column::EffectiveAddress, Qt::AscendingOrder); m_table_view->setSelectionMode(QAbstractItemView::ExtendedSelection); m_table_view->setSelectionBehavior(QAbstractItemView::SelectRows); m_table_view->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); m_table_view->setContextMenuPolicy(Qt::CustomContextMenu); m_table_view->setEditTriggers(QAbstractItemView::NoEditTriggers); m_table_view->setCornerButtonEnabled(false); m_table_view->verticalHeader()->hide(); connect(m_table_view, &QTableView::doubleClicked, this, &JITWidget::OnTableDoubleClicked); connect(m_table_view, &QTableView::customContextMenuRequested, this, &JITWidget::OnTableContextMenu); auto* const horizontal_header = m_table_view->horizontalHeader(); horizontal_header->setContextMenuPolicy(Qt::CustomContextMenu); horizontal_header->setStretchLastSection(true); horizontal_header->setSectionsMovable(true); horizontal_header->setFirstSectionMovable(true); connect(horizontal_header, &QHeaderView::sortIndicatorChanged, m_table_model, &JitBlockTableModel::OnSortIndicatorChanged); connect(horizontal_header, &QHeaderView::customContextMenuRequested, this, &JITWidget::OnTableHeaderContextMenu); auto* const selection_model = m_table_view->selectionModel(); connect(selection_model, &QItemSelectionModel::currentChanged, this, &JITWidget::OnTableCurrentChanged); auto* const controls_layout = new QHBoxLayout(nullptr); const auto address_filter_routine = [&](QLineEdit* line_edit, const QString& placeholder_text, void (JitBlockProxyModel::*slot)(const QString&)) { line_edit->setPlaceholderText(placeholder_text); connect(line_edit, &QLineEdit::textChanged, m_table_proxy, slot); controls_layout->addWidget(line_edit); }; address_filter_routine( new QLineEdit(nullptr), tr("Min Effective Address"), &JitBlockProxyModel::OnAddressTextChanged<&JitBlockProxyModel::m_em_address_min>); address_filter_routine( new QLineEdit(nullptr), tr("Max Effective Address"), &JitBlockProxyModel::OnAddressTextChanged<&JitBlockProxyModel::m_em_address_max>); address_filter_routine( m_pm_address_covered_line_edit = new QLineEdit(nullptr), tr("Recompiles Physical Address"), &JitBlockProxyModel::OnAddressTextChanged<&JitBlockProxyModel::m_pm_address_covered>); auto* const symbol_name_line_edit = new QLineEdit(nullptr); symbol_name_line_edit->setPlaceholderText(tr("Symbol Name")); connect(symbol_name_line_edit, &QLineEdit::textChanged, m_table_model, &JitBlockTableModel::OnFilterSymbolTextChanged); connect(symbol_name_line_edit, &QLineEdit::textChanged, m_table_proxy, &JitBlockProxyModel::OnSymbolTextChanged); controls_layout->addWidget(symbol_name_line_edit); m_toggle_profiling_button = new QPushButton(nullptr); m_toggle_profiling_button->setToolTip( tr("Toggle software JIT block profiling (will clear the JIT cache).")); m_toggle_profiling_button->setCheckable(true); connect(m_toggle_profiling_button, &QPushButton::toggled, this, &JITWidget::OnToggleProfiling); controls_layout->addWidget(m_toggle_profiling_button); m_clear_cache_button = new QPushButton(tr("Clear Cache"), nullptr); connect(m_clear_cache_button, &QPushButton::clicked, this, &JITWidget::OnClearCache); controls_layout->addWidget(m_clear_cache_button); m_wipe_profiling_button = new QPushButton(tr("Wipe Profiling"), nullptr); m_wipe_profiling_button->setToolTip(tr("Re-initialize software JIT block profiling data.")); connect(m_wipe_profiling_button, &QPushButton::clicked, this, &JITWidget::OnWipeProfiling); controls_layout->addWidget(m_wipe_profiling_button); m_disasm_splitter = new QSplitter(Qt::Horizontal, nullptr); const auto text_box_routine = [&](QPlainTextEdit* text_edit, const QString& placeholder_text) { text_edit->setWordWrapMode(QTextOption::NoWrap); text_edit->setPlaceholderText(placeholder_text); text_edit->setReadOnly(true); m_disasm_splitter->addWidget(text_edit); }; text_box_routine(m_ppc_asm_widget = new QPlainTextEdit(nullptr), tr("PPC Instruction Coverage")); text_box_routine(m_host_near_asm_widget = new QPlainTextEdit(nullptr), tr("Host Near Code Cache")); text_box_routine(m_host_far_asm_widget = new QPlainTextEdit(nullptr), tr("Host Far Code Cache")); m_table_splitter = new QSplitter(Qt::Vertical, nullptr); m_table_splitter->addWidget(m_table_view); m_table_splitter->addWidget(m_disasm_splitter); m_status_bar = new ClickableStatusBar(nullptr); m_status_bar->setSizeGripEnabled(false); connect(m_status_bar, &ClickableStatusBar::pressed, this, &JITWidget::OnStatusBarPressed); m_table_context_menu = new QMenu(this); m_table_context_menu->addAction(tr("View &Code"), this, &JITWidget::OnTableMenuViewCode); m_table_context_menu->addAction(tr("&Erase Block(s)"), this, &JITWidget::OnTableMenuEraseBlocks); LoadQSettings(); m_column_visibility_menu = new QMenu(this); // These table header display names have abbreviated counterparts in JitBlockTableModel.cpp static constexpr std::array headers = { QT_TR_NOOP("PPC Feature Flags"), // i18n: "Effective" means this memory address might be translated within the MMU. QT_TR_NOOP("Effective Address"), QT_TR_NOOP("Code Buffer Size"), // i18n: This means to say it is a count of PPC instructions recompiled more than once. QT_TR_NOOP("Repeat Instructions"), // i18n: "Near Code" refers to the near code cache of Dolphin's JITs. QT_TR_NOOP("Host Near Code Size"), // i18n: "Far Code" refers to the far code cache of Dolphin's JITs. QT_TR_NOOP("Host Far Code Size"), QT_TR_NOOP("Run Count"), // i18n: "Cycles" means instruction cycles. QT_TR_NOOP("Cycles Spent"), // i18n: "Cycles" means instruction cycles. QT_TR_NOOP("Cycles Average"), // i18n: "Cycles" means instruction cycles. QT_TR_NOOP("Cycles Percent"), // i18n: "ns" is an abbreviation of nanoseconds. QT_TR_NOOP("Time Spent (ns)"), // i18n: "ns" is an abbreviation of nanoseconds. QT_TR_NOOP("Time Average (ns)"), QT_TR_NOOP("Time Percent"), // i18n: "Symbol" means debugging symbol (its name in particular). QT_TR_NOOP("Symbol"), }; for (int column = 0; column < Column::NumberOfColumns; ++column) { auto* const action = m_column_visibility_menu->addAction(tr(headers[column]), [this, column](bool enabled) { m_table_view->setColumnHidden(column, !enabled); }); action->setChecked(!m_table_view->isColumnHidden(column)); action->setCheckable(true); } auto* const main_layout = new QVBoxLayout(nullptr); main_layout->setContentsMargins(2, 2, 2, 2); main_layout->setSpacing(0); main_layout->addLayout(controls_layout); main_layout->addWidget(m_table_splitter); main_layout->addWidget(m_status_bar); auto* const main_widget = new QWidget(nullptr); main_widget->setLayout(main_layout); setWidget(main_widget); } JITWidget::~JITWidget() { SaveQSettings(); }