// Copyright 2020 Citra Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. #include #include #include #include "citra_qt/dumping/option_set_dialog.h" #include "common/logging/log.h" #include "common/string_util.h" #include "ui_option_set_dialog.h" extern "C" { #include } static const std::unordered_map TypeNameMap{{ {AV_OPT_TYPE_BOOL, QT_TR_NOOP("boolean")}, {AV_OPT_TYPE_FLAGS, QT_TR_NOOP("flags")}, {AV_OPT_TYPE_DURATION, QT_TR_NOOP("duration")}, {AV_OPT_TYPE_INT, QT_TR_NOOP("int")}, {AV_OPT_TYPE_UINT64, QT_TR_NOOP("uint64")}, {AV_OPT_TYPE_INT64, QT_TR_NOOP("int64")}, {AV_OPT_TYPE_DOUBLE, QT_TR_NOOP("double")}, {AV_OPT_TYPE_FLOAT, QT_TR_NOOP("float")}, {AV_OPT_TYPE_RATIONAL, QT_TR_NOOP("rational")}, {AV_OPT_TYPE_PIXEL_FMT, QT_TR_NOOP("pixel format")}, {AV_OPT_TYPE_SAMPLE_FMT, QT_TR_NOOP("sample format")}, {AV_OPT_TYPE_COLOR, QT_TR_NOOP("color")}, {AV_OPT_TYPE_IMAGE_SIZE, QT_TR_NOOP("image size")}, {AV_OPT_TYPE_STRING, QT_TR_NOOP("string")}, {AV_OPT_TYPE_DICT, QT_TR_NOOP("dictionary")}, {AV_OPT_TYPE_VIDEO_RATE, QT_TR_NOOP("video rate")}, {AV_OPT_TYPE_CHANNEL_LAYOUT, QT_TR_NOOP("channel layout")}, }}; static const std::unordered_map TypeDescriptionMap{{ {AV_OPT_TYPE_DURATION, QT_TR_NOOP("[<hours (integer)>:][<minutes (integer):]<seconds " "(decimal)> e.g. 03:00.5 (3min 500ms)")}, {AV_OPT_TYPE_RATIONAL, QT_TR_NOOP("<num>/<den>")}, {AV_OPT_TYPE_COLOR, QT_TR_NOOP("0xRRGGBBAA")}, {AV_OPT_TYPE_IMAGE_SIZE, QT_TR_NOOP("<width>x<height>, or preset values like 'vga'.")}, {AV_OPT_TYPE_DICT, QT_TR_NOOP("Comma-splitted list of <key>=<value>. Do not put spaces.")}, {AV_OPT_TYPE_VIDEO_RATE, QT_TR_NOOP("<num>/<den>, or preset values like 'pal'.")}, {AV_OPT_TYPE_CHANNEL_LAYOUT, QT_TR_NOOP("Hexadecimal channel layout mask starting with '0x'.")}, }}; /// Get the preset values of an option. returns {display value, real value} std::vector> GetPresetValues(const VideoDumper::OptionInfo& option) { switch (option.type) { case AV_OPT_TYPE_BOOL: { return {{QObject::tr("auto"), QStringLiteral("auto")}, {QObject::tr("true"), QStringLiteral("true")}, {QObject::tr("false"), QStringLiteral("false")}}; } case AV_OPT_TYPE_PIXEL_FMT: { std::vector> out{{QObject::tr("none"), QStringLiteral("none")}}; // List all pixel formats const AVPixFmtDescriptor* current = nullptr; while ((current = av_pix_fmt_desc_next(current))) { out.emplace_back(QString::fromUtf8(current->name), QString::fromUtf8(current->name)); } return out; } case AV_OPT_TYPE_SAMPLE_FMT: { std::vector> out{{QObject::tr("none"), QStringLiteral("none")}}; // List all sample formats int current = 0; while (true) { const char* name = av_get_sample_fmt_name(static_cast(current)); if (name == nullptr) break; out.emplace_back(QString::fromUtf8(name), QString::fromUtf8(name)); } return out; } case AV_OPT_TYPE_INT: case AV_OPT_TYPE_INT64: case AV_OPT_TYPE_UINT64: { std::vector> out; // Add in all named constants for (const auto& constant : option.named_constants) { out.emplace_back(QObject::tr("%1 (0x%2)") .arg(QString::fromStdString(constant.name)) .arg(constant.value, 0, 16), QString::fromStdString(constant.name)); } return out; } default: return {}; } } void OptionSetDialog::InitializeUI(const std::string& initial_value) { const QString type_name = TypeNameMap.count(option.type) ? tr(TypeNameMap.at(option.type)) : tr("unknown"); ui->nameLabel->setText(tr("%1 <%2> %3") .arg(QString::fromStdString(option.name), type_name, QString::fromStdString(option.description))); if (TypeDescriptionMap.count(option.type)) { ui->formatLabel->setVisible(true); ui->formatLabel->setText(tr(TypeDescriptionMap.at(option.type))); } if (option.type == AV_OPT_TYPE_INT || option.type == AV_OPT_TYPE_INT64 || option.type == AV_OPT_TYPE_UINT64 || option.type == AV_OPT_TYPE_FLOAT || option.type == AV_OPT_TYPE_DOUBLE || option.type == AV_OPT_TYPE_DURATION || option.type == AV_OPT_TYPE_RATIONAL) { // scalar types ui->formatLabel->setVisible(true); if (!ui->formatLabel->text().isEmpty()) { ui->formatLabel->text().append(QStringLiteral("\n")); } ui->formatLabel->setText( ui->formatLabel->text().append(tr("Range: %1 - %2").arg(option.min).arg(option.max))); } // Decide and initialize layout if (option.type == AV_OPT_TYPE_BOOL || option.type == AV_OPT_TYPE_PIXEL_FMT || option.type == AV_OPT_TYPE_SAMPLE_FMT || ((option.type == AV_OPT_TYPE_INT || option.type == AV_OPT_TYPE_INT64 || option.type == AV_OPT_TYPE_UINT64) && !option.named_constants.empty())) { // Use the combobox layout layout_type = 1; ui->comboBox->setVisible(true); ui->comboBoxHelpLabel->setVisible(true); QString real_initial_value = QString::fromStdString(initial_value); if (option.type == AV_OPT_TYPE_INT || option.type == AV_OPT_TYPE_INT64 || option.type == AV_OPT_TYPE_UINT64) { // Get the name of the initial value try { s64 initial_value_integer = std::stoll(initial_value, nullptr, 0); for (const auto& constant : option.named_constants) { if (constant.value == initial_value_integer) { real_initial_value = QString::fromStdString(constant.name); break; } } } catch (...) { // Not convertible to integer, ignore } } bool found = false; for (const auto& [display, value] : GetPresetValues(option)) { ui->comboBox->addItem(display, value); if (value == real_initial_value) { found = true; ui->comboBox->setCurrentIndex(ui->comboBox->count() - 1); } } ui->comboBox->addItem(tr("custom")); if (!found) { ui->comboBox->setCurrentIndex(ui->comboBox->count() - 1); ui->lineEdit->setText(QString::fromStdString(initial_value)); } UpdateUIDisplay(); connect(ui->comboBox, &QComboBox::currentTextChanged, this, &OptionSetDialog::UpdateUIDisplay); } else if (option.type == AV_OPT_TYPE_FLAGS && !option.named_constants.empty()) { // Use the check boxes layout layout_type = 2; for (const auto& constant : option.named_constants) { auto* checkBox = new QCheckBox(tr("%1 (0x%2) %3") .arg(QString::fromStdString(constant.name)) .arg(constant.value, 0, 16) .arg(QString::fromStdString(constant.description))); checkBox->setProperty("value", static_cast(constant.value)); checkBox->setProperty("name", QString::fromStdString(constant.name)); ui->checkBoxLayout->addWidget(checkBox); } SetCheckBoxDefaults(initial_value); } else { // Use the line edit layout layout_type = 0; ui->lineEdit->setVisible(true); ui->lineEdit->setText(QString::fromStdString(initial_value)); } adjustSize(); } void OptionSetDialog::SetCheckBoxDefaults(const std::string& initial_value) { if (initial_value.size() >= 2 && (initial_value.substr(0, 2) == "0x" || initial_value.substr(0, 2) == "0X")) { // This is a hex mask try { u64 value = std::stoull(initial_value, nullptr, 16); for (int i = 0; i < ui->checkBoxLayout->count(); ++i) { auto* checkBox = qobject_cast(ui->checkBoxLayout->itemAt(i)->widget()); if (checkBox) { checkBox->setChecked(value & checkBox->property("value").toULongLong()); } } } catch (...) { LOG_ERROR(Frontend, "Could not convert {} to number", initial_value); } } else { // This is a combination of constants, splitted with + or | std::vector tmp; Common::SplitString(initial_value, '+', tmp); std::vector out; std::vector tmp2; for (const auto& str : tmp) { Common::SplitString(str, '|', tmp2); out.insert(out.end(), tmp2.begin(), tmp2.end()); } for (int i = 0; i < ui->checkBoxLayout->count(); ++i) { auto* checkBox = qobject_cast(ui->checkBoxLayout->itemAt(i)->widget()); if (checkBox) { checkBox->setChecked( std::find(out.begin(), out.end(), checkBox->property("name").toString().toStdString()) != out.end()); } } } } void OptionSetDialog::UpdateUIDisplay() { if (layout_type != 1) return; if (ui->comboBox->currentIndex() == ui->comboBox->count() - 1) { // custom ui->comboBoxHelpLabel->setVisible(false); ui->lineEdit->setVisible(true); adjustSize(); return; } ui->lineEdit->setVisible(false); for (const auto& constant : option.named_constants) { if (constant.name == ui->comboBox->currentData().toString().toStdString()) { ui->comboBoxHelpLabel->setVisible(true); ui->comboBoxHelpLabel->setText(QString::fromStdString(constant.description)); return; } } } std::pair OptionSetDialog::GetCurrentValue() { if (!is_set) { return {}; } switch (layout_type) { case 0: // line edit layout return {true, ui->lineEdit->text().toStdString()}; case 1: // combo box layout if (ui->comboBox->currentIndex() == ui->comboBox->count() - 1) { return {true, ui->lineEdit->text().toStdString()}; // custom } return {true, ui->comboBox->currentData().toString().toStdString()}; case 2: { // check boxes layout std::string out; for (int i = 0; i < ui->checkBoxLayout->count(); ++i) { auto* checkBox = qobject_cast(ui->checkBoxLayout->itemAt(i)->widget()); if (checkBox && checkBox->isChecked()) { if (!out.empty()) { out.append("+"); } out.append(checkBox->property("name").toString().toStdString()); } } if (out.empty()) { out = "0x0"; } return {true, out}; } default: return {}; } } OptionSetDialog::OptionSetDialog(QWidget* parent, VideoDumper::OptionInfo option_, const std::string& initial_value) : QDialog(parent), ui(std::make_unique()), option(std::move(option_)) { ui->setupUi(this); InitializeUI(initial_value); connect(ui->unsetButton, &QPushButton::clicked, [this] { is_set = false; accept(); }); connect(ui->buttonBox, &QDialogButtonBox::accepted, this, &OptionSetDialog::accept); connect(ui->buttonBox, &QDialogButtonBox::rejected, this, &OptionSetDialog::reject); } OptionSetDialog::~OptionSetDialog() = default;