// Copyright 2026 Dolphin Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #include "DolphinQt/Config/ElementsGroupOverrideAddEditDialog.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "DolphinQt/Config/TextureHashBrowserDialog.h" namespace { constexpr double UPM_OVERRIDE_MIN = 0.01; constexpr double UPM_OVERRIDE_MAX = 1000.0; constexpr double UPM_OVERRIDE_STEP = 0.01; constexpr int MAX_VISIBLE_TEXTURE_HASH_ROWS = 10; constexpr int TEXTURE_HASH_ROW_HEIGHT = 28; QString ToHashHex(u64 hash) { return QStringLiteral("%1").arg(static_cast(hash), 16, 16, QLatin1Char('0')); } QString FormatRuntimeElementSummary(const ShaderHunter::RuntimeElementSignature& sig) { if (!sig.valid) return QObject::tr("No runtime element captured."); QStringList parts; parts << QObject::tr("Projection: %1") .arg(sig.perspective ? QObject::tr("Perspective") : QObject::tr("Orthographic")); if (sig.perspective) { parts << QObject::tr("FOV: %1 / %2") .arg(sig.perspective_hfov_x100 / 100.0, 0, 'f', 2) .arg(sig.perspective_vfov_x100 / 100.0, 0, 'f', 2); parts << QObject::tr("Near/Far: %1 / %2") .arg(sig.perspective_near_x1000 / 1000.0, 0, 'f', 3) .arg(sig.perspective_far_x100 / 100.0, 0, 'f', 2); } else { parts << QObject::tr("Ortho: L%1 R%2 T%3 B%4") .arg(sig.ortho_left_x100 / 100.0, 0, 'f', 2) .arg(sig.ortho_right_x100 / 100.0, 0, 'f', 2) .arg(sig.ortho_top_x100 / 100.0, 0, 'f', 2) .arg(sig.ortho_bottom_x100 / 100.0, 0, 'f', 2); parts << QObject::tr("Layer: %1").arg(sig.ortho_layer); } parts << QObject::tr("Viewport: %1,%2 %3x%4") .arg(sig.viewport_x) .arg(sig.viewport_y) .arg(sig.viewport_width) .arg(sig.viewport_height); parts << QObject::tr("Scissor: %1,%2 %3,%4") .arg(sig.scissor_left) .arg(sig.scissor_top) .arg(sig.scissor_right) .arg(sig.scissor_bottom); parts << QObject::tr("State: alpha=%1 ztest=%2 zupdate=%3 zfunc=%4 blend=%5/%6") .arg(QStringLiteral("0x%1").arg(sig.alpha_test_hex, 8, 16, QLatin1Char('0'))) .arg(sig.ztest ? QObject::tr("on") : QObject::tr("off")) .arg(sig.zupdate ? QObject::tr("on") : QObject::tr("off")) .arg(sig.zfunc) .arg(sig.blend_color_update ? QObject::tr("col") : QObject::tr("-")) .arg(sig.blend_alpha_update ? QObject::tr("alpha") : QObject::tr("-")); return parts.join(QStringLiteral("\n")); } QString FormatTextureSummary(const std::vector& textures) { if (textures.empty()) return QObject::tr("No textures"); QStringList parts; const int shown = std::min(2, static_cast(textures.size())); for (int i = 0; i < shown; ++i) { parts << QStringLiteral("%1") .arg(static_cast(textures[static_cast(i)]), 16, 16, QLatin1Char('0')); } if (static_cast(textures.size()) > shown) parts << QObject::tr("+%1 more").arg(static_cast(textures.size()) - shown); return parts.join(QStringLiteral(", ")); } bool SelectedSubgroupSignaturesEqual(const ElementsGroupManager::SelectedSubgroupSignature& lhs, const ElementsGroupManager::SelectedSubgroupSignature& rhs) { return lhs.vs_family == rhs.vs_family && lhs.ps_family == rhs.ps_family && lhs.gs_family == rhs.gs_family && lhs.texture_hashes == rhs.texture_hashes; } QString FormatSelectedMatchSummary(const ElementsGroupManager::SelectedSubgroupSignature& sig) { QString label = QObject::tr("F %1/%2/%3") .arg(QStringLiteral("%1") .arg(static_cast(sig.vs_family), 16, 8, QLatin1Char('0'))) .arg(QStringLiteral("%1") .arg(static_cast(sig.ps_family), 16, 8, QLatin1Char('0'))) .arg(QStringLiteral("%1") .arg(static_cast(sig.gs_family), 16, 8, QLatin1Char('0'))); label += QObject::tr(" | Tex %1").arg(FormatTextureSummary(sig.texture_hashes)); if (sig.family_version < ShaderHunter::FAMILY_SCHEME_VERSION) label += QObject::tr(" [legacy - re-capture to survive shader updates]"); return label; } std::vector CollectTextureBrowserEntries( const std::vector& draws) { std::vector entries; std::unordered_map indices; for (const auto& draw : draws) { for (size_t i = 0; i < draw.textures.size(); ++i) { const u64 hash = draw.textures[i]; if (hash == 0) continue; const auto it = indices.find(hash); if (it == indices.end()) { indices.emplace(hash, entries.size()); entries.push_back(TextureHashBrowserEntry{.hash = hash, .name = draw.texture_names[i]}); } else if (entries[it->second].name.empty() && !draw.texture_names[i].empty()) { entries[it->second].name = draw.texture_names[i]; } } } std::sort(entries.begin(), entries.end(), [](const TextureHashBrowserEntry& a, const TextureHashBrowserEntry& b) { return a.hash < b.hash; }); return entries; } } // namespace ElementsGroupOverrideAddEditDialog::ElementsGroupOverrideAddEditDialog( QWidget* parent, const ElementsGroupManager::ElementGroupOverride* edit_override, const std::vector& available_flags) : QDialog(parent), m_available_flags(available_flags) { setWindowTitle(edit_override ? tr("Edit Elements Group Override") : tr("Add Elements Group Override")); setMinimumWidth(420); m_name_edit = new QLineEdit; m_name_edit->setPlaceholderText(tr("Override name...")); m_comments_edit = new QPlainTextEdit; m_comments_edit->setPlaceholderText(tr("Optional notes...")); m_comments_edit->setTabChangesFocus(true); m_comments_edit->setMinimumHeight(70); m_credits_edit = new QLineEdit; m_credits_edit->setPlaceholderText(tr("Optional author/credits...")); m_match_kind_combo = new QComboBox; m_match_kind_combo->addItem(tr("Runtime Signature"), static_cast(ElementsGroupManager::MatchKind::RuntimeSignature)); m_match_kind_combo->addItem(tr("Profile Layer"), static_cast(ElementsGroupManager::MatchKind::ProfileLayer)); m_profile_label = new QLabel(tr("Profile:")); m_profile_combo = new QComboBox; for (MetroidElementProfile profile : GetMetroidElementProfiles()) { const std::string_view label = MetroidElementProfileToDisplayName(profile); m_profile_combo->addItem(QString::fromUtf8(label.data(), static_cast(label.size())), static_cast(profile)); } m_profile_layers_label = new QLabel(tr("Profile Layers:")); m_profile_layers_list = new QListWidget; m_profile_layers_list->setMinimumHeight(160); for (MetroidElementLayer layer : GetMetroidElementLayers()) { if (layer == MetroidElementLayer::Unknown) continue; const std::string_view label = MetroidElementLayerToDisplayName(layer); auto* item = new QListWidgetItem( QString::fromUtf8(label.data(), static_cast(label.size()))); item->setFlags(item->flags() | Qt::ItemIsUserCheckable); item->setCheckState(Qt::Unchecked); item->setData(Qt::UserRole, static_cast(layer)); m_profile_layers_list->addItem(item); } m_handling_combo = new QComboBox; m_handling_combo->addItem(tr("Skip"), static_cast(ElementsGroupManager::HandlingType::Skip)); m_handling_combo->addItem(tr("Screen"), static_cast(ElementsGroupManager::HandlingType::Screen)); m_handling_combo->addItem(tr("Screen Pane"), static_cast(ElementsGroupManager::HandlingType::ScreenPane)); m_handling_combo->addItem(tr("Fullscreen"), static_cast(ElementsGroupManager::HandlingType::Fullscreen)); m_handling_combo->addItem(tr("Head Locked"), static_cast(ElementsGroupManager::HandlingType::HeadLocked)); m_handling_combo->addItem(tr("Flag"), static_cast(ElementsGroupManager::HandlingType::Flag)); m_handling_combo->addItem( tr("Units per Meter"), static_cast(ElementsGroupManager::HandlingType::UnitsPerMeter)); m_handling_combo->addItem( tr("Passthrough"), static_cast(ElementsGroupManager::HandlingType::Passthrough)); m_handling_combo->addItem( tr("Camera Anchor"), static_cast(ElementsGroupManager::HandlingType::CameraAnchor)); m_handling_combo->addItem( tr("Controller Anchor"), static_cast(ElementsGroupManager::HandlingType::ControllerAnchor)); m_preserve_stereo_efb_check = new QCheckBox(tr("Preserve Stereo EFB")); m_preserve_stereo_efb_check->setToolTip( tr("Preserve separate left- and right-eye EFB layers when this fullscreen draw uses a " "one-layer texture partially updated from a stereo EFB copy.\n" "Enable this only for effects that incorrectly duplicate the left eye into the right " "eye.")); m_screen_pane_depth_label = new QLabel(tr("Screen Pane Depth:")); m_screen_pane_depth_combo = new QComboBox; m_screen_pane_depth_combo->addItem( tr("Original Game Depth"), static_cast(ElementsGroupManager::ScreenPaneDepthMode::Game)); m_screen_pane_depth_combo->addItem( tr("Physical VR Depth"), static_cast(ElementsGroupManager::ScreenPaneDepthMode::VR)); m_screen_pane_depth_combo->addItem( tr("Flat"), static_cast(ElementsGroupManager::ScreenPaneDepthMode::Flat)); m_screen_pane_depth_combo->setToolTip( tr("Original Game Depth preserves the game's depth-buffer ordering between separate draws.\n" "Physical VR Depth keeps one shared Z anchor for the complete matched element, preserving\n" "its multipart depth composition while X/Y remain fixed to the original screen pane.\n" "Flat uses the world-fixed 2D screen plane at the pane's original X/Y position, with\n" "zero visual thickness while retaining game depth for stable ordering.")); m_element_depth_label = new QLabel(tr("Element Depth:")); m_element_depth_spin = new QDoubleSpinBox; m_element_depth_spin->setRange(-1.0, 1000.0); m_element_depth_spin->setDecimals(3); m_element_depth_spin->setSingleStep(0.1); m_element_depth_spin->setSpecialValueText(tr("Default")); m_element_depth_spin->setValue(-1.0); m_units_per_meter_label = new QLabel(tr("Units per Meter:")); m_units_per_meter_spin = new QDoubleSpinBox; m_units_per_meter_spin->setRange(0.0, UPM_OVERRIDE_MAX); m_units_per_meter_spin->setDecimals(2); m_units_per_meter_spin->setSingleStep(UPM_OVERRIDE_STEP); m_units_per_meter_spin->setSpecialValueText(tr("Default")); m_units_per_meter_spin->setValue(0.0); m_passthrough_opacity_label = new QLabel(tr("Opacity:")); m_passthrough_opacity_spin = new QDoubleSpinBox; m_passthrough_opacity_spin->setRange(0.0, 1.0); m_passthrough_opacity_spin->setDecimals(2); m_passthrough_opacity_spin->setSingleStep(0.05); m_passthrough_opacity_spin->setValue(0.0); m_passthrough_opacity_spin->setToolTip( tr("How opaque this element stays over the headset camera feed.\n" "0.00 = fully see-through (pure passthrough window).\n" "1.00 = fully opaque (no passthrough).\n" "Requires the VR Passthrough setting to be enabled.")); const auto make_anchor_offset_spin = []() { auto* spin = new QDoubleSpinBox; spin->setRange(-10.0, 10.0); spin->setDecimals(2); spin->setSingleStep(0.05); spin->setValue(0.0); spin->setSuffix(QStringLiteral(" m")); return spin; }; m_anchor_right_label = new QLabel(tr("Anchor Right:")); m_anchor_right_spin = make_anchor_offset_spin(); m_anchor_right_spin->setToolTip( tr("Sideways offset of the VR camera from the anchor element's origin, in meters.\n" "Positive = right in camera space.")); m_anchor_up_label = new QLabel(tr("Anchor Up:")); m_anchor_up_spin = make_anchor_offset_spin(); m_anchor_up_spin->setToolTip( tr("Height offset of the VR camera above the anchor element's origin, in meters.\n" "Use this when the element's matrix sits at the character's root instead of the head.")); m_anchor_forward_label = new QLabel(tr("Anchor Forward:")); m_anchor_forward_spin = make_anchor_offset_spin(); m_anchor_forward_spin->setToolTip( tr("Forward offset of the VR camera from the anchor element's origin, in meters.\n" "Positive = further ahead in the game camera's view direction.")); m_anchor_rotation_label = new QLabel(tr("Anchor Rotation:")); m_anchor_rotation_combo = new QComboBox; m_anchor_rotation_combo->addItem(tr("Off"), static_cast(ShaderHunter::AnchorRotationMode::Off)); m_anchor_rotation_combo->addItem(tr("Yaw Only"), static_cast(ShaderHunter::AnchorRotationMode::YawOnly)); m_anchor_rotation_combo->addItem(tr("Full"), static_cast(ShaderHunter::AnchorRotationMode::Full)); m_anchor_rotation_combo->setToolTip( tr("Rotate the VR camera with the anchor element.\n" "Off = camera keeps the game camera's orientation (position only).\n" "Yaw Only = follow the element's heading but keep the horizon level (comfortable).\n" "Full = follow the complete orientation including pitch and roll (intense).\n" "Head tracking still works on top in every mode.")); m_anchor_yaw_label = new QLabel(tr("Rotation Yaw Offset:")); m_anchor_yaw_spin = new QDoubleSpinBox; m_anchor_yaw_spin->setRange(-180.0, 180.0); m_anchor_yaw_spin->setDecimals(0); m_anchor_yaw_spin->setSingleStep(15.0); m_anchor_yaw_spin->setValue(0.0); m_anchor_yaw_spin->setSuffix(QStringLiteral("°")); m_anchor_yaw_spin->setToolTip( tr("Fixed heading correction for the rotation modes. Models differ in which way\n" "they face: if the anchored view comes up looking backward use 180, if it looks\n" "sideways use 90 or -90.")); m_anchor_upm_label = new QLabel(tr("Anchor Units per Meter:")); m_anchor_upm_spin = new QDoubleSpinBox; m_anchor_upm_spin->setRange(0.0, UPM_OVERRIDE_MAX); m_anchor_upm_spin->setDecimals(2); m_anchor_upm_spin->setSingleStep(UPM_OVERRIDE_STEP); m_anchor_upm_spin->setSpecialValueText(tr("Default")); m_anchor_upm_spin->setValue(0.0); m_anchor_upm_spin->setToolTip( tr("World scale to use while this anchor is active, so a first-person view can have a\n" "different scale than the game's global Units per Meter setting, which is left\n" "untouched. Default = keep the global value. The scale eases in when the anchor\n" "engages and back out when it is released.")); m_anchor_hide_check = new QCheckBox(tr("Hide Anchor Element")); m_anchor_hide_check->setChecked(true); m_anchor_hide_check->setToolTip( tr("Skip drawing the anchor element itself so its mesh (e.g. the character's head)\n" "does not block the first-person view. Other body parts need their own Skip\n" "overrides. Disabled while the Camera Anchor toggle in the VR pane is off.")); m_anchor_hand_label = new QLabel(tr("Controller:")); m_anchor_hand_combo = new QComboBox; m_anchor_hand_combo->addItem(tr("Right"), 1); m_anchor_hand_combo->addItem(tr("Left"), 0); m_anchor_hand_combo->setToolTip( tr("Which VR controller the element follows. The element's position is replaced by\n" "the controller's aim pose (plus the anchor offsets).\n" "Disabled while the Controller Anchor toggle in the VR pane is off.")); m_anchor_follow_rotation_check = new QCheckBox(tr("Follow Controller Rotation")); m_anchor_follow_rotation_check->setChecked(false); m_anchor_follow_rotation_check->setToolTip( tr("Rotate the element with the controller so it stays rigidly in hand (sword,\n" "cannon). Off = the element only moves with the controller and keeps its\n" "game orientation. The anchor offsets follow the controller's frame when on.")); const auto make_anchor_angle_spin = []() { auto* spin = new QDoubleSpinBox; spin->setRange(-180.0, 180.0); spin->setDecimals(0); spin->setSingleStep(15.0); spin->setValue(0.0); spin->setSuffix(QStringLiteral("°")); return spin; }; m_anchor_ctrl_yaw_label = new QLabel(tr("Model Yaw:")); m_anchor_ctrl_yaw_spin = make_anchor_angle_spin(); m_anchor_ctrl_pitch_label = new QLabel(tr("Model Pitch:")); m_anchor_ctrl_pitch_spin = make_anchor_angle_spin(); m_anchor_ctrl_roll_label = new QLabel(tr("Model Roll:")); m_anchor_ctrl_roll_spin = make_anchor_angle_spin(); const QString anchor_angle_tooltip = tr("Fixed correction for the model's native orientation, applied in the\n" "controller's frame (yaw, then pitch, then roll). Models differ in which way\n" "they point: if the element comes up backward use yaw 180, if it lies flat\n" "use pitch ±90, and roll spins it about the aim direction."); m_anchor_ctrl_yaw_spin->setToolTip(anchor_angle_tooltip); m_anchor_ctrl_pitch_spin->setToolTip(anchor_angle_tooltip); m_anchor_ctrl_roll_spin->setToolTip(anchor_angle_tooltip); m_anchor_ctrl_yaw_spin->setEnabled(false); m_anchor_ctrl_pitch_spin->setEnabled(false); m_anchor_ctrl_roll_spin->setEnabled(false); connect(m_anchor_follow_rotation_check, &QCheckBox::toggled, this, [this](bool checked) { m_anchor_ctrl_yaw_spin->setEnabled(checked); m_anchor_ctrl_pitch_spin->setEnabled(checked); m_anchor_ctrl_roll_spin->setEnabled(checked); }); m_flag_label = new QLabel(tr("Flag Group:")); m_flag_edit = new QLineEdit; m_flag_edit->setPlaceholderText(tr("e.g. thermal_visor")); m_condition_label = new QLabel(tr("Condition Flag:")); m_condition_combo = new QComboBox; m_condition_combo->setEditable(true); m_condition_combo->addItem(QString()); for (const std::string& flag : m_available_flags) m_condition_combo->addItem(QString::fromStdString(flag)); m_condition_mode_label = new QLabel(tr("Condition Mode:")); m_condition_mode_combo = new QComboBox; m_condition_mode_combo->addItem(tr("Activate"), false); m_condition_mode_combo->addItem(tr("Deactivate"), true); m_capture_seed_button = new QPushButton(tr("Use Current Hunt Seed")); m_runtime_element_summary_label = new QLabel; m_runtime_element_summary_label->setWordWrap(true); m_runtime_use_projection_check = new QCheckBox(tr("Projection")); m_runtime_use_projection_check->setToolTip( tr("Match the full projection: type plus FOV (perspective) or bounds (orthographic).")); m_runtime_use_projection_type_check = new QCheckBox(tr("Projection Type")); m_runtime_use_projection_type_check->setToolTip( tr("Match only whether the draw is Perspective or Orthographic, ignoring the exact " "FOV/bounds values. More robust when the detailed projection drifts.")); m_runtime_use_layer_check = new QCheckBox(tr("Layer")); m_runtime_use_viewport_check = new QCheckBox(tr("Viewport")); m_runtime_use_scissor_check = new QCheckBox(tr("Scissor")); m_runtime_use_render_state_check = new QCheckBox(tr("Render State")); m_clear_efb_check = new QCheckBox(tr("Clear EFB Copy")); m_clear_efb_check->setToolTip( tr("When this element is drawn, clear the next EFB-to-texture copy to transparent instead of\n" "copying the framebuffer content. Use to remove an effect captured via EFB copy.\n" "Independent of handling (can combine with Skip, Screen, etc.). Matching is on the full\n" "element signature, so only this element arms the clear.")); m_clear_efb_min_label = new QLabel(tr("EFB Min Width:")); m_clear_efb_min_spin = new QSpinBox; m_clear_efb_min_spin->setRange(0, 640); m_clear_efb_min_spin->setSingleStep(10); m_clear_efb_min_spin->setSpecialValueText(tr("Any")); m_clear_efb_min_spin->setValue(0); m_clear_efb_min_spin->setToolTip(tr("Only clear EFB copies whose native width >= this value.\n" "0 (Any) = no lower bound. Full EFB = 640 wide.")); m_clear_efb_max_label = new QLabel(tr("EFB Max Width:")); m_clear_efb_max_spin = new QSpinBox; m_clear_efb_max_spin->setRange(0, 640); m_clear_efb_max_spin->setSingleStep(10); m_clear_efb_max_spin->setSpecialValueText(tr("Any")); m_clear_efb_max_spin->setValue(0); m_clear_efb_max_spin->setToolTip(tr("Only clear EFB copies whose native width <= this value.\n" "0 (Any) = no upper bound.")); m_texture_mode_combo = new QComboBox; m_texture_mode_combo->addItem(tr("Include"), false); m_texture_mode_combo->addItem(tr("Exclude"), true); m_selected_match_mode_combo = new QComboBox; m_selected_match_mode_combo->addItem(tr("Include"), false); m_selected_match_mode_combo->addItem(tr("Exclude"), true); m_view_textures_button = new QPushButton(tr("View Textures")); m_texture_hash_container = new QWidget; m_texture_hash_layout = new QVBoxLayout(m_texture_hash_container); m_texture_hash_layout->setContentsMargins(0, 0, 0, 0); m_texture_hash_layout->setSpacing(4); m_texture_hash_scroll = new QScrollArea; m_texture_hash_scroll->setWidgetResizable(true); m_texture_hash_scroll->setFrameShape(QFrame::NoFrame); m_texture_hash_scroll->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); m_texture_hash_scroll->setWidget(m_texture_hash_container); AddTextureHashField(); EnsureTextureHashFieldRows(); m_selected_match_list = new QListWidget; m_add_current_match_button = new QPushButton(tr("Add Current Hunt Match")); m_remove_selected_match_button = new QPushButton(tr("Remove Selected Match")); auto* form = new QFormLayout; form->addRow(tr("Name:"), m_name_edit); form->addRow(tr("Match Type:"), m_match_kind_combo); form->addRow(m_profile_label, m_profile_combo); form->addRow(m_profile_layers_label, m_profile_layers_list); form->addRow(tr("Handling:"), m_handling_combo); form->addRow(QString(), m_preserve_stereo_efb_check); form->addRow(m_screen_pane_depth_label, m_screen_pane_depth_combo); form->addRow(m_element_depth_label, m_element_depth_spin); form->addRow(m_units_per_meter_label, m_units_per_meter_spin); form->addRow(m_passthrough_opacity_label, m_passthrough_opacity_spin); form->addRow(m_anchor_right_label, m_anchor_right_spin); form->addRow(m_anchor_up_label, m_anchor_up_spin); form->addRow(m_anchor_forward_label, m_anchor_forward_spin); form->addRow(m_anchor_rotation_label, m_anchor_rotation_combo); form->addRow(m_anchor_yaw_label, m_anchor_yaw_spin); form->addRow(m_anchor_upm_label, m_anchor_upm_spin); form->addRow(QString(), m_anchor_hide_check); form->addRow(m_anchor_hand_label, m_anchor_hand_combo); form->addRow(QString(), m_anchor_follow_rotation_check); form->addRow(m_anchor_ctrl_yaw_label, m_anchor_ctrl_yaw_spin); form->addRow(m_anchor_ctrl_pitch_label, m_anchor_ctrl_pitch_spin); form->addRow(m_anchor_ctrl_roll_label, m_anchor_ctrl_roll_spin); form->addRow(m_flag_label, m_flag_edit); form->addRow(m_condition_label, m_condition_combo); form->addRow(m_condition_mode_label, m_condition_mode_combo); form->addRow(QString(), m_view_textures_button); form->addRow(tr("Texture Mode:"), m_texture_mode_combo); form->addRow(tr("Texture Filters:"), m_texture_hash_scroll); form->addRow(tr("Selected Match Mode:"), m_selected_match_mode_combo); form->addRow(tr("Selected Matches:"), m_selected_match_list); form->addRow(QString(), m_clear_efb_check); form->addRow(m_clear_efb_min_label, m_clear_efb_min_spin); form->addRow(m_clear_efb_max_label, m_clear_efb_max_spin); auto* runtime_group_row = new QHBoxLayout; runtime_group_row->addWidget(m_runtime_use_projection_check); runtime_group_row->addWidget(m_runtime_use_projection_type_check); runtime_group_row->addWidget(m_runtime_use_layer_check); runtime_group_row->addWidget(m_runtime_use_viewport_check); runtime_group_row->addWidget(m_runtime_use_scissor_check); runtime_group_row->addWidget(m_runtime_use_render_state_check); auto* layout = new QVBoxLayout(this); layout->addLayout(form); layout->addWidget(m_capture_seed_button); layout->addWidget(m_runtime_element_summary_label); layout->addLayout(runtime_group_row); auto* selected_match_buttons = new QHBoxLayout; selected_match_buttons->addWidget(m_add_current_match_button); selected_match_buttons->addWidget(m_remove_selected_match_button); layout->addLayout(selected_match_buttons); auto* notes_form = new QFormLayout; notes_form->addRow(tr("Comments:"), m_comments_edit); notes_form->addRow(tr("Credits:"), m_credits_edit); layout->addLayout(notes_form); auto* buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); layout->addWidget(buttons); connect(buttons, &QDialogButtonBox::accepted, this, &ElementsGroupOverrideAddEditDialog::OnAccept); connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject); connect(m_match_kind_combo, QOverload::of(&QComboBox::currentIndexChanged), this, &ElementsGroupOverrideAddEditDialog::RefreshMatchKindUi); connect(m_clear_efb_check, &QCheckBox::toggled, this, &ElementsGroupOverrideAddEditDialog::RefreshClearEFBUi); connect(m_handling_combo, QOverload::of(&QComboBox::currentIndexChanged), this, &ElementsGroupOverrideAddEditDialog::RefreshHandlingUi); connect(m_capture_seed_button, &QPushButton::clicked, this, &ElementsGroupOverrideAddEditDialog::CaptureCurrentSeed); connect(m_view_textures_button, &QPushButton::clicked, this, &ElementsGroupOverrideAddEditDialog::ShowTextureBrowser); connect(m_add_current_match_button, &QPushButton::clicked, this, &ElementsGroupOverrideAddEditDialog::AddCurrentHuntMatch); connect(m_remove_selected_match_button, &QPushButton::clicked, this, &ElementsGroupOverrideAddEditDialog::RemoveSelectedMatchFilter); connect(m_selected_match_list, &QListWidget::itemSelectionChanged, this, [this]() { m_remove_selected_match_button->setEnabled(!m_selected_match_list->selectedItems().empty()); }); for (QCheckBox* checkbox : {m_runtime_use_projection_check, m_runtime_use_projection_type_check, m_runtime_use_layer_check, m_runtime_use_viewport_check, m_runtime_use_scissor_check, m_runtime_use_render_state_check}) { connect(checkbox, &QCheckBox::toggled, this, [this]() { m_runtime_element.use_projection = m_runtime_use_projection_check->isChecked(); m_runtime_element.use_projection_type = m_runtime_use_projection_type_check->isChecked(); m_runtime_element.use_layer = m_runtime_use_layer_check->isChecked(); m_runtime_element.use_viewport = m_runtime_use_viewport_check->isChecked(); m_runtime_element.use_scissor = m_runtime_use_scissor_check->isChecked(); m_runtime_element.use_render_state = m_runtime_use_render_state_check->isChecked(); RefreshRuntimeElementSummary(); }); } if (edit_override) { m_name_edit->setText(QString::fromStdString(edit_override->name)); m_comments_edit->setPlainText(QString::fromStdString(edit_override->comments)); m_credits_edit->setText(QString::fromStdString(edit_override->credits)); { const int idx = m_match_kind_combo->findData(static_cast(edit_override->match_kind)); if (idx >= 0) m_match_kind_combo->setCurrentIndex(idx); } { const int idx = m_profile_combo->findData(static_cast(edit_override->profile_id)); if (idx >= 0) m_profile_combo->setCurrentIndex(idx); SetProfileLayers(edit_override->profile_layers); } { const auto handling = edit_override->handling == ElementsGroupManager::HandlingType::FullscreenMono ? ElementsGroupManager::HandlingType::Fullscreen : edit_override->handling; const int handling_idx = m_handling_combo->findData(static_cast(handling)); if (handling_idx >= 0) m_handling_combo->setCurrentIndex(handling_idx); } m_preserve_stereo_efb_check->setChecked(edit_override->preserve_stereo_efb); { const int depth_idx = m_screen_pane_depth_combo->findData(static_cast(edit_override->screen_pane_depth)); if (depth_idx >= 0) m_screen_pane_depth_combo->setCurrentIndex(depth_idx); } m_element_depth_spin->setValue(edit_override->element_depth); if (edit_override->units_per_meter > 0.0f) m_units_per_meter_spin->setValue(edit_override->units_per_meter); m_passthrough_opacity_spin->setValue(edit_override->passthrough_opacity); m_anchor_right_spin->setValue(edit_override->anchor_right); m_anchor_up_spin->setValue(edit_override->anchor_up); m_anchor_forward_spin->setValue(edit_override->anchor_forward); { const int idx = m_anchor_rotation_combo->findData(static_cast(edit_override->anchor_rotation)); if (idx >= 0) m_anchor_rotation_combo->setCurrentIndex(idx); } m_anchor_yaw_spin->setValue(edit_override->anchor_yaw_deg); if (edit_override->anchor_units_per_meter > 0.0f) m_anchor_upm_spin->setValue(edit_override->anchor_units_per_meter); m_anchor_hide_check->setChecked(edit_override->anchor_hide); { const int idx = m_anchor_hand_combo->findData(edit_override->anchor_hand == 0 ? 0 : 1); if (idx >= 0) m_anchor_hand_combo->setCurrentIndex(idx); } if (edit_override->handling == ElementsGroupManager::HandlingType::ControllerAnchor) { m_anchor_follow_rotation_check->setChecked(edit_override->anchor_rotation != ShaderHunter::AnchorRotationMode::Off); m_anchor_ctrl_yaw_spin->setValue(edit_override->anchor_yaw_deg); m_anchor_ctrl_pitch_spin->setValue(edit_override->anchor_pitch_deg); m_anchor_ctrl_roll_spin->setValue(edit_override->anchor_roll_deg); } m_flag_edit->setText(QString::fromStdString(edit_override->flag_group)); { const QString condition = QString::fromStdString(edit_override->condition_flag); int idx = m_condition_combo->findText(condition); if (idx < 0 && !condition.isEmpty()) { m_condition_combo->addItem(condition); idx = m_condition_combo->findText(condition); } m_condition_combo->setCurrentIndex(std::max(idx, 0)); } { const int idx = m_condition_mode_combo->findData(edit_override->condition_inverted); if (idx >= 0) m_condition_mode_combo->setCurrentIndex(idx); } { const int idx = m_texture_mode_combo->findData(edit_override->texture_hashes_excluded); if (idx >= 0) m_texture_mode_combo->setCurrentIndex(idx); } SetTextureHashValues(edit_override->texture_hashes); m_runtime_element = edit_override->runtime_element; m_clear_efb_check->setChecked(edit_override->clear_efb); m_clear_efb_min_spin->setValue(edit_override->clear_efb_min_width); m_clear_efb_max_spin->setValue(edit_override->clear_efb_max_width); m_selected_match_filters = edit_override->selected_match_filter; m_selected_match_filters_excluded = edit_override->selected_match_filter_excluded; } else { m_name_edit->setText(tr("Element Override")); } for (const auto& filter : m_selected_match_filters) m_selected_match_list->addItem(FormatSelectedMatchSummary(filter)); { const int idx = m_selected_match_mode_combo->findData(m_selected_match_filters_excluded); if (idx >= 0) m_selected_match_mode_combo->setCurrentIndex(idx); } m_remove_selected_match_button->setEnabled(false); RefreshHandlingUi(); RefreshMatchKindUi(); RefreshClearEFBUi(); RefreshRuntimeElementSummary(); } ElementsGroupManager::ElementGroupOverride ElementsGroupOverrideAddEditDialog::GetResult() const { ElementsGroupManager::ElementGroupOverride result; result.name = m_name_edit->text().trimmed().toStdString(); result.comments = m_comments_edit->toPlainText().trimmed().toStdString(); result.credits = m_credits_edit->text().trimmed().toStdString(); result.match_kind = static_cast( m_match_kind_combo->currentData().toInt()); result.handling = static_cast( m_handling_combo->currentData().toInt()); result.preserve_stereo_efb = result.handling == ElementsGroupManager::HandlingType::Fullscreen && m_preserve_stereo_efb_check->isChecked(); result.screen_pane_depth = static_cast( m_screen_pane_depth_combo->currentData().toInt()); result.runtime_element = m_runtime_element; result.profile_id = static_cast(m_profile_combo->currentData().toInt()); result.profile_layers = CollectProfileLayers(); result.element_depth = m_element_depth_spin->value(); result.units_per_meter = m_units_per_meter_spin->value() > 0.0 ? m_units_per_meter_spin->value() : -1.0f; result.passthrough_opacity = result.handling == ElementsGroupManager::HandlingType::Passthrough ? static_cast(m_passthrough_opacity_spin->value()) : 0.0f; if (result.handling == ElementsGroupManager::HandlingType::CameraAnchor) { result.anchor_right = static_cast(m_anchor_right_spin->value()); result.anchor_up = static_cast(m_anchor_up_spin->value()); result.anchor_forward = static_cast(m_anchor_forward_spin->value()); result.anchor_rotation = static_cast( m_anchor_rotation_combo->currentData().toInt()); result.anchor_yaw_deg = static_cast(m_anchor_yaw_spin->value()); result.anchor_units_per_meter = m_anchor_upm_spin->value() >= UPM_OVERRIDE_MIN ? static_cast(m_anchor_upm_spin->value()) : -1.0f; result.anchor_hide = m_anchor_hide_check->isChecked(); } if (result.handling == ElementsGroupManager::HandlingType::ControllerAnchor) { result.anchor_hand = m_anchor_hand_combo->currentData().toInt(); result.anchor_right = static_cast(m_anchor_right_spin->value()); result.anchor_up = static_cast(m_anchor_up_spin->value()); result.anchor_forward = static_cast(m_anchor_forward_spin->value()); result.anchor_rotation = m_anchor_follow_rotation_check->isChecked() ? ShaderHunter::AnchorRotationMode::Full : ShaderHunter::AnchorRotationMode::Off; result.anchor_yaw_deg = static_cast(m_anchor_ctrl_yaw_spin->value()); result.anchor_pitch_deg = static_cast(m_anchor_ctrl_pitch_spin->value()); result.anchor_roll_deg = static_cast(m_anchor_ctrl_roll_spin->value()); } result.texture_hashes = CollectTextureHashValues(); result.texture_hashes_excluded = !result.texture_hashes.empty() && m_texture_mode_combo->currentData().toBool(); result.selected_match_filter = m_selected_match_filters; result.selected_match_filter_excluded = !result.selected_match_filter.empty() && m_selected_match_mode_combo->currentData().toBool(); result.clear_efb = m_clear_efb_check->isChecked(); result.clear_efb_min_width = result.clear_efb ? m_clear_efb_min_spin->value() : 0; result.clear_efb_max_width = result.clear_efb ? m_clear_efb_max_spin->value() : 0; result.flag_group = m_flag_edit->text().trimmed().toStdString(); result.condition_flag = m_condition_combo->currentText().trimmed().toStdString(); result.condition_inverted = !result.condition_flag.empty() && m_condition_mode_combo->currentData().toBool(); if (result.match_kind == ElementsGroupManager::MatchKind::ProfileLayer) { result.runtime_element = {}; result.selected_match_filter.clear(); result.selected_match_filter_excluded = false; } else { result.profile_id = MetroidElementProfile::None; result.profile_layers.clear(); } return result; } std::vector ElementsGroupOverrideAddEditDialog::CollectTextureHashValues() const { std::vector hashes; hashes.reserve(m_texture_hash_edits.size()); for (QLineEdit* edit : m_texture_hash_edits) { const QString text = edit->text().trimmed(); if (text.isEmpty()) continue; const u64 parsed = std::strtoull(text.toStdString().c_str(), nullptr, 16); if (parsed != 0) hashes.push_back(parsed); } std::sort(hashes.begin(), hashes.end()); hashes.erase(std::unique(hashes.begin(), hashes.end()), hashes.end()); return hashes; } void ElementsGroupOverrideAddEditDialog::SetTextureHashValues(const std::vector& hashes) { m_updating_texture_hash_fields = true; while (m_texture_hash_edits.size() < hashes.size()) AddTextureHashField(); for (size_t i = 0; i < hashes.size(); ++i) m_texture_hash_edits[i]->setText(ToHashHex(hashes[i])); for (size_t i = hashes.size(); i < m_texture_hash_edits.size(); ++i) m_texture_hash_edits[i]->clear(); m_updating_texture_hash_fields = false; EnsureTextureHashFieldRows(); } void ElementsGroupOverrideAddEditDialog::AddTextureHashField(const QString& text) { auto* edit = new QLineEdit; edit->setPlaceholderText(tr("0000000000000000")); edit->setText(text); connect(edit, &QLineEdit::editingFinished, this, [this, edit]() { SanitizeTextureHashField(edit); }); connect(edit, &QLineEdit::textChanged, this, [this]() { EnsureTextureHashFieldRows(); }); m_texture_hash_layout->addWidget(edit); m_texture_hash_edits.push_back(edit); } void ElementsGroupOverrideAddEditDialog::EnsureTextureHashFieldRows() { if (m_updating_texture_hash_fields) return; m_updating_texture_hash_fields = true; int filled = 0; for (QLineEdit* edit : m_texture_hash_edits) { if (!edit->text().trimmed().isEmpty()) ++filled; } const int desired_rows = std::max(1, filled + 1); while (static_cast(m_texture_hash_edits.size()) < desired_rows) AddTextureHashField(); while (static_cast(m_texture_hash_edits.size()) > desired_rows) { QLineEdit* edit = m_texture_hash_edits.back(); if (!edit->text().trimmed().isEmpty()) break; m_texture_hash_edits.pop_back(); m_texture_hash_layout->removeWidget(edit); delete edit; } const int visible_rows = std::min(desired_rows, MAX_VISIBLE_TEXTURE_HASH_ROWS); const int viewport_height = visible_rows * TEXTURE_HASH_ROW_HEIGHT; m_texture_hash_scroll->setMinimumHeight(viewport_height); m_texture_hash_scroll->setMaximumHeight(viewport_height); m_updating_texture_hash_fields = false; } void ElementsGroupOverrideAddEditDialog::SanitizeTextureHashField(QLineEdit* edit) { if (edit == nullptr || m_updating_texture_hash_fields) return; QString text = edit->text().trimmed().toLower(); QString filtered; filtered.reserve(text.size()); for (const QChar c : text) { if (c.isDigit() || (c >= QLatin1Char('a') && c <= QLatin1Char('f'))) filtered.append(c); } if (filtered.size() > 16) filtered = filtered.right(16); edit->setText(filtered); } void ElementsGroupOverrideAddEditDialog::RefreshRuntimeElementSummary() { QString summary = FormatRuntimeElementSummary(m_runtime_element); QStringList groups; if (m_runtime_element.use_projection) groups << tr("Projection"); if (m_runtime_element.use_projection_type) groups << tr("Projection Type"); if (m_runtime_element.use_layer) groups << tr("Layer"); if (m_runtime_element.use_viewport) groups << tr("Viewport"); if (m_runtime_element.use_scissor) groups << tr("Scissor"); if (m_runtime_element.use_render_state) groups << tr("Render State"); summary += tr("\nActive Groups: %1") .arg(groups.isEmpty() ? tr("(none)") : groups.join(QStringLiteral(", "))); m_runtime_element_summary_label->setText(summary); const QSignalBlocker projection_blocker(m_runtime_use_projection_check); const QSignalBlocker projection_type_blocker(m_runtime_use_projection_type_check); const QSignalBlocker layer_blocker(m_runtime_use_layer_check); const QSignalBlocker viewport_blocker(m_runtime_use_viewport_check); const QSignalBlocker scissor_blocker(m_runtime_use_scissor_check); const QSignalBlocker render_blocker(m_runtime_use_render_state_check); m_runtime_use_projection_check->setChecked(m_runtime_element.use_projection); m_runtime_use_projection_type_check->setChecked(m_runtime_element.use_projection_type); m_runtime_use_layer_check->setChecked(m_runtime_element.use_layer); m_runtime_use_viewport_check->setChecked(m_runtime_element.use_viewport); m_runtime_use_scissor_check->setChecked(m_runtime_element.use_scissor); m_runtime_use_render_state_check->setChecked(m_runtime_element.use_render_state); } void ElementsGroupOverrideAddEditDialog::RefreshHandlingUi() { const auto handling = static_cast( m_handling_combo->currentData().toInt()); const bool show_element_depth = (handling == ElementsGroupManager::HandlingType::Screen || handling == ElementsGroupManager::HandlingType::HeadLocked); const bool show_screen_pane_depth = handling == ElementsGroupManager::HandlingType::ScreenPane; const bool show_preserve_stereo_efb = handling == ElementsGroupManager::HandlingType::Fullscreen; const bool show_units_per_meter = (handling == ElementsGroupManager::HandlingType::UnitsPerMeter); const bool show_passthrough = (handling == ElementsGroupManager::HandlingType::Passthrough); const bool show_anchor = (handling == ElementsGroupManager::HandlingType::CameraAnchor); const bool show_controller_anchor = (handling == ElementsGroupManager::HandlingType::ControllerAnchor); const bool show_anchor_offsets = show_anchor || show_controller_anchor; const bool is_flag = (handling == ElementsGroupManager::HandlingType::Flag); m_element_depth_label->setVisible(show_element_depth); m_element_depth_spin->setVisible(show_element_depth); m_screen_pane_depth_label->setVisible(show_screen_pane_depth); m_screen_pane_depth_combo->setVisible(show_screen_pane_depth); m_preserve_stereo_efb_check->setVisible(show_preserve_stereo_efb); m_units_per_meter_label->setVisible(show_units_per_meter); m_units_per_meter_spin->setVisible(show_units_per_meter); m_passthrough_opacity_label->setVisible(show_passthrough); m_passthrough_opacity_spin->setVisible(show_passthrough); m_anchor_right_label->setVisible(show_anchor_offsets); m_anchor_right_spin->setVisible(show_anchor_offsets); m_anchor_up_label->setVisible(show_anchor_offsets); m_anchor_up_spin->setVisible(show_anchor_offsets); m_anchor_forward_label->setVisible(show_anchor_offsets); m_anchor_forward_spin->setVisible(show_anchor_offsets); m_anchor_rotation_label->setVisible(show_anchor); m_anchor_rotation_combo->setVisible(show_anchor); m_anchor_yaw_label->setVisible(show_anchor); m_anchor_yaw_spin->setVisible(show_anchor); m_anchor_upm_label->setVisible(show_anchor); m_anchor_upm_spin->setVisible(show_anchor); m_anchor_hide_check->setVisible(show_anchor); m_anchor_hand_label->setVisible(show_controller_anchor); m_anchor_hand_combo->setVisible(show_controller_anchor); m_anchor_follow_rotation_check->setVisible(show_controller_anchor); m_anchor_ctrl_yaw_label->setVisible(show_controller_anchor); m_anchor_ctrl_yaw_spin->setVisible(show_controller_anchor); m_anchor_ctrl_pitch_label->setVisible(show_controller_anchor); m_anchor_ctrl_pitch_spin->setVisible(show_controller_anchor); m_anchor_ctrl_roll_label->setVisible(show_controller_anchor); m_anchor_ctrl_roll_spin->setVisible(show_controller_anchor); m_condition_label->setVisible(!is_flag); m_condition_combo->setVisible(!is_flag); m_condition_mode_label->setVisible(!is_flag); m_condition_mode_combo->setVisible(!is_flag); } std::vector ElementsGroupOverrideAddEditDialog::CollectProfileLayers() const { std::vector layers; for (int i = 0; i < m_profile_layers_list->count(); ++i) { const QListWidgetItem* item = m_profile_layers_list->item(i); if (item->checkState() == Qt::Checked) layers.push_back(static_cast(item->data(Qt::UserRole).toInt())); } return layers; } void ElementsGroupOverrideAddEditDialog::SetProfileLayers( const std::vector& layers) { for (int i = 0; i < m_profile_layers_list->count(); ++i) { QListWidgetItem* item = m_profile_layers_list->item(i); const auto layer = static_cast(item->data(Qt::UserRole).toInt()); item->setCheckState(std::find(layers.begin(), layers.end(), layer) != layers.end() ? Qt::Checked : Qt::Unchecked); } } void ElementsGroupOverrideAddEditDialog::RefreshMatchKindUi() { const auto match_kind = static_cast( m_match_kind_combo->currentData().toInt()); const bool profile_layer = match_kind == ElementsGroupManager::MatchKind::ProfileLayer; m_profile_label->setVisible(profile_layer); m_profile_combo->setVisible(profile_layer); m_profile_layers_label->setVisible(profile_layer); m_profile_layers_list->setVisible(profile_layer); m_capture_seed_button->setVisible(!profile_layer); m_runtime_element_summary_label->setVisible(!profile_layer); m_runtime_use_projection_check->setVisible(!profile_layer); m_runtime_use_projection_type_check->setVisible(!profile_layer); m_runtime_use_layer_check->setVisible(!profile_layer); m_runtime_use_viewport_check->setVisible(!profile_layer); m_runtime_use_scissor_check->setVisible(!profile_layer); m_runtime_use_render_state_check->setVisible(!profile_layer); m_selected_match_mode_combo->setVisible(!profile_layer); m_selected_match_list->setVisible(!profile_layer); m_add_current_match_button->setVisible(!profile_layer); m_remove_selected_match_button->setVisible(!profile_layer); } void ElementsGroupOverrideAddEditDialog::RefreshClearEFBUi() { // Clear EFB works for both runtime-signature and profile-layer matches, so it stays visible in // either mode; only the width bounds depend on the checkbox. const bool show = m_clear_efb_check->isChecked(); m_clear_efb_min_label->setVisible(show); m_clear_efb_min_spin->setVisible(show); m_clear_efb_max_label->setVisible(show); m_clear_efb_max_spin->setVisible(show); } void ElementsGroupOverrideAddEditDialog::CaptureCurrentSeed() { const auto status = ElementsGroupManager::GetInstance().GetStatus(); if (!status.seed_valid) { QMessageBox::warning(this, tr("Elements Group Override"), tr("No active Elements Hunting seed is available.")); return; } m_runtime_element = status.seed_signature; RefreshRuntimeElementSummary(); } void ElementsGroupOverrideAddEditDialog::ShowTextureBrowser() { const auto status = ElementsGroupManager::GetInstance().GetStatus(); if (!status.seed_valid) { QMessageBox::warning(this, tr("View Textures"), tr("No active Elements Hunting preview scope is available.\n" "Open Elements Hunting and select a seed first.")); return; } TextureHashBrowserConfig browser_config; browser_config.title = tr("Textures for Current Element Selection"); browser_config.empty_info_text = tr("No textures were captured yet for the current preview scope."); browser_config.current_label = tr("current preview scope"); browser_config.fetch_current_label = []() { const auto current_status = ElementsGroupManager::GetInstance().GetStatus(); return current_status.selected_match_filter_count > 0 ? (current_status.selected_match_filter_excluded ? QObject::tr("unchecked matches only") : QObject::tr("checked matches only")) : QObject::tr("whole current group"); }; browser_config.initial_selected_hashes = CollectTextureHashValues(); browser_config.fetch_current_entries = []() { return CollectTextureBrowserEntries( ElementsGroupManager::GetInstance().GetCurrentTextureSourceDraws()); }; browser_config.apply_selected_hashes = [this](const std::vector& hashes) { SetTextureHashValues(hashes); }; ShowTextureHashBrowserDialog(this, browser_config); } void ElementsGroupOverrideAddEditDialog::AddCurrentHuntMatch() { const auto status = ElementsGroupManager::GetInstance().GetStatus(); if (!status.highlighted_draw) { QMessageBox::warning(this, tr("Elements Group Override"), tr("No active Elements Hunting match is available.")); return; } const auto matches = ElementsGroupManager::GetInstance().GetCurrentMatches(); if (status.selected_match < 0 || status.selected_match >= static_cast(matches.size())) { QMessageBox::warning(this, tr("Elements Group Override"), tr("No stable Elements Hunting match is available.")); return; } const auto filter = matches[static_cast(status.selected_match)].subgroup; const auto it = std::find_if(m_selected_match_filters.begin(), m_selected_match_filters.end(), [&filter](const ElementsGroupManager::SelectedSubgroupSignature& existing) { return SelectedSubgroupSignaturesEqual(existing, filter); }); if (it != m_selected_match_filters.end()) return; m_selected_match_filters.push_back(filter); m_selected_match_list->addItem(FormatSelectedMatchSummary(filter)); } void ElementsGroupOverrideAddEditDialog::RemoveSelectedMatchFilter() { const auto items = m_selected_match_list->selectedItems(); if (items.empty()) return; const int row = m_selected_match_list->row(items[0]); if (row < 0 || row >= static_cast(m_selected_match_filters.size())) return; m_selected_match_filters.erase(m_selected_match_filters.begin() + row); delete m_selected_match_list->takeItem(row); } void ElementsGroupOverrideAddEditDialog::OnAccept() { const auto result = GetResult(); if (result.name.empty()) { QMessageBox::warning(this, tr("Elements Group Override"), tr("Name cannot be empty.")); return; } if (result.match_kind == ElementsGroupManager::MatchKind::ProfileLayer) { if (result.profile_id == MetroidElementProfile::None) { QMessageBox::warning(this, tr("Elements Group Override"), tr("A Metroid profile is required.")); return; } if (result.profile_layers.empty()) { QMessageBox::warning(this, tr("Elements Group Override"), tr("Select at least one profile layer.")); return; } } else if (!result.runtime_element.valid) { QMessageBox::warning(this, tr("Elements Group Override"), tr("A runtime element signature is required.")); return; } if (result.match_kind == ElementsGroupManager::MatchKind::RuntimeSignature && !result.runtime_element.use_projection && !result.runtime_element.use_projection_type && !result.runtime_element.use_layer && !result.runtime_element.use_viewport && !result.runtime_element.use_scissor && !result.runtime_element.use_render_state) { QMessageBox::warning(this, tr("Elements Group Override"), tr("Enable at least one runtime signature group.")); return; } if (result.handling == ElementsGroupManager::HandlingType::Flag && result.flag_group.empty()) { QMessageBox::warning(this, tr("Elements Group Override"), tr("Flag handling requires a flag group name.")); return; } accept(); }