Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
stenzek
GitHub Repository: stenzek/duckstation
Path: blob/master/src/duckstation-qt/controllersettingswindow.cpp
4246 views
1
// SPDX-FileCopyrightText: 2019-2025 Connor McLaughlin <[email protected]>
2
// SPDX-License-Identifier: CC-BY-NC-ND-4.0
3
4
#include "controllersettingswindow.h"
5
#include "controllerbindingwidgets.h"
6
#include "controllerglobalsettingswidget.h"
7
#include "hotkeysettingswidget.h"
8
#include "mainwindow.h"
9
#include "qthost.h"
10
11
#include "core/controller.h"
12
#include "core/host.h"
13
14
#include "util/ini_settings_interface.h"
15
#include "util/input_manager.h"
16
17
#include "common/assert.h"
18
#include "common/file_system.h"
19
20
#include <QtWidgets/QInputDialog>
21
#include <QtWidgets/QMessageBox>
22
#include <QtWidgets/QTextEdit>
23
#include <array>
24
25
#include "moc_controllersettingswindow.cpp"
26
27
ControllerSettingsWindow::ControllerSettingsWindow(INISettingsInterface* game_sif /* = nullptr */,
28
bool edit_profiles /* = false */, QWidget* parent /* = nullptr */)
29
: QWidget(parent), m_editing_settings_interface(game_sif), m_editing_input_profiles(edit_profiles)
30
{
31
m_ui.setupUi(this);
32
m_ui.buttonBox->button(QDialogButtonBox::Close)->setDefault(true);
33
34
setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint);
35
36
connect(m_ui.settingsCategory, &QListWidget::currentRowChanged, this,
37
&ControllerSettingsWindow::onCategoryCurrentRowChanged);
38
connect(m_ui.buttonBox, &QDialogButtonBox::rejected, this, &ControllerSettingsWindow::close);
39
40
if (!game_sif && !edit_profiles)
41
{
42
// editing global settings
43
m_ui.editProfileLayout->removeWidget(m_ui.editProfileLabel);
44
delete m_ui.editProfileLabel;
45
m_ui.editProfileLabel = nullptr;
46
m_ui.editProfileLayout->removeWidget(m_ui.currentProfile);
47
delete m_ui.currentProfile;
48
m_ui.currentProfile = nullptr;
49
m_ui.editProfileLayout->removeWidget(m_ui.newProfile);
50
delete m_ui.newProfile;
51
m_ui.newProfile = nullptr;
52
m_ui.editProfileLayout->removeWidget(m_ui.applyProfile);
53
delete m_ui.applyProfile;
54
m_ui.applyProfile = nullptr;
55
m_ui.editProfileLayout->removeWidget(m_ui.deleteProfile);
56
delete m_ui.deleteProfile;
57
m_ui.deleteProfile = nullptr;
58
m_ui.editProfileLayout->removeWidget(m_ui.copyGlobalSettings);
59
delete m_ui.copyGlobalSettings;
60
m_ui.copyGlobalSettings = nullptr;
61
62
if (QPushButton* button = m_ui.buttonBox->button(QDialogButtonBox::RestoreDefaults))
63
connect(button, &QPushButton::clicked, this, &ControllerSettingsWindow::onRestoreDefaultsClicked);
64
}
65
else
66
{
67
if (QPushButton* button = m_ui.buttonBox->button(QDialogButtonBox::RestoreDefaults))
68
m_ui.buttonBox->removeButton(button);
69
70
connect(m_ui.copyGlobalSettings, &QPushButton::clicked, this,
71
&ControllerSettingsWindow::onCopyGlobalSettingsClicked);
72
73
if (edit_profiles)
74
{
75
setWindowTitle(tr("DuckStation Controller Presets"));
76
refreshProfileList();
77
78
connect(m_ui.currentProfile, &QComboBox::currentIndexChanged, this,
79
&ControllerSettingsWindow::onCurrentProfileChanged);
80
connect(m_ui.newProfile, &QPushButton::clicked, this, &ControllerSettingsWindow::onNewProfileClicked);
81
connect(m_ui.applyProfile, &QPushButton::clicked, this, &ControllerSettingsWindow::onApplyProfileClicked);
82
connect(m_ui.deleteProfile, &QPushButton::clicked, this, &ControllerSettingsWindow::onDeleteProfileClicked);
83
}
84
else
85
{
86
// editing game settings
87
m_ui.editProfileLayout->removeWidget(m_ui.editProfileLabel);
88
delete m_ui.editProfileLabel;
89
m_ui.editProfileLabel = nullptr;
90
m_ui.editProfileLayout->removeWidget(m_ui.currentProfile);
91
delete m_ui.currentProfile;
92
m_ui.currentProfile = nullptr;
93
m_ui.editProfileLayout->removeWidget(m_ui.newProfile);
94
delete m_ui.newProfile;
95
m_ui.newProfile = nullptr;
96
m_ui.editProfileLayout->removeWidget(m_ui.applyProfile);
97
delete m_ui.applyProfile;
98
m_ui.applyProfile = nullptr;
99
m_ui.editProfileLayout->removeWidget(m_ui.deleteProfile);
100
delete m_ui.deleteProfile;
101
m_ui.deleteProfile = nullptr;
102
}
103
}
104
105
createWidgets();
106
}
107
108
ControllerSettingsWindow::~ControllerSettingsWindow() = default;
109
110
void ControllerSettingsWindow::editControllerSettingsForGame(QWidget* parent, INISettingsInterface* sif)
111
{
112
ControllerSettingsWindow* dlg = new ControllerSettingsWindow(sif, false, parent);
113
dlg->setWindowFlag(Qt::Window);
114
dlg->setAttribute(Qt::WA_DeleteOnClose);
115
dlg->setWindowModality(Qt::WindowModality::WindowModal);
116
dlg->setWindowTitle(parent->windowTitle());
117
dlg->setWindowIcon(parent->windowIcon());
118
dlg->show();
119
}
120
121
int ControllerSettingsWindow::getHotkeyCategoryIndex() const
122
{
123
const std::array<bool, 2> mtap_enabled = getEnabledMultitaps();
124
return 1 + (mtap_enabled[0] ? 4 : 1) + (mtap_enabled[1] ? 4 : 1);
125
}
126
127
ControllerSettingsWindow::Category ControllerSettingsWindow::getCurrentCategory() const
128
{
129
const int index = m_ui.settingsCategory->currentRow();
130
if (index == 0)
131
return Category::GlobalSettings;
132
else if (index >= getHotkeyCategoryIndex())
133
return Category::HotkeySettings;
134
else
135
return Category::FirstControllerSettings;
136
}
137
138
void ControllerSettingsWindow::setCategory(Category category)
139
{
140
switch (category)
141
{
142
case Category::GlobalSettings:
143
m_ui.settingsCategory->setCurrentRow(0);
144
break;
145
146
case Category::FirstControllerSettings:
147
m_ui.settingsCategory->setCurrentRow(1);
148
break;
149
150
case Category::HotkeySettings:
151
m_ui.settingsCategory->setCurrentRow(getHotkeyCategoryIndex());
152
break;
153
154
default:
155
break;
156
}
157
}
158
159
void ControllerSettingsWindow::onCategoryCurrentRowChanged(int row)
160
{
161
m_ui.settingsContainer->setCurrentIndex(row);
162
}
163
164
void ControllerSettingsWindow::onCurrentProfileChanged(int index)
165
{
166
switchProfile(m_ui.currentProfile->itemText(index).toStdString());
167
}
168
169
void ControllerSettingsWindow::onNewProfileClicked()
170
{
171
const std::string profile_name =
172
QInputDialog::getText(this, tr("Create Controller Preset"), tr("Enter the name for the new controller preset:"))
173
.toStdString();
174
if (profile_name.empty())
175
return;
176
177
std::string profile_path = System::GetInputProfilePath(profile_name);
178
if (FileSystem::FileExists(profile_path.c_str()))
179
{
180
QMessageBox::critical(this, tr("Error"),
181
tr("A preset with the name '%1' already exists.").arg(QString::fromStdString(profile_name)));
182
return;
183
}
184
185
const int res = QMessageBox::question(this, tr("Create Controller Preset"),
186
tr("Do you want to copy all bindings from the currently-selected preset to "
187
"the new preset? Selecting No will create a completely empty preset."),
188
QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel);
189
if (res == QMessageBox::Cancel)
190
return;
191
192
INISettingsInterface temp_si(std::move(profile_path));
193
if (res == QMessageBox::Yes)
194
{
195
// copy from global or the current profile
196
if (!m_editing_settings_interface)
197
{
198
const int hkres = QMessageBox::question(
199
this, tr("Create Controller Preset"),
200
tr("Do you want to copy the current hotkey bindings from global settings to the new controller preset?"),
201
QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel);
202
if (hkres == QMessageBox::Cancel)
203
return;
204
205
const bool copy_hotkey_bindings = (hkres == QMessageBox::Yes);
206
if (copy_hotkey_bindings)
207
temp_si.SetBoolValue("ControllerPorts", "UseProfileHotkeyBindings", true);
208
209
// from global
210
auto lock = Host::GetSettingsLock();
211
InputManager::CopyConfiguration(&temp_si, *Host::Internal::GetBaseSettingsLayer(), true, true, true,
212
copy_hotkey_bindings);
213
}
214
else
215
{
216
// from profile
217
const bool copy_hotkey_bindings =
218
m_editing_settings_interface->GetBoolValue("ControllerPorts", "UseProfileHotkeyBindings", false);
219
temp_si.SetBoolValue("ControllerPorts", "UseProfileHotkeyBindings", copy_hotkey_bindings);
220
InputManager::CopyConfiguration(&temp_si, *m_editing_settings_interface, true, true, true, copy_hotkey_bindings);
221
}
222
}
223
else
224
{
225
// still need to copy the source config
226
if (!m_editing_settings_interface)
227
InputManager::CopyConfiguration(&temp_si, *Host::Internal::GetBaseSettingsLayer(), false, true, false, false);
228
else
229
InputManager::CopyConfiguration(&temp_si, *m_editing_settings_interface, false, true, false, false);
230
}
231
232
if (!temp_si.Save())
233
{
234
QMessageBox::critical(this, tr("Error"),
235
tr("Failed to save the new preset to '%1'.").arg(QString::fromStdString(temp_si.GetPath())));
236
return;
237
}
238
239
refreshProfileList();
240
switchProfile(profile_name);
241
}
242
243
void ControllerSettingsWindow::onApplyProfileClicked()
244
{
245
if (QMessageBox::question(this, tr("Load Controller Preset"),
246
tr("Are you sure you want to apply the controller preset named '%1'?\n\n"
247
"All current global bindings will be removed, and the preset bindings loaded.\n\n"
248
"You cannot undo this action.")
249
.arg(m_profile_name)) != QMessageBox::Yes)
250
{
251
return;
252
}
253
254
{
255
const bool copy_hotkey_bindings =
256
m_editing_settings_interface->GetBoolValue("ControllerPorts", "UseProfileHotkeyBindings", false);
257
auto lock = Host::GetSettingsLock();
258
InputManager::CopyConfiguration(Host::Internal::GetBaseSettingsLayer(), *m_editing_settings_interface, true, true,
259
true, copy_hotkey_bindings);
260
QtHost::QueueSettingsSave();
261
}
262
g_emu_thread->applySettings();
263
264
// Recreate global widget on profile apply
265
g_main_window->getControllerSettingsWindow()->createWidgets();
266
}
267
268
void ControllerSettingsWindow::onDeleteProfileClicked()
269
{
270
if (QMessageBox::question(this, tr("Delete Controller Preset"),
271
tr("Are you sure you want to delete the controller preset named '%1'?\n\n"
272
"You cannot undo this action.")
273
.arg(m_profile_name)) != QMessageBox::Yes)
274
{
275
return;
276
}
277
278
std::string profile_path(System::GetInputProfilePath(m_profile_name.toStdString()));
279
if (!FileSystem::DeleteFile(profile_path.c_str()))
280
{
281
QMessageBox::critical(this, tr("Error"), tr("Failed to delete '%1'.").arg(QString::fromStdString(profile_path)));
282
return;
283
}
284
285
// switch back to global
286
refreshProfileList();
287
switchProfile({});
288
}
289
290
void ControllerSettingsWindow::onRestoreDefaultsClicked()
291
{
292
if (QMessageBox::question(this, tr("Restore Defaults"),
293
tr("Are you sure you want to restore the default controller configuration?\n\n"
294
"All bindings and configuration will be lost. You cannot undo this action.")) !=
295
QMessageBox::Yes)
296
{
297
return;
298
}
299
300
// actually restore it
301
g_emu_thread->setDefaultSettings(false, true);
302
303
// reload all settings
304
createWidgets();
305
}
306
307
void ControllerSettingsWindow::onCopyGlobalSettingsClicked()
308
{
309
DebugAssert(!isEditingGlobalSettings());
310
311
{
312
const auto lock = Host::GetSettingsLock();
313
InputManager::CopyConfiguration(m_editing_settings_interface, *Host::Internal::GetBaseSettingsLayer(), true, true,
314
true, false);
315
}
316
317
m_editing_settings_interface->Save();
318
g_emu_thread->reloadGameSettings();
319
createWidgets();
320
321
QMessageBox::information(QtUtils::GetRootWidget(this), tr("DuckStation Controller Settings"),
322
isEditingGameSettings() ? tr("Per-game controller configuration reset to global settings.") :
323
tr("Controller preset reset to global settings."));
324
}
325
326
bool ControllerSettingsWindow::getBoolValue(const char* section, const char* key, bool default_value) const
327
{
328
if (m_editing_settings_interface)
329
return m_editing_settings_interface->GetBoolValue(section, key, default_value);
330
else
331
return Host::GetBaseBoolSettingValue(section, key, default_value);
332
}
333
334
s32 ControllerSettingsWindow::getIntValue(const char* section, const char* key, s32 default_value) const
335
{
336
if (m_editing_settings_interface)
337
return m_editing_settings_interface->GetIntValue(section, key, default_value);
338
else
339
return Host::GetBaseIntSettingValue(section, key, default_value);
340
}
341
342
std::string ControllerSettingsWindow::getStringValue(const char* section, const char* key,
343
const char* default_value) const
344
{
345
std::string value;
346
if (m_editing_settings_interface)
347
value = m_editing_settings_interface->GetStringValue(section, key, default_value);
348
else
349
value = Host::GetBaseStringSettingValue(section, key, default_value);
350
return value;
351
}
352
353
void ControllerSettingsWindow::setBoolValue(const char* section, const char* key, bool value)
354
{
355
if (m_editing_settings_interface)
356
{
357
m_editing_settings_interface->SetBoolValue(section, key, value);
358
saveAndReloadGameSettings();
359
}
360
else
361
{
362
Host::SetBaseBoolSettingValue(section, key, value);
363
Host::CommitBaseSettingChanges();
364
g_emu_thread->applySettings();
365
}
366
}
367
368
void ControllerSettingsWindow::setIntValue(const char* section, const char* key, s32 value)
369
{
370
if (m_editing_settings_interface)
371
{
372
m_editing_settings_interface->SetIntValue(section, key, value);
373
saveAndReloadGameSettings();
374
}
375
else
376
{
377
Host::SetBaseIntSettingValue(section, key, value);
378
Host::CommitBaseSettingChanges();
379
g_emu_thread->applySettings();
380
}
381
}
382
383
void ControllerSettingsWindow::setStringValue(const char* section, const char* key, const char* value)
384
{
385
if (m_editing_settings_interface)
386
{
387
m_editing_settings_interface->SetStringValue(section, key, value);
388
saveAndReloadGameSettings();
389
}
390
else
391
{
392
Host::SetBaseStringSettingValue(section, key, value);
393
Host::CommitBaseSettingChanges();
394
g_emu_thread->applySettings();
395
}
396
}
397
398
void ControllerSettingsWindow::saveAndReloadGameSettings()
399
{
400
DebugAssert(m_editing_settings_interface);
401
QtHost::SaveGameSettings(m_editing_settings_interface, false);
402
g_emu_thread->reloadGameSettings(false);
403
}
404
405
void ControllerSettingsWindow::clearSettingValue(const char* section, const char* key)
406
{
407
if (m_editing_settings_interface)
408
{
409
m_editing_settings_interface->DeleteValue(section, key);
410
m_editing_settings_interface->Save();
411
g_emu_thread->reloadGameSettings();
412
}
413
else
414
{
415
Host::DeleteBaseSettingValue(section, key);
416
Host::CommitBaseSettingChanges();
417
g_emu_thread->applySettings();
418
}
419
}
420
421
void ControllerSettingsWindow::createWidgets()
422
{
423
QSignalBlocker sb(m_ui.settingsContainer);
424
QSignalBlocker sb2(m_ui.settingsCategory);
425
426
while (m_ui.settingsContainer->count() > 0)
427
{
428
QWidget* widget = m_ui.settingsContainer->widget(m_ui.settingsContainer->count() - 1);
429
m_ui.settingsContainer->removeWidget(widget);
430
widget->deleteLater();
431
}
432
433
m_ui.settingsCategory->clear();
434
435
m_global_settings = nullptr;
436
m_hotkey_settings = nullptr;
437
438
{
439
// global settings
440
QListWidgetItem* item = new QListWidgetItem();
441
item->setText(tr("Global Settings"));
442
item->setIcon(QIcon::fromTheme(QStringLiteral("settings-3-line")));
443
m_ui.settingsCategory->addItem(item);
444
m_ui.settingsCategory->setCurrentRow(0);
445
m_global_settings = new ControllerGlobalSettingsWidget(m_ui.settingsContainer, this);
446
m_ui.settingsContainer->addWidget(m_global_settings);
447
connect(m_global_settings, &ControllerGlobalSettingsWidget::bindingSetupChanged, this,
448
&ControllerSettingsWindow::createWidgets);
449
}
450
451
// load mtap settings
452
const std::array<bool, 2> mtap_enabled = getEnabledMultitaps();
453
for (u32 global_slot : Controller::PortDisplayOrder)
454
{
455
const bool is_mtap_port = Controller::PadIsMultitapSlot(global_slot);
456
const auto [port, slot] = Controller::ConvertPadToPortAndSlot(global_slot);
457
if (is_mtap_port && !mtap_enabled[port])
458
continue;
459
460
m_port_bindings[global_slot] = new ControllerBindingWidget(m_ui.settingsContainer, this, global_slot);
461
m_ui.settingsContainer->addWidget(m_port_bindings[global_slot]);
462
463
const QString display_name(
464
QtUtils::StringViewToQString(m_port_bindings[global_slot]->getControllerInfo()->GetDisplayName()));
465
466
QListWidgetItem* item = new QListWidgetItem();
467
item->setText(tr("Controller Port %1\n%2")
468
.arg(Controller::GetPortDisplayName(port, slot, mtap_enabled[port]))
469
.arg(display_name));
470
item->setIcon(m_port_bindings[global_slot]->getIcon());
471
item->setData(Qt::UserRole, QVariant(global_slot));
472
m_ui.settingsCategory->addItem(item);
473
}
474
475
// only add hotkeys if we're editing global settings
476
if (!m_editing_settings_interface ||
477
m_editing_settings_interface->GetBoolValue("ControllerPorts", "UseProfileHotkeyBindings", false))
478
{
479
QListWidgetItem* item = new QListWidgetItem();
480
item->setText(tr("Hotkeys"));
481
item->setIcon(QIcon::fromTheme(QStringLiteral("keyboard-line")));
482
m_ui.settingsCategory->addItem(item);
483
m_hotkey_settings = new HotkeySettingsWidget(m_ui.settingsContainer, this);
484
m_ui.settingsContainer->addWidget(m_hotkey_settings);
485
}
486
487
if (isEditingProfile())
488
{
489
const bool enable_buttons = static_cast<bool>(m_profile_settings_interface);
490
m_ui.applyProfile->setEnabled(enable_buttons);
491
m_ui.deleteProfile->setEnabled(enable_buttons);
492
m_ui.copyGlobalSettings->setEnabled(enable_buttons);
493
}
494
}
495
496
void ControllerSettingsWindow::closeEvent(QCloseEvent* event)
497
{
498
QWidget::closeEvent(event);
499
emit windowClosed();
500
}
501
502
void ControllerSettingsWindow::updateListDescription(u32 global_slot, ControllerBindingWidget* widget)
503
{
504
for (int i = 0; i < m_ui.settingsCategory->count(); i++)
505
{
506
QListWidgetItem* item = m_ui.settingsCategory->item(i);
507
const QVariant item_data(item->data(Qt::UserRole));
508
bool is_ok;
509
if (item_data.toUInt(&is_ok) == global_slot && is_ok)
510
{
511
const std::array<bool, 2> mtap_enabled = getEnabledMultitaps();
512
const auto [port, slot] = Controller::ConvertPadToPortAndSlot(global_slot);
513
514
const QString display_name = QtUtils::StringViewToQString(widget->getControllerInfo()->GetDisplayName());
515
516
item->setText(tr("Controller Port %1\n%2")
517
.arg(Controller::GetPortDisplayName(port, slot, mtap_enabled[port]))
518
.arg(display_name));
519
item->setIcon(widget->getIcon());
520
break;
521
}
522
}
523
}
524
525
std::array<bool, 2> ControllerSettingsWindow::getEnabledMultitaps() const
526
{
527
const MultitapMode mtap_mode =
528
Settings::ParseMultitapModeName(
529
getStringValue("ControllerPorts", "MultitapMode", Settings::GetMultitapModeName(Settings::DEFAULT_MULTITAP_MODE))
530
.c_str())
531
.value_or(Settings::DEFAULT_MULTITAP_MODE);
532
return {{(mtap_mode == MultitapMode::Port1Only || mtap_mode == MultitapMode::BothPorts),
533
(mtap_mode == MultitapMode::Port2Only || mtap_mode == MultitapMode::BothPorts)}};
534
}
535
536
void ControllerSettingsWindow::refreshProfileList()
537
{
538
const std::vector<std::string> names(InputManager::GetInputProfileNames());
539
540
QSignalBlocker sb(m_ui.currentProfile);
541
m_ui.currentProfile->clear();
542
543
bool current_profile_found = false;
544
for (const std::string& name : names)
545
{
546
const QString qname(QString::fromStdString(name));
547
m_ui.currentProfile->addItem(qname);
548
if (qname == m_profile_name)
549
{
550
m_ui.currentProfile->setCurrentIndex(m_ui.currentProfile->count() - 1);
551
current_profile_found = true;
552
}
553
}
554
555
if (!current_profile_found)
556
switchProfile(names.empty() ? std::string_view() : std::string_view(names.front()));
557
}
558
559
void ControllerSettingsWindow::switchProfile(const std::string_view name)
560
{
561
const QString name_qstr = QtUtils::StringViewToQString(name);
562
{
563
QSignalBlocker sb(m_ui.currentProfile);
564
m_ui.currentProfile->setCurrentIndex(m_ui.currentProfile->findText(name_qstr));
565
}
566
m_profile_name = name_qstr;
567
m_profile_settings_interface.reset();
568
m_editing_settings_interface = nullptr;
569
570
// disable UI if there is no selection
571
const bool disable_ui = name.empty();
572
m_ui.settingsCategory->setDisabled(disable_ui);
573
m_ui.settingsContainer->setDisabled(disable_ui);
574
575
if (name_qstr.isEmpty())
576
{
577
createWidgets();
578
return;
579
}
580
581
std::string path = System::GetInputProfilePath(name);
582
if (!FileSystem::FileExists(path.c_str()))
583
{
584
QMessageBox::critical(this, tr("Error"), tr("The controller preset named '%1' cannot be found.").arg(name_qstr));
585
return;
586
}
587
588
std::unique_ptr<INISettingsInterface> sif = std::make_unique<INISettingsInterface>(std::move(path));
589
sif->Load();
590
591
m_profile_settings_interface = std::move(sif);
592
m_editing_settings_interface = m_profile_settings_interface.get();
593
594
createWidgets();
595
}
596
597