postprocessingsettingswidget.cpp (17098B)
1 // SPDX-FileCopyrightText: 2019-2023 Connor McLaughlin <stenzek@gmail.com> 2 // SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0) 3 4 #include "postprocessingsettingswidget.h" 5 #include "qthost.h" 6 #include "settingwidgetbinder.h" 7 8 #include "util/postprocessing.h" 9 10 #include "common/error.h" 11 12 #include <QtWidgets/QCheckBox> 13 #include <QtWidgets/QDialogButtonBox> 14 #include <QtWidgets/QGridLayout> 15 #include <QtWidgets/QLabel> 16 #include <QtWidgets/QMessageBox> 17 #include <QtWidgets/QSlider> 18 19 PostProcessingSettingsWidget::PostProcessingSettingsWidget(SettingsWindow* dialog, QWidget* parent) : QTabWidget(parent) 20 { 21 addTab(new PostProcessingChainConfigWidget(dialog, this, PostProcessing::Config::DISPLAY_CHAIN_SECTION), 22 tr("Display")); 23 addTab(new PostProcessingChainConfigWidget(dialog, this, PostProcessing::Config::INTERNAL_CHAIN_SECTION), 24 tr("Internal")); 25 setDocumentMode(true); 26 } 27 28 PostProcessingSettingsWidget::~PostProcessingSettingsWidget() = default; 29 30 PostProcessingChainConfigWidget::PostProcessingChainConfigWidget(SettingsWindow* dialog, QWidget* parent, 31 const char* section) 32 : QWidget(parent), m_dialog(dialog), m_section(section) 33 { 34 SettingsInterface* sif = dialog->getSettingsInterface(); 35 36 m_ui.setupUi(this); 37 38 SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.enablePostProcessing, section, "Enabled", false); 39 40 updateList(); 41 updateButtonsAndConfigPane(std::nullopt); 42 connectUi(); 43 } 44 45 PostProcessingChainConfigWidget::~PostProcessingChainConfigWidget() = default; 46 47 SettingsInterface& PostProcessingChainConfigWidget::getSettingsInterfaceToUpdate() 48 { 49 return m_dialog->isPerGameSettings() ? *m_dialog->getSettingsInterface() : *Host::Internal::GetBaseSettingsLayer(); 50 } 51 52 void PostProcessingChainConfigWidget::commitSettingsUpdate() 53 { 54 if (m_dialog->isPerGameSettings()) 55 { 56 m_dialog->saveAndReloadGameSettings(); 57 } 58 else 59 { 60 Host::CommitBaseSettingChanges(); 61 g_emu_thread->updatePostProcessingSettings(); 62 } 63 } 64 65 void PostProcessingChainConfigWidget::connectUi() 66 { 67 connect(m_ui.reload, &QPushButton::clicked, this, &PostProcessingChainConfigWidget::onReloadButtonClicked); 68 connect(m_ui.add, &QToolButton::clicked, this, &PostProcessingChainConfigWidget::onAddButtonClicked); 69 connect(m_ui.remove, &QToolButton::clicked, this, &PostProcessingChainConfigWidget::onRemoveButtonClicked); 70 connect(m_ui.clear, &QToolButton::clicked, this, &PostProcessingChainConfigWidget::onClearButtonClicked); 71 connect(m_ui.moveUp, &QToolButton::clicked, this, &PostProcessingChainConfigWidget::onMoveUpButtonClicked); 72 connect(m_ui.moveDown, &QToolButton::clicked, this, &PostProcessingChainConfigWidget::onMoveDownButtonClicked); 73 connect(m_ui.stages, &QListWidget::itemSelectionChanged, this, 74 &PostProcessingChainConfigWidget::onSelectedShaderChanged); 75 } 76 77 std::optional<u32> PostProcessingChainConfigWidget::getSelectedIndex() const 78 { 79 QList<QListWidgetItem*> selected_items = m_ui.stages->selectedItems(); 80 return selected_items.empty() ? std::nullopt : 81 std::optional<u32>(selected_items.first()->data(Qt::UserRole).toUInt()); 82 } 83 84 void PostProcessingChainConfigWidget::selectIndex(s32 index) 85 { 86 if (index < 0 || index >= m_ui.stages->count()) 87 return; 88 89 QSignalBlocker sb(m_ui.stages); 90 m_ui.stages->setCurrentItem(m_ui.stages->item(index)); 91 updateButtonsAndConfigPane(index); 92 } 93 94 void PostProcessingChainConfigWidget::updateList() 95 { 96 const auto lock = Host::GetSettingsLock(); 97 const SettingsInterface& si = getSettingsInterfaceToUpdate(); 98 99 updateList(si); 100 } 101 102 void PostProcessingChainConfigWidget::updateList(const SettingsInterface& si) 103 { 104 m_ui.stages->clear(); 105 106 const u32 stage_count = PostProcessing::Config::GetStageCount(si, m_section); 107 108 for (u32 i = 0; i < stage_count; i++) 109 { 110 const std::string stage_name = PostProcessing::Config::GetStageShaderName(si, m_section, i); 111 QListWidgetItem* item = new QListWidgetItem(QString::fromStdString(stage_name), m_ui.stages); 112 item->setData(Qt::UserRole, QVariant(i)); 113 } 114 115 m_ui.clear->setEnabled(stage_count > 0); 116 m_ui.reload->setEnabled(stage_count > 0); 117 updateButtonsAndConfigPane(std::nullopt); 118 } 119 120 void PostProcessingChainConfigWidget::updateButtonsAndConfigPane(std::optional<u32> index) 121 { 122 m_ui.remove->setEnabled(index.has_value()); 123 124 if (index.has_value()) 125 { 126 m_ui.moveUp->setEnabled(index.value() > 0); 127 m_ui.moveDown->setEnabled(index.value() < static_cast<u32>(m_ui.stages->count() - 1)); 128 } 129 else 130 { 131 m_ui.moveUp->setEnabled(false); 132 m_ui.moveDown->setEnabled(false); 133 } 134 135 m_ui.scrollArea->setWidget(nullptr); 136 m_ui.scrollArea->setVisible(false); 137 138 if (m_shader_config) 139 { 140 delete m_shader_config; 141 m_shader_config = nullptr; 142 } 143 144 if (!index.has_value()) 145 return; 146 147 const auto lock = Host::GetSettingsLock(); 148 const SettingsInterface& si = getSettingsInterfaceToUpdate(); 149 std::vector<PostProcessing::ShaderOption> options = 150 PostProcessing::Config::GetStageOptions(si, m_section, index.value()); 151 if (options.empty()) 152 return; 153 154 m_shader_config = 155 new PostProcessingShaderConfigWidget(m_ui.scrollArea, this, m_section, index.value(), std::move(options)); 156 m_ui.scrollArea->setWidget(m_shader_config); 157 m_ui.scrollArea->setVisible(true); 158 } 159 160 void PostProcessingChainConfigWidget::onAddButtonClicked() 161 { 162 QMenu menu; 163 164 const std::vector<std::pair<std::string, std::string>> shaders = PostProcessing::GetAvailableShaderNames(); 165 if (shaders.empty()) 166 { 167 menu.addAction(tr("No Shaders Available"))->setEnabled(false); 168 } 169 else 170 { 171 for (auto& [display_name, name] : shaders) 172 { 173 QAction* action = menu.addAction(QString::fromStdString(display_name)); 174 connect(action, &QAction::triggered, [this, shader = std::move(name)]() { 175 auto lock = Host::GetSettingsLock(); 176 SettingsInterface& si = getSettingsInterfaceToUpdate(); 177 178 Error error; 179 if (!PostProcessing::Config::AddStage(si, m_section, shader, &error)) 180 { 181 lock.unlock(); 182 QMessageBox::critical(this, tr("Error"), 183 tr("Failed to add shader: %1").arg(QString::fromStdString(error.GetDescription()))); 184 return; 185 } 186 187 updateList(si); 188 lock.unlock(); 189 commitSettingsUpdate(); 190 }); 191 } 192 } 193 194 menu.exec(QCursor::pos()); 195 } 196 197 void PostProcessingChainConfigWidget::onRemoveButtonClicked() 198 { 199 QList<QListWidgetItem*> selected_items = m_ui.stages->selectedItems(); 200 if (selected_items.empty()) 201 return; 202 203 auto lock = Host::GetSettingsLock(); 204 SettingsInterface& si = getSettingsInterfaceToUpdate(); 205 206 QListWidgetItem* item = selected_items.first(); 207 u32 index = item->data(Qt::UserRole).toUInt(); 208 if (index < PostProcessing::Config::GetStageCount(si, m_section)) 209 { 210 PostProcessing::Config::RemoveStage(si, m_section, index); 211 updateList(si); 212 lock.unlock(); 213 commitSettingsUpdate(); 214 } 215 } 216 217 void PostProcessingChainConfigWidget::onClearButtonClicked() 218 { 219 if (QMessageBox::question(this, tr("Question"), tr("Are you sure you want to clear all shader stages?"), 220 QMessageBox::Yes, QMessageBox::No) == QMessageBox::Yes) 221 { 222 auto lock = Host::GetSettingsLock(); 223 SettingsInterface& si = getSettingsInterfaceToUpdate(); 224 PostProcessing::Config::ClearStages(si, m_section); 225 updateList(si); 226 lock.unlock(); 227 commitSettingsUpdate(); 228 } 229 } 230 231 void PostProcessingChainConfigWidget::onMoveUpButtonClicked() 232 { 233 std::optional<u32> index = getSelectedIndex(); 234 if (index.has_value() && index.value() > 0) 235 { 236 auto lock = Host::GetSettingsLock(); 237 SettingsInterface& si = getSettingsInterfaceToUpdate(); 238 PostProcessing::Config::MoveStageUp(si, m_section, index.value()); 239 updateList(si); 240 lock.unlock(); 241 selectIndex(index.value() - 1); 242 commitSettingsUpdate(); 243 } 244 } 245 246 void PostProcessingChainConfigWidget::onMoveDownButtonClicked() 247 { 248 std::optional<u32> index = getSelectedIndex(); 249 if (index.has_value() || index.value() < (static_cast<u32>(m_ui.stages->count() - 1))) 250 { 251 auto lock = Host::GetSettingsLock(); 252 SettingsInterface& si = getSettingsInterfaceToUpdate(); 253 PostProcessing::Config::MoveStageDown(si, m_section, index.value()); 254 updateList(si); 255 lock.unlock(); 256 selectIndex(index.value() + 1); 257 commitSettingsUpdate(); 258 } 259 } 260 261 void PostProcessingChainConfigWidget::onReloadButtonClicked() 262 { 263 g_emu_thread->reloadPostProcessingShaders(); 264 } 265 266 void PostProcessingChainConfigWidget::onSelectedShaderChanged() 267 { 268 std::optional<u32> index = getSelectedIndex(); 269 updateButtonsAndConfigPane(index); 270 } 271 272 PostProcessingShaderConfigWidget::PostProcessingShaderConfigWidget(QWidget* parent, 273 PostProcessingChainConfigWidget* widget, 274 const char* section, u32 stage_index, 275 std::vector<PostProcessing::ShaderOption> options) 276 : QWidget(parent), m_widget(widget), m_section(section), m_stage_index(stage_index), m_options(std::move(options)) 277 { 278 m_layout = new QGridLayout(this); 279 280 createUi(); 281 } 282 283 PostProcessingShaderConfigWidget::~PostProcessingShaderConfigWidget() = default; 284 285 void PostProcessingShaderConfigWidget::updateConfigForOption(const PostProcessing::ShaderOption& option) 286 { 287 const auto lock = Host::GetSettingsLock(); 288 SettingsInterface& si = m_widget->getSettingsInterfaceToUpdate(); 289 PostProcessing::Config::SetStageOption(si, m_section, m_stage_index, option); 290 m_widget->commitSettingsUpdate(); 291 } 292 293 void PostProcessingShaderConfigWidget::onResetDefaultsClicked() 294 { 295 { 296 const auto lock = Host::GetSettingsLock(); 297 SettingsInterface& si = m_widget->getSettingsInterfaceToUpdate(); 298 for (PostProcessing::ShaderOption& option : m_options) 299 { 300 if (std::memcmp(option.value.data(), option.default_value.data(), sizeof(option.value)) == 0) 301 continue; 302 303 option.value = option.default_value; 304 PostProcessing::Config::UnsetStageOption(si, m_section, m_stage_index, option); 305 } 306 m_widget->commitSettingsUpdate(); 307 } 308 309 // Toss and recreate UI. 310 for (auto it = m_widgets.rbegin(); it != m_widgets.rend(); ++it) 311 { 312 m_layout->removeWidget(*it); 313 delete *it; 314 } 315 m_widgets.clear(); 316 createUi(); 317 } 318 319 void PostProcessingShaderConfigWidget::createUi() 320 { 321 u32 row = 0; 322 323 const std::string* last_category = nullptr; 324 325 for (PostProcessing::ShaderOption& option : m_options) 326 { 327 if (option.ui_name.empty()) 328 continue; 329 330 if (!last_category || option.category != *last_category) 331 { 332 if (last_category) 333 m_layout->addItem(new QSpacerItem(1, 4), row++, 0); 334 335 if (!option.category.empty()) 336 { 337 QLabel* label = new QLabel(QString::fromStdString(option.category), this); 338 QFont label_font(label->font()); 339 label_font.setPointSizeF(12.0f); 340 label->setFont(label_font); 341 m_layout->addWidget(label, row++, 0, 1, 3, Qt::AlignLeft); 342 } 343 344 if (last_category || !option.category.empty()) 345 { 346 QLabel* line = new QLabel(this); 347 line->setFrameShape(QFrame::HLine); 348 line->setFixedHeight(4); 349 line->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); 350 m_layout->addWidget(line, row++, 0, 1, 3); 351 } 352 353 last_category = &option.category; 354 } 355 356 const QString tooltip = QString::fromStdString(option.tooltip); 357 358 if (option.type == PostProcessing::ShaderOption::Type::Bool) 359 { 360 QCheckBox* checkbox = new QCheckBox(QString::fromStdString(option.ui_name), this); 361 checkbox->setToolTip(tooltip); 362 checkbox->setChecked(option.value[0].int_value != 0); 363 connect(checkbox, &QCheckBox::checkStateChanged, [this, &option](Qt::CheckState state) { 364 option.value[0].int_value = (state == Qt::Checked) ? 1 : 0; 365 updateConfigForOption(option); 366 }); 367 m_layout->addWidget(checkbox, row, 0, 1, 3, Qt::AlignLeft); 368 m_widgets.push_back(checkbox); 369 row++; 370 } 371 else if (option.type == PostProcessing::ShaderOption::Type::Int && !option.choice_options.empty()) 372 { 373 QLabel* label = new QLabel(QString::fromStdString(option.ui_name), this); 374 label->setToolTip(tooltip); 375 m_layout->addWidget(label, row, 0, 1, 1, Qt::AlignLeft); 376 377 QComboBox* combo = new QComboBox(this); 378 combo->setToolTip(tooltip); 379 for (const std::string& combo_option : option.choice_options) 380 combo->addItem(QString::fromStdString(combo_option)); 381 combo->setCurrentIndex(option.value[0].int_value); 382 connect(combo, &QComboBox::currentIndexChanged, [this, &option](int index) { 383 option.value[0].int_value = index; 384 updateConfigForOption(option); 385 }); 386 m_layout->addWidget(combo, row, 1, 1, 2, Qt::AlignLeft); 387 m_widgets.push_back(combo); 388 row++; 389 } 390 else 391 { 392 for (u32 i = 0; i < option.vector_size; i++) 393 { 394 QString label; 395 if (option.vector_size <= 1) 396 { 397 label = QString::fromStdString(option.ui_name); 398 } 399 else 400 { 401 static constexpr std::array<const char*, PostProcessing::ShaderOption::MAX_VECTOR_COMPONENTS + 1> suffixes = { 402 {QT_TR_NOOP("Red"), QT_TR_NOOP("Green"), QT_TR_NOOP("Blue"), QT_TR_NOOP("Alpha")}}; 403 label = tr("%1 (%2)").arg(QString::fromStdString(option.ui_name)).arg(tr(suffixes[i])); 404 } 405 406 QWidget* label_w = new QLabel(label, this); 407 label_w->setToolTip(tooltip); 408 m_layout->addWidget(label_w, row, 0, 1, 1, Qt::AlignLeft); 409 m_widgets.push_back(label_w); 410 411 QSlider* slider = new QSlider(Qt::Horizontal, this); 412 slider->setToolTip(tooltip); 413 m_layout->addWidget(slider, row, 1, 1, 1, Qt::AlignLeft); 414 m_widgets.push_back(slider); 415 416 QLabel* slider_label = new QLabel(this); 417 slider_label->setToolTip(tooltip); 418 m_layout->addWidget(slider_label, row, 2, 1, 1, Qt::AlignLeft); 419 m_widgets.push_back(slider_label); 420 421 if (option.type == PostProcessing::ShaderOption::Type::Int) 422 { 423 slider_label->setText(QString::number(option.value[i].int_value)); 424 425 const int range = option.max_value[i].int_value - option.min_value[i].int_value; 426 const int step_value = 427 (option.step_value[i].int_value != 0) ? option.step_value[i].int_value : ((range + 99) / 100); 428 const int num_steps = range / step_value; 429 slider->setMinimum(0); 430 slider->setMaximum(num_steps); 431 slider->setSingleStep(1); 432 slider->setTickInterval(step_value); 433 slider->setValue((option.value[i].int_value - option.min_value[i].int_value) / step_value); 434 connect(slider, &QSlider::valueChanged, [this, &option, i, slider_label](int value) { 435 const int new_value = std::clamp(option.min_value[i].int_value + (value * option.step_value[i].int_value), 436 option.min_value[i].int_value, option.max_value[i].int_value); 437 option.value[i].int_value = new_value; 438 slider_label->setText(QString::number(new_value)); 439 updateConfigForOption(option); 440 }); 441 } 442 else 443 { 444 slider_label->setText(QString::number(option.value[i].float_value)); 445 446 const float range = option.max_value[i].float_value - option.min_value[i].float_value; 447 const float step_value = 448 (option.step_value[i].float_value != 0) ? option.step_value[i].float_value : ((range + 99.0f) / 100.0f); 449 const float num_steps = std::ceil(range / step_value); 450 slider->setMinimum(0); 451 slider->setMaximum(num_steps); 452 slider->setSingleStep(1); 453 slider->setTickInterval(step_value); 454 slider->setValue( 455 static_cast<int>((option.value[i].float_value - option.min_value[i].float_value) / step_value)); 456 connect(slider, &QSlider::valueChanged, [this, &option, i, slider_label](int value) { 457 const float new_value = std::clamp(option.min_value[i].float_value + 458 (static_cast<float>(value) * option.step_value[i].float_value), 459 option.min_value[i].float_value, option.max_value[i].float_value); 460 option.value[i].float_value = new_value; 461 slider_label->setText(QString::number(new_value)); 462 updateConfigForOption(option); 463 }); 464 } 465 466 row++; 467 } 468 } 469 } 470 471 QDialogButtonBox* button_box = new QDialogButtonBox(QDialogButtonBox::RestoreDefaults, this); 472 connect(button_box, &QDialogButtonBox::clicked, this, &PostProcessingShaderConfigWidget::onResetDefaultsClicked); 473 m_layout->addWidget(button_box, row, 0, 1, -1); 474 475 row++; 476 m_layout->addItem(new QSpacerItem(1, 1, QSizePolicy::Minimum, QSizePolicy::Expanding), row, 0, 1, 3); 477 }