controllerbindingwidgets.cpp (32029B)
1 // SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin <stenzek@gmail.com> 2 // SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0) 3 4 #include "controllerbindingwidgets.h" 5 #include "controllersettingswindow.h" 6 #include "controllersettingwidgetbinder.h" 7 #include "qthost.h" 8 #include "qtutils.h" 9 #include "settingswindow.h" 10 #include "settingwidgetbinder.h" 11 12 #include "ui_controllerbindingwidget_analog_controller.h" 13 #include "ui_controllerbindingwidget_analog_joystick.h" 14 #include "ui_controllerbindingwidget_digital_controller.h" 15 #include "ui_controllerbindingwidget_guncon.h" 16 #include "ui_controllerbindingwidget_justifier.h" 17 #include "ui_controllerbindingwidget_mouse.h" 18 #include "ui_controllerbindingwidget_negcon.h" 19 #include "ui_controllerbindingwidget_negconrumble.h" 20 21 #include "core/controller.h" 22 #include "core/host.h" 23 24 #include "util/input_manager.h" 25 26 #include "common/log.h" 27 #include "common/string_util.h" 28 29 #include <QtWidgets/QCheckBox> 30 #include <QtWidgets/QDoubleSpinBox> 31 #include <QtWidgets/QInputDialog> 32 #include <QtWidgets/QLineEdit> 33 #include <QtWidgets/QMenu> 34 #include <QtWidgets/QMessageBox> 35 #include <QtWidgets/QScrollArea> 36 #include <QtWidgets/QSpinBox> 37 #include <algorithm> 38 39 Log_SetChannel(ControllerBindingWidget); 40 41 ControllerBindingWidget::ControllerBindingWidget(QWidget* parent, ControllerSettingsWindow* dialog, u32 port) 42 : QWidget(parent), m_dialog(dialog), m_config_section(Controller::GetSettingsSection(port)), m_port_number(port) 43 { 44 m_ui.setupUi(this); 45 populateControllerTypes(); 46 populateWidgets(); 47 48 connect(m_ui.controllerType, QOverload<int>::of(&QComboBox::currentIndexChanged), this, 49 &ControllerBindingWidget::onTypeChanged); 50 connect(m_ui.bindings, &QPushButton::clicked, this, &ControllerBindingWidget::onBindingsClicked); 51 connect(m_ui.settings, &QPushButton::clicked, this, &ControllerBindingWidget::onSettingsClicked); 52 connect(m_ui.macros, &QPushButton::clicked, this, &ControllerBindingWidget::onMacrosClicked); 53 connect(m_ui.automaticBinding, &QPushButton::clicked, this, &ControllerBindingWidget::onAutomaticBindingClicked); 54 connect(m_ui.clearBindings, &QPushButton::clicked, this, &ControllerBindingWidget::onClearBindingsClicked); 55 } 56 57 ControllerBindingWidget::~ControllerBindingWidget() = default; 58 59 void ControllerBindingWidget::populateControllerTypes() 60 { 61 for (u32 i = 0; i < static_cast<u32>(ControllerType::Count); i++) 62 { 63 const ControllerType ctype = static_cast<ControllerType>(i); 64 const Controller::ControllerInfo* cinfo = Controller::GetControllerInfo(ctype); 65 if (!cinfo) 66 continue; 67 68 m_ui.controllerType->addItem(QString::fromUtf8(cinfo->GetDisplayName()), QVariant(static_cast<int>(i))); 69 } 70 71 m_controller_info = Controller::GetControllerInfo( 72 m_dialog->getStringValue(m_config_section.c_str(), "Type", Controller::GetDefaultPadType(m_port_number))); 73 if (!m_controller_info) 74 { 75 m_controller_info = Controller::GetControllerInfo(m_port_number == 0 ? Settings::DEFAULT_CONTROLLER_1_TYPE : 76 Settings::DEFAULT_CONTROLLER_2_TYPE); 77 } 78 79 const int index = m_ui.controllerType->findData(QVariant(static_cast<int>(m_controller_info->type))); 80 if (index >= 0 && index != m_ui.controllerType->currentIndex()) 81 { 82 QSignalBlocker sb(m_ui.controllerType); 83 m_ui.controllerType->setCurrentIndex(index); 84 } 85 } 86 87 void ControllerBindingWidget::populateWidgets() 88 { 89 const bool is_initializing = (m_ui.stackedWidget->count() == 0); 90 if (m_bindings_widget) 91 { 92 m_ui.stackedWidget->removeWidget(m_bindings_widget); 93 delete m_bindings_widget; 94 m_bindings_widget = nullptr; 95 } 96 if (m_settings_widget) 97 { 98 m_ui.stackedWidget->removeWidget(m_settings_widget); 99 delete m_settings_widget; 100 m_settings_widget = nullptr; 101 } 102 if (m_macros_widget) 103 { 104 m_ui.stackedWidget->removeWidget(m_macros_widget); 105 delete m_macros_widget; 106 m_macros_widget = nullptr; 107 } 108 109 const bool has_settings = !m_controller_info->settings.empty(); 110 const bool has_macros = !m_controller_info->bindings.empty(); 111 m_ui.settings->setEnabled(has_settings); 112 m_ui.macros->setEnabled(has_macros); 113 114 m_bindings_widget = new QWidget(this); 115 switch (m_controller_info->type) 116 { 117 case ControllerType::AnalogController: 118 { 119 Ui::ControllerBindingWidget_AnalogController ui; 120 ui.setupUi(m_bindings_widget); 121 bindBindingWidgets(m_bindings_widget); 122 m_icon = QIcon::fromTheme(QStringLiteral("controller-line")); 123 } 124 break; 125 126 case ControllerType::AnalogJoystick: 127 { 128 Ui::ControllerBindingWidget_AnalogJoystick ui; 129 ui.setupUi(m_bindings_widget); 130 bindBindingWidgets(m_bindings_widget); 131 m_icon = QIcon::fromTheme(QStringLiteral("joystick-line")); 132 } 133 break; 134 135 case ControllerType::DigitalController: 136 { 137 Ui::ControllerBindingWidget_DigitalController ui; 138 ui.setupUi(m_bindings_widget); 139 bindBindingWidgets(m_bindings_widget); 140 m_icon = QIcon::fromTheme(QStringLiteral("controller-digital-line")); 141 } 142 break; 143 144 case ControllerType::GunCon: 145 { 146 Ui::ControllerBindingWidget_GunCon ui; 147 ui.setupUi(m_bindings_widget); 148 bindBindingWidgets(m_bindings_widget); 149 m_icon = QIcon::fromTheme(QStringLiteral("guncon-line")); 150 } 151 break; 152 153 case ControllerType::NeGcon: 154 { 155 Ui::ControllerBindingWidget_NeGcon ui; 156 ui.setupUi(m_bindings_widget); 157 bindBindingWidgets(m_bindings_widget); 158 m_icon = QIcon::fromTheme(QStringLiteral("negcon-line")); 159 } 160 break; 161 162 case ControllerType::NeGconRumble: 163 { 164 Ui::ControllerBindingWidget_NeGconRumble ui; 165 ui.setupUi(m_bindings_widget); 166 bindBindingWidgets(m_bindings_widget); 167 m_icon = QIcon::fromTheme(QStringLiteral("negcon-line")); 168 } 169 break; 170 171 case ControllerType::PlayStationMouse: 172 { 173 Ui::ControllerBindingWidget_Mouse ui; 174 ui.setupUi(m_bindings_widget); 175 bindBindingWidgets(m_bindings_widget); 176 m_icon = QIcon::fromTheme(QStringLiteral("mouse-line")); 177 } 178 break; 179 180 case ControllerType::Justifier: 181 { 182 Ui::ControllerBindingWidget_Justifier ui; 183 ui.setupUi(m_bindings_widget); 184 bindBindingWidgets(m_bindings_widget); 185 m_icon = QIcon::fromTheme(QStringLiteral("guncon-line")); 186 } 187 break; 188 189 case ControllerType::None: 190 { 191 m_icon = QIcon::fromTheme(QStringLiteral("controller-strike-line")); 192 } 193 break; 194 195 default: 196 { 197 createBindingWidgets(m_bindings_widget); 198 m_icon = QIcon::fromTheme(QStringLiteral("controller-line")); 199 } 200 break; 201 } 202 203 m_ui.stackedWidget->addWidget(m_bindings_widget); 204 m_ui.stackedWidget->setCurrentWidget(m_bindings_widget); 205 206 if (has_settings) 207 { 208 m_settings_widget = new ControllerCustomSettingsWidget(this); 209 m_ui.stackedWidget->addWidget(m_settings_widget); 210 } 211 212 if (has_macros) 213 { 214 m_macros_widget = new ControllerMacroWidget(this); 215 m_ui.stackedWidget->addWidget(m_macros_widget); 216 } 217 218 updateHeaderToolButtons(); 219 220 // no need to do this on first init, only changes 221 if (!is_initializing) 222 m_dialog->updateListDescription(m_port_number, this); 223 } 224 225 void ControllerBindingWidget::updateHeaderToolButtons() 226 { 227 const QWidget* current_widget = m_ui.stackedWidget->currentWidget(); 228 const QSignalBlocker bindings_sb(m_ui.bindings); 229 const QSignalBlocker settings_sb(m_ui.settings); 230 const QSignalBlocker macros_sb(m_ui.macros); 231 232 const bool is_bindings = (current_widget == m_bindings_widget); 233 m_ui.bindings->setChecked(is_bindings); 234 m_ui.automaticBinding->setEnabled(is_bindings); 235 m_ui.clearBindings->setEnabled(is_bindings); 236 m_ui.macros->setChecked(current_widget == m_macros_widget); 237 m_ui.settings->setChecked((current_widget == m_settings_widget)); 238 } 239 240 void ControllerBindingWidget::onTypeChanged() 241 { 242 bool ok; 243 const int index = m_ui.controllerType->currentData().toInt(&ok); 244 if (!ok || index < 0 || index >= static_cast<int>(ControllerType::Count)) 245 return; 246 247 m_controller_info = Controller::GetControllerInfo(static_cast<ControllerType>(index)); 248 DebugAssert(m_controller_info); 249 250 SettingsInterface* sif = m_dialog->getEditingSettingsInterface(); 251 if (sif) 252 { 253 sif->SetStringValue(m_config_section.c_str(), "Type", m_controller_info->name); 254 QtHost::SaveGameSettings(sif, false); 255 g_emu_thread->reloadGameSettings(); 256 } 257 else 258 { 259 Host::SetBaseStringSettingValue(m_config_section.c_str(), "Type", m_controller_info->name); 260 Host::CommitBaseSettingChanges(); 261 g_emu_thread->applySettings(); 262 } 263 264 populateWidgets(); 265 } 266 267 void ControllerBindingWidget::onAutomaticBindingClicked() 268 { 269 QMenu menu(this); 270 bool added = false; 271 272 for (const auto& [identifier, device_name] : m_dialog->getDeviceList()) 273 { 274 // we set it as data, because the device list could get invalidated while the menu is up 275 const QString qidentifier = QString::fromStdString(identifier); 276 QAction* action = 277 menu.addAction(QStringLiteral("%1 (%2)").arg(qidentifier).arg(QString::fromStdString(device_name))); 278 action->setData(qidentifier); 279 connect(action, &QAction::triggered, this, 280 [this, action]() { doDeviceAutomaticBinding(action->data().toString()); }); 281 added = true; 282 } 283 284 if (!added) 285 { 286 QAction* action = menu.addAction(tr("No devices available")); 287 action->setEnabled(false); 288 } 289 290 menu.exec(QCursor::pos()); 291 } 292 293 void ControllerBindingWidget::onClearBindingsClicked() 294 { 295 if (QMessageBox::question( 296 QtUtils::GetRootWidget(this), tr("Clear Mapping"), 297 tr("Are you sure you want to clear all mappings for this controller? This action cannot be undone.")) != 298 QMessageBox::Yes) 299 { 300 return; 301 } 302 303 if (m_dialog->isEditingGlobalSettings()) 304 { 305 auto lock = Host::GetSettingsLock(); 306 InputManager::ClearPortBindings(*Host::Internal::GetBaseSettingsLayer(), m_port_number); 307 } 308 else 309 { 310 InputManager::ClearPortBindings(*m_dialog->getEditingSettingsInterface(), m_port_number); 311 } 312 313 saveAndRefresh(); 314 } 315 316 void ControllerBindingWidget::onBindingsClicked() 317 { 318 m_ui.stackedWidget->setCurrentWidget(m_bindings_widget); 319 updateHeaderToolButtons(); 320 } 321 322 void ControllerBindingWidget::onSettingsClicked() 323 { 324 if (!m_settings_widget) 325 return; 326 327 m_ui.stackedWidget->setCurrentWidget(m_settings_widget); 328 updateHeaderToolButtons(); 329 } 330 331 void ControllerBindingWidget::onMacrosClicked() 332 { 333 if (!m_macros_widget) 334 return; 335 336 m_ui.stackedWidget->setCurrentWidget(m_macros_widget); 337 updateHeaderToolButtons(); 338 } 339 340 void ControllerBindingWidget::doDeviceAutomaticBinding(const QString& device) 341 { 342 std::vector<std::pair<GenericInputBinding, std::string>> mapping = 343 InputManager::GetGenericBindingMapping(device.toStdString()); 344 if (mapping.empty()) 345 { 346 QMessageBox::critical( 347 QtUtils::GetRootWidget(this), tr("Automatic Mapping"), 348 tr("No generic bindings were generated for device '%1'. The controller/source may not support automatic mapping.") 349 .arg(device)); 350 return; 351 } 352 353 bool result; 354 if (m_dialog->isEditingGlobalSettings()) 355 { 356 auto lock = Host::GetSettingsLock(); 357 result = InputManager::MapController(*Host::Internal::GetBaseSettingsLayer(), m_port_number, mapping); 358 } 359 else 360 { 361 result = InputManager::MapController(*m_dialog->getEditingSettingsInterface(), m_port_number, mapping); 362 QtHost::SaveGameSettings(m_dialog->getEditingSettingsInterface(), false); 363 g_emu_thread->reloadInputBindings(); 364 } 365 366 // force a refresh after mapping 367 if (result) 368 saveAndRefresh(); 369 } 370 371 void ControllerBindingWidget::saveAndRefresh() 372 { 373 onTypeChanged(); 374 QtHost::QueueSettingsSave(); 375 g_emu_thread->applySettings(); 376 } 377 378 void ControllerBindingWidget::createBindingWidgets(QWidget* parent) 379 { 380 SettingsInterface* sif = getDialog()->getEditingSettingsInterface(); 381 DebugAssert(m_controller_info); 382 383 QGroupBox* axis_gbox = nullptr; 384 QGridLayout* axis_layout = nullptr; 385 QGroupBox* button_gbox = nullptr; 386 QGridLayout* button_layout = nullptr; 387 388 QScrollArea* scrollarea = new QScrollArea(parent); 389 QWidget* scrollarea_widget = new QWidget(scrollarea); 390 scrollarea->setWidget(scrollarea_widget); 391 scrollarea->setWidgetResizable(true); 392 scrollarea->setFrameShape(QFrame::StyledPanel); 393 scrollarea->setFrameShadow(QFrame::Sunken); 394 395 // We do axes and buttons separately, so we can figure out how many columns to use. 396 constexpr int NUM_AXIS_COLUMNS = 2; 397 int column = 0; 398 int row = 0; 399 for (const Controller::ControllerBindingInfo& bi : m_controller_info->bindings) 400 { 401 if (bi.type == InputBindingInfo::Type::Axis || bi.type == InputBindingInfo::Type::HalfAxis || 402 bi.type == InputBindingInfo::Type::Pointer) 403 { 404 if (!axis_gbox) 405 { 406 axis_gbox = new QGroupBox(tr("Axes"), scrollarea_widget); 407 axis_layout = new QGridLayout(axis_gbox); 408 } 409 410 QGroupBox* gbox = new QGroupBox(qApp->translate("USB", bi.display_name), axis_gbox); 411 QVBoxLayout* temp = new QVBoxLayout(gbox); 412 InputBindingWidget* widget = new InputBindingWidget(gbox, sif, bi.type, getConfigSection(), bi.name); 413 temp->addWidget(widget); 414 axis_layout->addWidget(gbox, row, column); 415 if ((++column) == NUM_AXIS_COLUMNS) 416 { 417 column = 0; 418 row++; 419 } 420 } 421 } 422 if (axis_gbox) 423 axis_layout->addItem(new QSpacerItem(1, 1, QSizePolicy::Minimum, QSizePolicy::Expanding), ++row, 0); 424 425 const int num_button_columns = axis_layout ? 2 : 4; 426 row = 0; 427 column = 0; 428 for (const Controller::ControllerBindingInfo& bi : m_controller_info->bindings) 429 { 430 if (bi.type == InputBindingInfo::Type::Button) 431 { 432 if (!button_gbox) 433 { 434 button_gbox = new QGroupBox(tr("Buttons"), scrollarea_widget); 435 button_layout = new QGridLayout(button_gbox); 436 } 437 438 QGroupBox* gbox = new QGroupBox(qApp->translate("USB", bi.display_name), button_gbox); 439 QVBoxLayout* temp = new QVBoxLayout(gbox); 440 InputBindingWidget* widget = new InputBindingWidget(gbox, sif, bi.type, getConfigSection(), bi.name); 441 temp->addWidget(widget); 442 button_layout->addWidget(gbox, row, column); 443 if ((++column) == num_button_columns) 444 { 445 column = 0; 446 row++; 447 } 448 } 449 } 450 451 if (button_gbox) 452 button_layout->addItem(new QSpacerItem(1, 1, QSizePolicy::Minimum, QSizePolicy::Expanding), ++row, 0); 453 454 if (!axis_gbox && !button_gbox) 455 { 456 delete scrollarea_widget; 457 delete scrollarea; 458 return; 459 } 460 461 QHBoxLayout* layout = new QHBoxLayout(scrollarea_widget); 462 if (axis_gbox) 463 layout->addWidget(axis_gbox); 464 if (button_gbox) 465 layout->addWidget(button_gbox); 466 layout->addItem(new QSpacerItem(1, 1, QSizePolicy::Expanding, QSizePolicy::Minimum)); 467 468 QHBoxLayout* main_layout = new QHBoxLayout(parent); 469 main_layout->addWidget(scrollarea); 470 } 471 472 void ControllerBindingWidget::bindBindingWidgets(QWidget* parent) 473 { 474 SettingsInterface* sif = getDialog()->getEditingSettingsInterface(); 475 DebugAssert(m_controller_info); 476 477 const std::string& config_section = getConfigSection(); 478 for (const Controller::ControllerBindingInfo& bi : m_controller_info->bindings) 479 { 480 if (bi.type == InputBindingInfo::Type::Axis || bi.type == InputBindingInfo::Type::HalfAxis || 481 bi.type == InputBindingInfo::Type::Button || bi.type == InputBindingInfo::Type::Pointer) 482 { 483 InputBindingWidget* widget = parent->findChild<InputBindingWidget*>(QString::fromUtf8(bi.name)); 484 if (!widget) 485 { 486 ERROR_LOG("No widget found for '{}' ({})", bi.name, m_controller_info->name); 487 continue; 488 } 489 490 widget->initialize(sif, bi.type, config_section, bi.name); 491 } 492 } 493 494 switch (m_controller_info->vibration_caps) 495 { 496 case Controller::VibrationCapabilities::LargeSmallMotors: 497 { 498 InputVibrationBindingWidget* widget = 499 parent->findChild<InputVibrationBindingWidget*>(QStringLiteral("LargeMotor")); 500 if (widget) 501 widget->setKey(getDialog(), config_section, "LargeMotor"); 502 503 widget = parent->findChild<InputVibrationBindingWidget*>(QStringLiteral("SmallMotor")); 504 if (widget) 505 widget->setKey(getDialog(), config_section, "SmallMotor"); 506 } 507 break; 508 509 case Controller::VibrationCapabilities::SingleMotor: 510 { 511 InputVibrationBindingWidget* widget = parent->findChild<InputVibrationBindingWidget*>(QStringLiteral("Motor")); 512 if (widget) 513 widget->setKey(getDialog(), config_section, "Motor"); 514 } 515 break; 516 517 case Controller::VibrationCapabilities::NoVibration: 518 default: 519 break; 520 } 521 } 522 523 ////////////////////////////////////////////////////////////////////////// 524 525 ControllerMacroWidget::ControllerMacroWidget(ControllerBindingWidget* parent) : QWidget(parent) 526 { 527 m_ui.setupUi(this); 528 setWindowTitle(tr("Controller Port %1 Macros").arg(parent->getPortNumber() + 1u)); 529 createWidgets(parent); 530 } 531 532 ControllerMacroWidget::~ControllerMacroWidget() = default; 533 534 void ControllerMacroWidget::updateListItem(u32 index) 535 { 536 m_ui.portList->item(static_cast<int>(index)) 537 ->setText(tr("Macro %1\n%2").arg(index + 1).arg(m_macros[index]->getSummary())); 538 } 539 540 void ControllerMacroWidget::createWidgets(ControllerBindingWidget* parent) 541 { 542 for (u32 i = 0; i < NUM_MACROS; i++) 543 { 544 m_macros[i] = new ControllerMacroEditWidget(this, parent, i); 545 m_ui.container->addWidget(m_macros[i]); 546 547 QListWidgetItem* item = new QListWidgetItem(); 548 item->setIcon(QIcon::fromTheme(QStringLiteral("flashlight-line"))); 549 m_ui.portList->addItem(item); 550 updateListItem(i); 551 } 552 553 m_ui.portList->setCurrentRow(0); 554 m_ui.container->setCurrentIndex(0); 555 556 connect(m_ui.portList, &QListWidget::currentRowChanged, m_ui.container, &QStackedWidget::setCurrentIndex); 557 } 558 559 ////////////////////////////////////////////////////////////////////////// 560 561 ControllerMacroEditWidget::ControllerMacroEditWidget(ControllerMacroWidget* parent, ControllerBindingWidget* bwidget, 562 u32 index) 563 : QWidget(parent), m_parent(parent), m_bwidget(bwidget), m_index(index) 564 { 565 m_ui.setupUi(this); 566 567 ControllerSettingsWindow* dialog = m_bwidget->getDialog(); 568 const std::string& section = m_bwidget->getConfigSection(); 569 const Controller::ControllerInfo* cinfo = m_bwidget->getControllerInfo(); 570 DebugAssert(cinfo); 571 572 // load binds (single string joined by &) 573 const std::string binds_string( 574 dialog->getStringValue(section.c_str(), TinyString::from_format("Macro{}Binds", index + 1u), "")); 575 const std::vector<std::string_view> buttons_split(StringUtil::SplitString(binds_string, '&', true)); 576 577 for (const std::string_view& button : buttons_split) 578 { 579 for (const Controller::ControllerBindingInfo& bi : cinfo->bindings) 580 { 581 if (button == bi.name) 582 { 583 m_binds.push_back(&bi); 584 break; 585 } 586 } 587 } 588 589 // populate list view 590 for (const Controller::ControllerBindingInfo& bi : cinfo->bindings) 591 { 592 if (bi.type == InputBindingInfo::Type::Motor) 593 continue; 594 595 QListWidgetItem* item = new QListWidgetItem(); 596 item->setText(qApp->translate(cinfo->name, bi.display_name)); 597 item->setCheckState((std::find(m_binds.begin(), m_binds.end(), &bi) != m_binds.end()) ? Qt::Checked : 598 Qt::Unchecked); 599 m_ui.bindList->addItem(item); 600 } 601 602 m_frequency = dialog->getIntValue(section.c_str(), TinyString::from_format("Macro{}Frequency", index + 1u), 0); 603 ControllerSettingWidgetBinder::BindWidgetToInputProfileBool(dialog->getEditingSettingsInterface(), m_ui.triggerToggle, 604 section.c_str(), fmt::format("Macro{}Toggle", index + 1u), 605 false); 606 updateFrequencyText(); 607 608 m_ui.trigger->initialize(dialog->getEditingSettingsInterface(), InputBindingInfo::Type::Macro, section, 609 fmt::format("Macro{}", index + 1u)); 610 611 connect(m_ui.increaseFrequency, &QAbstractButton::clicked, this, [this]() { modFrequency(1); }); 612 connect(m_ui.decreateFrequency, &QAbstractButton::clicked, this, [this]() { modFrequency(-1); }); 613 connect(m_ui.setFrequency, &QAbstractButton::clicked, this, &ControllerMacroEditWidget::onSetFrequencyClicked); 614 connect(m_ui.bindList, &QListWidget::itemChanged, this, &ControllerMacroEditWidget::updateBinds); 615 } 616 617 ControllerMacroEditWidget::~ControllerMacroEditWidget() = default; 618 619 QString ControllerMacroEditWidget::getSummary() const 620 { 621 SmallString str; 622 for (const Controller::ControllerBindingInfo* bi : m_binds) 623 { 624 if (!str.empty()) 625 str.append('/'); 626 str.append(bi->name); 627 } 628 return str.empty() ? tr("Not Configured") : QString::fromUtf8(str.c_str(), static_cast<int>(str.length())); 629 } 630 631 void ControllerMacroEditWidget::onSetFrequencyClicked() 632 { 633 bool okay; 634 int new_freq = QInputDialog::getInt(this, tr("Set Frequency"), tr("Frequency: "), static_cast<int>(m_frequency), 0, 635 std::numeric_limits<int>::max(), 1, &okay); 636 if (!okay) 637 return; 638 639 m_frequency = static_cast<u32>(new_freq); 640 updateFrequency(); 641 } 642 643 void ControllerMacroEditWidget::modFrequency(s32 delta) 644 { 645 if (delta < 0 && m_frequency == 0) 646 return; 647 648 m_frequency = static_cast<u32>(static_cast<s32>(m_frequency) + delta); 649 updateFrequency(); 650 } 651 652 void ControllerMacroEditWidget::updateFrequency() 653 { 654 m_bwidget->getDialog()->setIntValue(m_bwidget->getConfigSection().c_str(), 655 fmt::format("Macro{}Frequency", m_index + 1u).c_str(), 656 static_cast<s32>(m_frequency)); 657 updateFrequencyText(); 658 } 659 660 void ControllerMacroEditWidget::updateFrequencyText() 661 { 662 if (m_frequency == 0) 663 m_ui.frequencyText->setText(tr("Macro will not repeat.")); 664 else 665 m_ui.frequencyText->setText(tr("Macro will toggle buttons every %1 frames.").arg(m_frequency)); 666 } 667 668 void ControllerMacroEditWidget::updateBinds() 669 { 670 ControllerSettingsWindow* dialog = m_bwidget->getDialog(); 671 const Controller::ControllerInfo* cinfo = m_bwidget->getControllerInfo(); 672 DebugAssert(cinfo); 673 674 std::vector<const Controller::ControllerBindingInfo*> new_binds; 675 u32 bind_index = 0; 676 for (const Controller::ControllerBindingInfo& bi : cinfo->bindings) 677 { 678 if (bi.type == InputBindingInfo::Type::Motor) 679 continue; 680 681 const QListWidgetItem* item = m_ui.bindList->item(static_cast<int>(bind_index)); 682 bind_index++; 683 684 if (!item) 685 { 686 // shouldn't happen 687 continue; 688 } 689 690 if (item->checkState() == Qt::Checked) 691 new_binds.push_back(&bi); 692 } 693 if (m_binds == new_binds) 694 return; 695 696 m_binds = std::move(new_binds); 697 698 std::string binds_string; 699 for (const Controller::ControllerBindingInfo* bi : m_binds) 700 { 701 if (!binds_string.empty()) 702 binds_string.append(" & "); 703 binds_string.append(bi->name); 704 } 705 706 const std::string& section = m_bwidget->getConfigSection(); 707 const std::string key(fmt::format("Macro{}Binds", m_index + 1u)); 708 if (binds_string.empty()) 709 dialog->clearSettingValue(section.c_str(), key.c_str()); 710 else 711 dialog->setStringValue(section.c_str(), key.c_str(), binds_string.c_str()); 712 713 m_parent->updateListItem(m_index); 714 } 715 716 ////////////////////////////////////////////////////////////////////////// 717 718 ControllerCustomSettingsWidget::ControllerCustomSettingsWidget(ControllerBindingWidget* parent) 719 : QWidget(parent), m_parent(parent) 720 { 721 const Controller::ControllerInfo* cinfo = parent->getControllerInfo(); 722 DebugAssert(cinfo); 723 if (cinfo->settings.empty()) 724 return; 725 726 QScrollArea* sarea = new QScrollArea(this); 727 QWidget* swidget = new QWidget(sarea); 728 sarea->setWidget(swidget); 729 sarea->setWidgetResizable(true); 730 sarea->setFrameShape(QFrame::StyledPanel); 731 sarea->setFrameShadow(QFrame::Sunken); 732 733 QGridLayout* swidget_layout = new QGridLayout(swidget); 734 createSettingWidgets(parent, swidget, swidget_layout, cinfo); 735 736 QVBoxLayout* layout = new QVBoxLayout(this); 737 layout->setContentsMargins(0, 0, 0, 0); 738 layout->addWidget(sarea); 739 } 740 741 ControllerCustomSettingsWidget::~ControllerCustomSettingsWidget() 742 { 743 } 744 745 void ControllerCustomSettingsWidget::createSettingWidgets(ControllerBindingWidget* parent, QWidget* parent_widget, 746 QGridLayout* layout, const Controller::ControllerInfo* cinfo) 747 { 748 const std::string& section = parent->getConfigSection(); 749 SettingsInterface* sif = parent->getDialog()->getEditingSettingsInterface(); 750 int current_row = 0; 751 752 for (const SettingInfo& si : cinfo->settings) 753 { 754 std::string key_name = si.name; 755 756 switch (si.type) 757 { 758 case SettingInfo::Type::Boolean: 759 { 760 QCheckBox* cb = new QCheckBox(qApp->translate(cinfo->name, si.display_name), this); 761 cb->setObjectName(QString::fromUtf8(si.name)); 762 ControllerSettingWidgetBinder::BindWidgetToInputProfileBool(sif, cb, section, std::move(key_name), 763 si.BooleanDefaultValue()); 764 layout->addWidget(cb, current_row, 0, 1, 4); 765 current_row++; 766 } 767 break; 768 769 case SettingInfo::Type::Integer: 770 { 771 QSpinBox* sb = new QSpinBox(this); 772 sb->setObjectName(QString::fromUtf8(si.name)); 773 sb->setMinimum(si.IntegerMinValue()); 774 sb->setMaximum(si.IntegerMaxValue()); 775 sb->setSingleStep(si.IntegerStepValue()); 776 SettingWidgetBinder::BindWidgetToIntSetting(sif, sb, section, std::move(key_name), si.IntegerDefaultValue()); 777 layout->addWidget(new QLabel(qApp->translate(cinfo->name, si.display_name), this), current_row, 0); 778 layout->addWidget(sb, current_row, 1, 1, 3); 779 current_row++; 780 } 781 break; 782 783 case SettingInfo::Type::IntegerList: 784 { 785 QComboBox* cb = new QComboBox(this); 786 cb->setObjectName(QString::fromUtf8(si.name)); 787 for (u32 j = 0; si.options[j] != nullptr; j++) 788 cb->addItem(qApp->translate(cinfo->name, si.options[j])); 789 SettingWidgetBinder::BindWidgetToIntSetting(sif, cb, section, std::move(key_name), si.IntegerDefaultValue(), 790 si.IntegerMinValue()); 791 layout->addWidget(new QLabel(qApp->translate(cinfo->name, si.display_name), this), current_row, 0); 792 layout->addWidget(cb, current_row, 1, 1, 3); 793 current_row++; 794 } 795 break; 796 797 case SettingInfo::Type::Float: 798 { 799 QDoubleSpinBox* sb = new QDoubleSpinBox(this); 800 sb->setObjectName(QString::fromUtf8(si.name)); 801 if (si.multiplier != 0.0f && si.multiplier != 1.0f) 802 { 803 const float multiplier = si.multiplier; 804 sb->setMinimum(si.FloatMinValue() * multiplier); 805 sb->setMaximum(si.FloatMaxValue() * multiplier); 806 sb->setSingleStep(si.FloatStepValue() * multiplier); 807 if (std::abs(si.multiplier - 100.0f) < 0.01f) 808 { 809 sb->setDecimals(0); 810 sb->setSuffix(QStringLiteral("%")); 811 } 812 813 SettingWidgetBinder::BindWidgetToNormalizedSetting(sif, sb, section, std::move(key_name), si.multiplier, 814 si.FloatDefaultValue()); 815 } 816 else 817 { 818 sb->setMinimum(si.FloatMinValue()); 819 sb->setMaximum(si.FloatMaxValue()); 820 sb->setSingleStep(si.FloatStepValue()); 821 822 SettingWidgetBinder::BindWidgetToFloatSetting(sif, sb, section, std::move(key_name), si.FloatDefaultValue()); 823 } 824 layout->addWidget(new QLabel(qApp->translate(cinfo->name, si.display_name), this), current_row, 0); 825 layout->addWidget(sb, current_row, 1, 1, 3); 826 current_row++; 827 } 828 break; 829 830 case SettingInfo::Type::String: 831 { 832 QLineEdit* le = new QLineEdit(this); 833 le->setObjectName(QString::fromUtf8(si.name)); 834 SettingWidgetBinder::BindWidgetToStringSetting(sif, le, section, std::move(key_name), si.StringDefaultValue()); 835 layout->addWidget(new QLabel(qApp->translate(cinfo->name, si.display_name), this), current_row, 0); 836 layout->addWidget(le, current_row, 1, 1, 3); 837 current_row++; 838 } 839 break; 840 841 case SettingInfo::Type::Path: 842 { 843 QLineEdit* le = new QLineEdit(this); 844 le->setObjectName(QString::fromUtf8(si.name)); 845 QPushButton* browse_button = new QPushButton(tr("Browse..."), this); 846 SettingWidgetBinder::BindWidgetToStringSetting(sif, le, section, std::move(key_name), si.StringDefaultValue()); 847 connect(browse_button, &QPushButton::clicked, [this, le]() { 848 QString path = QDir::toNativeSeparators(QFileDialog::getOpenFileName(this, tr("Select File"))); 849 if (!path.isEmpty()) 850 le->setText(path); 851 }); 852 853 QHBoxLayout* hbox = new QHBoxLayout(); 854 hbox->addWidget(le, 1); 855 hbox->addWidget(browse_button); 856 857 layout->addWidget(new QLabel(qApp->translate(cinfo->name, si.display_name), this), current_row, 0); 858 layout->addLayout(hbox, current_row, 1, 1, 3); 859 current_row++; 860 } 861 break; 862 } 863 864 QLabel* label = new QLabel(si.description ? qApp->translate(cinfo->name, si.description) : QString(), this); 865 label->setWordWrap(true); 866 layout->addWidget(label, current_row++, 0, 1, 4); 867 868 layout->addItem(new QSpacerItem(1, 10, QSizePolicy::Minimum, QSizePolicy::Fixed), current_row++, 0, 1, 4); 869 } 870 871 QHBoxLayout* bottom_hlayout = new QHBoxLayout(); 872 QPushButton* restore_defaults = new QPushButton(tr("Restore Default Settings"), this); 873 restore_defaults->setIcon(QIcon::fromTheme(QStringLiteral("restart-line"))); 874 connect(restore_defaults, &QPushButton::clicked, this, &ControllerCustomSettingsWidget::restoreDefaults); 875 bottom_hlayout->addStretch(1); 876 bottom_hlayout->addWidget(restore_defaults); 877 layout->addLayout(bottom_hlayout, current_row++, 0, 1, 4); 878 879 layout->addItem(new QSpacerItem(1, 1, QSizePolicy::Minimum, QSizePolicy::Expanding), current_row++, 0, 1, 4); 880 } 881 882 void ControllerCustomSettingsWidget::restoreDefaults() 883 { 884 const Controller::ControllerInfo* cinfo = m_parent->getControllerInfo(); 885 DebugAssert(cinfo); 886 if (cinfo->settings.empty()) 887 return; 888 889 for (const SettingInfo& si : cinfo->settings) 890 { 891 const QString key(QString::fromStdString(si.name)); 892 893 switch (si.type) 894 { 895 case SettingInfo::Type::Boolean: 896 { 897 QCheckBox* widget = findChild<QCheckBox*>(QString::fromStdString(si.name)); 898 if (widget) 899 widget->setChecked(si.BooleanDefaultValue()); 900 } 901 break; 902 903 case SettingInfo::Type::Integer: 904 { 905 QSpinBox* widget = findChild<QSpinBox*>(QString::fromStdString(si.name)); 906 if (widget) 907 widget->setValue(si.IntegerDefaultValue()); 908 } 909 break; 910 911 case SettingInfo::Type::IntegerList: 912 { 913 QComboBox* widget = findChild<QComboBox*>(QString::fromStdString(si.name)); 914 if (widget) 915 widget->setCurrentIndex(si.IntegerDefaultValue() - si.IntegerMinValue()); 916 } 917 break; 918 919 case SettingInfo::Type::Float: 920 { 921 QDoubleSpinBox* widget = findChild<QDoubleSpinBox*>(QString::fromStdString(si.name)); 922 if (widget) 923 { 924 if (si.multiplier != 0.0f && si.multiplier != 1.0f) 925 widget->setValue(si.FloatDefaultValue() * si.multiplier); 926 else 927 widget->setValue(si.FloatDefaultValue()); 928 } 929 } 930 break; 931 932 case SettingInfo::Type::String: 933 { 934 QLineEdit* widget = findChild<QLineEdit*>(QString::fromStdString(si.name)); 935 if (widget) 936 widget->setText(QString::fromUtf8(si.StringDefaultValue())); 937 } 938 break; 939 940 case SettingInfo::Type::Path: 941 { 942 QLineEdit* widget = findChild<QLineEdit*>(QString::fromStdString(si.name)); 943 if (widget) 944 widget->setText(QString::fromUtf8(si.StringDefaultValue())); 945 } 946 break; 947 } 948 } 949 }