duckstation

duckstation, but archived from the revision just before upstream changed it to a proprietary software project, this version is the libre one
git clone https://git.neptards.moe/u3shit/duckstation.git
Log | Files | Refs | README | LICENSE

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 }