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