setupwizarddialog.cpp (17322B)
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 "setupwizarddialog.h" 5 #include "controllersettingwidgetbinder.h" 6 #include "interfacesettingswidget.h" 7 #include "mainwindow.h" 8 #include "qthost.h" 9 #include "qtutils.h" 10 #include "settingwidgetbinder.h" 11 12 #include "core/controller.h" 13 14 #include "util/input_manager.h" 15 16 #include "common/file_system.h" 17 18 #include <QtWidgets/QMessageBox> 19 20 SetupWizardDialog::SetupWizardDialog() 21 { 22 setupUi(); 23 updatePageLabels(-1); 24 updatePageButtons(); 25 } 26 27 SetupWizardDialog::~SetupWizardDialog() = default; 28 29 void SetupWizardDialog::resizeEvent(QResizeEvent* event) 30 { 31 QDialog::resizeEvent(event); 32 resizeDirectoryListColumns(); 33 } 34 35 bool SetupWizardDialog::canShowNextPage() 36 { 37 const int current_page = m_ui.pages->currentIndex(); 38 39 switch (current_page) 40 { 41 case Page_BIOS: 42 { 43 if (!BIOS::HasAnyBIOSImages()) 44 { 45 if (QMessageBox::question( 46 this, tr("Warning"), 47 tr("No BIOS images were found. DuckStation WILL NOT be able to run games without a BIOS image.\n\nAre " 48 "you sure you wish to continue without selecting a BIOS image?")) != QMessageBox::Yes) 49 { 50 return false; 51 } 52 } 53 } 54 break; 55 56 case Page_GameList: 57 { 58 if (m_ui.searchDirectoryList->rowCount() == 0) 59 { 60 if (QMessageBox::question( 61 this, tr("Warning"), 62 tr("No game directories have been selected. You will have to manually open any game dumps you " 63 "want to play, DuckStation's list will be empty.\n\nAre you sure you want to continue?")) != 64 QMessageBox::Yes) 65 { 66 return false; 67 } 68 } 69 } 70 break; 71 72 default: 73 break; 74 } 75 76 return true; 77 } 78 79 void SetupWizardDialog::previousPage() 80 { 81 const int current_page = m_ui.pages->currentIndex(); 82 if (current_page == 0) 83 return; 84 85 m_ui.pages->setCurrentIndex(current_page - 1); 86 updatePageLabels(current_page); 87 updatePageButtons(); 88 } 89 90 void SetupWizardDialog::nextPage() 91 { 92 const int current_page = m_ui.pages->currentIndex(); 93 if (current_page == Page_Complete) 94 { 95 accept(); 96 return; 97 } 98 99 if (!canShowNextPage()) 100 return; 101 102 const int new_page = current_page + 1; 103 m_ui.pages->setCurrentIndex(new_page); 104 updatePageLabels(current_page); 105 updatePageButtons(); 106 pageChangedTo(new_page); 107 } 108 109 void SetupWizardDialog::pageChangedTo(int page) 110 { 111 switch (page) 112 { 113 case Page_GameList: 114 resizeDirectoryListColumns(); 115 break; 116 117 default: 118 break; 119 } 120 } 121 122 void SetupWizardDialog::updatePageLabels(int prev_page) 123 { 124 if (prev_page >= 0) 125 { 126 QFont prev_font = m_page_labels[prev_page]->font(); 127 prev_font.setBold(false); 128 m_page_labels[prev_page]->setFont(prev_font); 129 } 130 131 const int page = m_ui.pages->currentIndex(); 132 QFont font = m_page_labels[page]->font(); 133 font.setBold(true); 134 m_page_labels[page]->setFont(font); 135 } 136 137 void SetupWizardDialog::updatePageButtons() 138 { 139 const int page = m_ui.pages->currentIndex(); 140 m_ui.next->setText((page == Page_Complete) ? tr("&Finish") : tr("&Next")); 141 m_ui.back->setEnabled(page > 0); 142 } 143 144 void SetupWizardDialog::confirmCancel() 145 { 146 if (QMessageBox::question(this, tr("Cancel Setup"), 147 tr("Are you sure you want to cancel DuckStation setup?\n\nAny changes have been saved, and " 148 "the wizard will run again next time you start DuckStation.")) != QMessageBox::Yes) 149 { 150 return; 151 } 152 153 reject(); 154 } 155 156 void SetupWizardDialog::setupUi() 157 { 158 m_ui.setupUi(this); 159 160 m_ui.logo->setPixmap( 161 QPixmap(QString::fromUtf8(Path::Combine(EmuFolders::Resources, "images" FS_OSPATH_SEPARATOR_STR "duck.png")))); 162 163 m_ui.pages->setCurrentIndex(0); 164 165 m_page_labels[Page_Language] = m_ui.labelLanguage; 166 m_page_labels[Page_BIOS] = m_ui.labelBIOS; 167 m_page_labels[Page_GameList] = m_ui.labelGameList; 168 m_page_labels[Page_Controller] = m_ui.labelController; 169 m_page_labels[Page_Complete] = m_ui.labelComplete; 170 171 connect(m_ui.back, &QPushButton::clicked, this, &SetupWizardDialog::previousPage); 172 connect(m_ui.next, &QPushButton::clicked, this, &SetupWizardDialog::nextPage); 173 connect(m_ui.cancel, &QPushButton::clicked, this, &SetupWizardDialog::confirmCancel); 174 175 setupLanguagePage(); 176 setupBIOSPage(); 177 setupGameListPage(); 178 setupControllerPage(true); 179 } 180 181 void SetupWizardDialog::setupLanguagePage() 182 { 183 SettingWidgetBinder::BindWidgetToEnumSetting(nullptr, m_ui.theme, "UI", "Theme", InterfaceSettingsWidget::THEME_NAMES, 184 InterfaceSettingsWidget::THEME_VALUES, 185 InterfaceSettingsWidget::DEFAULT_THEME_NAME, "InterfaceSettingsWidget"); 186 connect(m_ui.theme, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &SetupWizardDialog::themeChanged); 187 188 InterfaceSettingsWidget::populateLanguageDropdown(m_ui.language); 189 SettingWidgetBinder::BindWidgetToStringSetting(nullptr, m_ui.language, "Main", "Language", 190 QtHost::GetDefaultLanguage()); 191 connect(m_ui.language, QOverload<int>::of(&QComboBox::currentIndexChanged), this, 192 &SetupWizardDialog::languageChanged); 193 194 SettingWidgetBinder::BindWidgetToBoolSetting(nullptr, m_ui.autoUpdateEnabled, "AutoUpdater", "CheckAtStartup", true); 195 } 196 197 void SetupWizardDialog::themeChanged() 198 { 199 // Main window gets recreated at the end here anyway, so it's fine to just yolo it. 200 QtHost::UpdateApplicationTheme(); 201 } 202 203 void SetupWizardDialog::languageChanged() 204 { 205 // Skip the recreation, since we don't have many dynamic UI elements. 206 QtHost::UpdateApplicationLanguage(this); 207 m_ui.retranslateUi(this); 208 setupControllerPage(false); 209 } 210 211 void SetupWizardDialog::setupBIOSPage() 212 { 213 SettingWidgetBinder::BindWidgetToFolderSetting(nullptr, m_ui.biosSearchDirectory, m_ui.browseBiosSearchDirectory, 214 tr("Select BIOS Directory"), m_ui.openBiosSearchDirectory, 215 m_ui.resetBiosSearchDirectory, "BIOS", "SearchDirectory", 216 Path::Combine(EmuFolders::DataRoot, "bios")); 217 218 refreshBiosList(); 219 220 connect(m_ui.biosSearchDirectory, &QLineEdit::textChanged, this, &SetupWizardDialog::refreshBiosList); 221 connect(m_ui.refreshBiosList, &QPushButton::clicked, this, &SetupWizardDialog::refreshBiosList); 222 } 223 224 void SetupWizardDialog::refreshBiosList() 225 { 226 auto list = BIOS::FindBIOSImagesInDirectory(m_ui.biosSearchDirectory->text().toUtf8().constData()); 227 BIOSSettingsWidget::populateDropDownForRegion(ConsoleRegion::NTSC_U, m_ui.imageNTSCU, list, false); 228 BIOSSettingsWidget::populateDropDownForRegion(ConsoleRegion::NTSC_J, m_ui.imageNTSCJ, list, false); 229 BIOSSettingsWidget::populateDropDownForRegion(ConsoleRegion::PAL, m_ui.imagePAL, list, false); 230 231 BIOSSettingsWidget::setDropDownValue(m_ui.imageNTSCU, Host::GetBaseStringSettingValue("BIOS", "PathNTSCU"), false); 232 BIOSSettingsWidget::setDropDownValue(m_ui.imageNTSCJ, Host::GetBaseStringSettingValue("BIOS", "PathNTSCJ"), false); 233 BIOSSettingsWidget::setDropDownValue(m_ui.imagePAL, Host::GetBaseStringSettingValue("BIOS", "PathPAL"), false); 234 } 235 236 void SetupWizardDialog::setupGameListPage() 237 { 238 m_ui.searchDirectoryList->setSelectionMode(QAbstractItemView::SingleSelection); 239 m_ui.searchDirectoryList->setSelectionBehavior(QAbstractItemView::SelectRows); 240 m_ui.searchDirectoryList->setAlternatingRowColors(true); 241 m_ui.searchDirectoryList->setShowGrid(false); 242 m_ui.searchDirectoryList->horizontalHeader()->setHighlightSections(false); 243 m_ui.searchDirectoryList->verticalHeader()->hide(); 244 m_ui.searchDirectoryList->setCurrentIndex({}); 245 m_ui.searchDirectoryList->setContextMenuPolicy(Qt::ContextMenuPolicy::CustomContextMenu); 246 247 connect(m_ui.searchDirectoryList, &QTableWidget::customContextMenuRequested, this, 248 &SetupWizardDialog::onDirectoryListContextMenuRequested); 249 connect(m_ui.addSearchDirectoryButton, &QPushButton::clicked, this, 250 &SetupWizardDialog::onAddSearchDirectoryButtonClicked); 251 connect(m_ui.removeSearchDirectoryButton, &QPushButton::clicked, this, 252 &SetupWizardDialog::onRemoveSearchDirectoryButtonClicked); 253 connect(m_ui.searchDirectoryList, &QTableWidget::itemSelectionChanged, this, 254 &SetupWizardDialog::onSearchDirectoryListSelectionChanged); 255 256 refreshDirectoryList(); 257 } 258 259 void SetupWizardDialog::onDirectoryListContextMenuRequested(const QPoint& point) 260 { 261 QModelIndexList selection = m_ui.searchDirectoryList->selectionModel()->selectedIndexes(); 262 if (selection.size() < 1) 263 return; 264 265 const int row = selection[0].row(); 266 267 QMenu menu; 268 menu.addAction(tr("Remove"), [this]() { onRemoveSearchDirectoryButtonClicked(); }); 269 menu.addSeparator(); 270 menu.addAction(tr("Open Directory..."), [this, row]() { 271 QtUtils::OpenURL(this, QUrl::fromLocalFile(m_ui.searchDirectoryList->item(row, 0)->text())); 272 }); 273 menu.exec(m_ui.searchDirectoryList->mapToGlobal(point)); 274 } 275 276 void SetupWizardDialog::onAddSearchDirectoryButtonClicked() 277 { 278 QString dir = QDir::toNativeSeparators(QFileDialog::getExistingDirectory(this, tr("Select Search Directory"))); 279 280 if (dir.isEmpty()) 281 return; 282 283 QMessageBox::StandardButton selection = 284 QMessageBox::question(this, tr("Scan Recursively?"), 285 tr("Would you like to scan the directory \"%1\" recursively?\n\nScanning recursively takes " 286 "more time, but will identify files in subdirectories.") 287 .arg(dir), 288 QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel); 289 if (selection == QMessageBox::Cancel) 290 return; 291 292 const bool recursive = (selection == QMessageBox::Yes); 293 const std::string spath = dir.toStdString(); 294 Host::RemoveValueFromBaseStringListSetting("GameList", recursive ? "Paths" : "RecursivePaths", spath.c_str()); 295 Host::AddValueToBaseStringListSetting("GameList", recursive ? "RecursivePaths" : "Paths", spath.c_str()); 296 Host::CommitBaseSettingChanges(); 297 refreshDirectoryList(); 298 } 299 300 void SetupWizardDialog::onRemoveSearchDirectoryButtonClicked() 301 { 302 const int row = m_ui.searchDirectoryList->currentRow(); 303 std::unique_ptr<QTableWidgetItem> item((row >= 0) ? m_ui.searchDirectoryList->takeItem(row, 0) : nullptr); 304 if (!item) 305 return; 306 307 const std::string spath = item->text().toStdString(); 308 if (!Host::RemoveValueFromBaseStringListSetting("GameList", "Paths", spath.c_str()) && 309 !Host::RemoveValueFromBaseStringListSetting("GameList", "RecursivePaths", spath.c_str())) 310 { 311 return; 312 } 313 314 Host::CommitBaseSettingChanges(); 315 refreshDirectoryList(); 316 } 317 318 void SetupWizardDialog::onSearchDirectoryListSelectionChanged() 319 { 320 m_ui.removeSearchDirectoryButton->setEnabled(!m_ui.searchDirectoryList->selectedItems().isEmpty()); 321 } 322 323 void SetupWizardDialog::addPathToTable(const std::string& path, bool recursive) 324 { 325 const int row = m_ui.searchDirectoryList->rowCount(); 326 m_ui.searchDirectoryList->insertRow(row); 327 328 QTableWidgetItem* item = new QTableWidgetItem(); 329 item->setText(QString::fromStdString(path)); 330 item->setFlags(item->flags() & ~(Qt::ItemIsEditable)); 331 m_ui.searchDirectoryList->setItem(row, 0, item); 332 333 QCheckBox* cb = new QCheckBox(m_ui.searchDirectoryList); 334 m_ui.searchDirectoryList->setCellWidget(row, 1, cb); 335 cb->setChecked(recursive); 336 337 connect(cb, &QCheckBox::checkStateChanged, [item](Qt::CheckState state) { 338 const std::string path(item->text().toStdString()); 339 if (state == Qt::Checked) 340 { 341 Host::RemoveValueFromBaseStringListSetting("GameList", "Paths", path.c_str()); 342 Host::AddValueToBaseStringListSetting("GameList", "RecursivePaths", path.c_str()); 343 } 344 else 345 { 346 Host::RemoveValueFromBaseStringListSetting("GameList", "RecursivePaths", path.c_str()); 347 Host::AddValueToBaseStringListSetting("GameList", "Paths", path.c_str()); 348 } 349 Host::CommitBaseSettingChanges(); 350 }); 351 } 352 353 void SetupWizardDialog::refreshDirectoryList() 354 { 355 QSignalBlocker sb(m_ui.searchDirectoryList); 356 while (m_ui.searchDirectoryList->rowCount() > 0) 357 m_ui.searchDirectoryList->removeRow(0); 358 359 std::vector<std::string> path_list = Host::GetBaseStringListSetting("GameList", "Paths"); 360 for (const std::string& entry : path_list) 361 addPathToTable(entry, false); 362 363 path_list = Host::GetBaseStringListSetting("GameList", "RecursivePaths"); 364 for (const std::string& entry : path_list) 365 addPathToTable(entry, true); 366 367 m_ui.searchDirectoryList->sortByColumn(0, Qt::AscendingOrder); 368 m_ui.removeSearchDirectoryButton->setEnabled(false); 369 } 370 371 void SetupWizardDialog::resizeDirectoryListColumns() 372 { 373 QtUtils::ResizeColumnsForTableView(m_ui.searchDirectoryList, {-1, 100}); 374 } 375 376 void SetupWizardDialog::setupControllerPage(bool initial) 377 { 378 static constexpr u32 NUM_PADS = 2; 379 380 struct PadWidgets 381 { 382 QComboBox* type_combo; 383 QLabel* mapping_result; 384 QToolButton* mapping_button; 385 }; 386 const PadWidgets pad_widgets[NUM_PADS] = { 387 {m_ui.controller1Type, m_ui.controller1Mapping, m_ui.controller1AutomaticMapping}, 388 {m_ui.controller2Type, m_ui.controller2Mapping, m_ui.controller2AutomaticMapping}, 389 }; 390 391 if (!initial) 392 { 393 for (const PadWidgets& w : pad_widgets) 394 { 395 w.type_combo->blockSignals(true); 396 w.type_combo->clear(); 397 } 398 } 399 400 for (u32 port = 0; port < NUM_PADS; port++) 401 { 402 const std::string section = fmt::format("Pad{}", port + 1); 403 const PadWidgets& w = pad_widgets[port]; 404 405 for (u32 i = 0; i < static_cast<u32>(ControllerType::Count); i++) 406 { 407 const ControllerType ctype = static_cast<ControllerType>(i); 408 const Controller::ControllerInfo* cinfo = Controller::GetControllerInfo(ctype); 409 if (!cinfo) 410 continue; 411 412 w.type_combo->addItem(qApp->translate("ControllerType", cinfo->display_name), QString::fromUtf8(cinfo->name)); 413 } 414 415 ControllerSettingWidgetBinder::BindWidgetToInputProfileString( 416 nullptr, w.type_combo, section, "Type", Controller::GetControllerInfo(Controller::GetDefaultPadType(port))->name); 417 418 w.mapping_result->setText((port == 0) ? tr("Default (Keyboard)") : tr("Default (None)")); 419 420 if (initial) 421 { 422 connect(w.mapping_button, &QAbstractButton::clicked, this, 423 [this, port, label = w.mapping_result]() { openAutomaticMappingMenu(port, label); }); 424 } 425 } 426 427 if (initial) 428 { 429 // Trigger enumeration to populate the device list. 430 connect(g_emu_thread, &EmuThread::onInputDevicesEnumerated, this, &SetupWizardDialog::onInputDevicesEnumerated); 431 connect(g_emu_thread, &EmuThread::onInputDeviceConnected, this, &SetupWizardDialog::onInputDeviceConnected); 432 connect(g_emu_thread, &EmuThread::onInputDeviceDisconnected, this, &SetupWizardDialog::onInputDeviceDisconnected); 433 g_emu_thread->enumerateInputDevices(); 434 } 435 436 if (!initial) 437 { 438 for (const PadWidgets& w : pad_widgets) 439 w.type_combo->blockSignals(false); 440 } 441 } 442 443 void SetupWizardDialog::updateStylesheets() 444 { 445 } 446 447 void SetupWizardDialog::openAutomaticMappingMenu(u32 port, QLabel* update_label) 448 { 449 QMenu menu(this); 450 bool added = false; 451 452 for (const auto& [identifier, device_name] : m_device_list) 453 { 454 // we set it as data, because the device list could get invalidated while the menu is up 455 const QString qidentifier = QString::fromStdString(identifier); 456 QAction* action = 457 menu.addAction(QStringLiteral("%1 (%2)").arg(qidentifier).arg(QString::fromStdString(device_name))); 458 action->setData(qidentifier); 459 connect(action, &QAction::triggered, this, [this, port, update_label, action]() { 460 doDeviceAutomaticBinding(port, update_label, action->data().toString()); 461 }); 462 added = true; 463 } 464 465 if (!added) 466 { 467 QAction* action = menu.addAction(tr("No devices available")); 468 action->setEnabled(false); 469 } 470 471 menu.exec(QCursor::pos()); 472 } 473 474 void SetupWizardDialog::doDeviceAutomaticBinding(u32 port, QLabel* update_label, const QString& device) 475 { 476 std::vector<std::pair<GenericInputBinding, std::string>> mapping = 477 InputManager::GetGenericBindingMapping(device.toStdString()); 478 if (mapping.empty()) 479 { 480 QMessageBox::critical( 481 this, tr("Automatic Binding"), 482 tr("No generic bindings were generated for device '%1'. The controller/source may not support automatic " 483 "mapping.") 484 .arg(device)); 485 return; 486 } 487 488 bool result; 489 { 490 auto lock = Host::GetSettingsLock(); 491 result = InputManager::MapController(*Host::Internal::GetBaseSettingsLayer(), port, mapping); 492 } 493 if (!result) 494 return; 495 496 Host::CommitBaseSettingChanges(); 497 498 update_label->setText(device); 499 } 500 501 void SetupWizardDialog::onInputDevicesEnumerated(const std::vector<std::pair<std::string, std::string>>& devices) 502 { 503 m_device_list = devices; 504 } 505 506 void SetupWizardDialog::onInputDeviceConnected(const std::string& identifier, const std::string& device_name) 507 { 508 m_device_list.emplace_back(identifier, device_name); 509 } 510 511 void SetupWizardDialog::onInputDeviceDisconnected(const std::string& identifier) 512 { 513 for (auto iter = m_device_list.begin(); iter != m_device_list.end(); ++iter) 514 { 515 if (iter->first == identifier) 516 { 517 m_device_list.erase(iter); 518 break; 519 } 520 } 521 }