Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
stenzek
GitHub Repository: stenzek/duckstation
Path: blob/master/src/duckstation-qt/controllerbindingwidgets.cpp
7365 views
1
// SPDX-FileCopyrightText: 2019-2026 Connor McLaughlin <[email protected]>
2
// SPDX-License-Identifier: CC-BY-NC-ND-4.0
3
4
#include "controllerbindingwidgets.h"
5
#include "controllersettingswindow.h"
6
#include "controllersettingwidgetbinder.h"
7
#include "mainwindow.h"
8
#include "qthost.h"
9
#include "qtutils.h"
10
#include "settingswindow.h"
11
#include "settingwidgetbinder.h"
12
13
#include "ui_controllerbindingwidget_analog_controller.h"
14
#include "ui_controllerbindingwidget_analog_joystick.h"
15
#include "ui_controllerbindingwidget_digital_controller.h"
16
#include "ui_controllerbindingwidget_guncon.h"
17
#include "ui_controllerbindingwidget_justifier.h"
18
#include "ui_controllerbindingwidget_mouse.h"
19
#include "ui_controllerbindingwidget_negcon.h"
20
#include "ui_controllerbindingwidget_negconrumble.h"
21
22
#include "core/controller.h"
23
#include "core/host.h"
24
25
#include "util/input_manager.h"
26
27
#include "common/log.h"
28
#include "common/string_util.h"
29
30
#include "fmt/format.h"
31
32
#include <QtWidgets/QCheckBox>
33
#include <QtWidgets/QDialogButtonBox>
34
#include <QtWidgets/QDoubleSpinBox>
35
#include <QtWidgets/QInputDialog>
36
#include <QtWidgets/QLineEdit>
37
#include <QtWidgets/QMenu>
38
#include <QtWidgets/QScrollArea>
39
#include <QtWidgets/QSpinBox>
40
#include <algorithm>
41
42
#include "moc_controllerbindingwidgets.cpp"
43
44
using namespace Qt::StringLiterals;
45
46
LOG_CHANNEL(Host);
47
48
ControllerBindingWidget::ControllerBindingWidget(QWidget* parent, ControllerSettingsWindow* dialog, u32 port)
49
: QWidget(parent), m_dialog(dialog), m_config_section(Controller::GetSettingsSection(port)), m_port_number(port)
50
{
51
m_ui.setupUi(this);
52
populateControllerTypes();
53
populateWidgets();
54
55
connect(m_ui.controllerType, QOverload<int>::of(&QComboBox::currentIndexChanged), this,
56
&ControllerBindingWidget::onTypeChanged);
57
connect(m_ui.bindings, &QPushButton::clicked, this, &ControllerBindingWidget::onBindingsClicked);
58
connect(m_ui.settings, &QPushButton::clicked, this, &ControllerBindingWidget::onSettingsClicked);
59
connect(m_ui.macros, &QPushButton::clicked, this, &ControllerBindingWidget::onMacrosClicked);
60
connect(m_ui.automaticBinding, &QPushButton::clicked, this, &ControllerBindingWidget::onAutomaticBindingClicked);
61
connect(m_ui.clearBindings, &QPushButton::clicked, this, &ControllerBindingWidget::onClearBindingsClicked);
62
}
63
64
ControllerBindingWidget::~ControllerBindingWidget() = default;
65
66
void ControllerBindingWidget::populateControllerTypes()
67
{
68
for (const Controller::ControllerInfo* cinfo : Controller::GetControllerInfoList())
69
m_ui.controllerType->addItem(QtUtils::StringViewToQString(cinfo->GetDisplayName()),
70
QVariant(static_cast<int>(cinfo->type)));
71
72
m_controller_info = Controller::GetControllerInfo(
73
m_dialog->getStringValue(m_config_section.c_str(), "Type",
74
Controller::GetControllerInfo(Settings::GetDefaultControllerType(m_port_number)).name));
75
if (!m_controller_info)
76
m_controller_info = &Controller::GetControllerInfo(Settings::GetDefaultControllerType(m_port_number));
77
78
const int index = m_ui.controllerType->findData(QVariant(static_cast<int>(m_controller_info->type)));
79
if (index >= 0 && index != m_ui.controllerType->currentIndex())
80
{
81
QSignalBlocker sb(m_ui.controllerType);
82
m_ui.controllerType->setCurrentIndex(index);
83
}
84
}
85
86
void ControllerBindingWidget::populateWidgets()
87
{
88
const bool is_initializing = (m_ui.stackedWidget->count() == 0);
89
if (m_bindings_widget)
90
{
91
m_ui.stackedWidget->removeWidget(m_bindings_widget);
92
delete m_bindings_widget;
93
m_bindings_widget = nullptr;
94
}
95
if (m_settings_widget)
96
{
97
m_ui.stackedWidget->removeWidget(m_settings_widget);
98
delete m_settings_widget;
99
m_settings_widget = nullptr;
100
}
101
if (m_macros_widget)
102
{
103
m_ui.stackedWidget->removeWidget(m_macros_widget);
104
delete m_macros_widget;
105
m_macros_widget = nullptr;
106
}
107
108
const bool has_settings = !m_controller_info->settings.empty();
109
const bool has_macros = !m_controller_info->bindings.empty();
110
m_ui.settings->setEnabled(has_settings);
111
m_ui.macros->setEnabled(has_macros);
112
113
m_bindings_widget = new QWidget(this);
114
switch (m_controller_info->type)
115
{
116
case ControllerType::AnalogController:
117
{
118
Ui::ControllerBindingWidget_AnalogController ui;
119
ui.setupUi(m_bindings_widget);
120
bindBindingWidgets(m_bindings_widget);
121
m_icon = QIcon::fromTheme("controller-line"_L1);
122
}
123
break;
124
125
case ControllerType::AnalogJoystick:
126
{
127
Ui::ControllerBindingWidget_AnalogJoystick ui;
128
ui.setupUi(m_bindings_widget);
129
bindBindingWidgets(m_bindings_widget);
130
m_icon = QIcon::fromTheme("joystick-line"_L1);
131
}
132
break;
133
134
case ControllerType::DigitalController:
135
{
136
Ui::ControllerBindingWidget_DigitalController ui;
137
ui.setupUi(m_bindings_widget);
138
bindBindingWidgets(m_bindings_widget);
139
m_icon = QIcon::fromTheme("controller-digital-line"_L1);
140
}
141
break;
142
143
case ControllerType::GunCon:
144
{
145
Ui::ControllerBindingWidget_GunCon ui;
146
ui.setupUi(m_bindings_widget);
147
bindBindingWidgets(m_bindings_widget);
148
m_icon = QIcon::fromTheme("guncon-line"_L1);
149
}
150
break;
151
152
case ControllerType::NeGcon:
153
{
154
Ui::ControllerBindingWidget_NeGcon ui;
155
ui.setupUi(m_bindings_widget);
156
bindBindingWidgets(m_bindings_widget);
157
m_icon = QIcon::fromTheme("negcon-line"_L1);
158
}
159
break;
160
161
case ControllerType::NeGconRumble:
162
{
163
Ui::ControllerBindingWidget_NeGconRumble ui;
164
ui.setupUi(m_bindings_widget);
165
bindBindingWidgets(m_bindings_widget);
166
m_icon = QIcon::fromTheme("negcon-line"_L1);
167
}
168
break;
169
170
case ControllerType::PlayStationMouse:
171
{
172
Ui::ControllerBindingWidget_Mouse ui;
173
ui.setupUi(m_bindings_widget);
174
bindBindingWidgets(m_bindings_widget);
175
m_icon = QIcon::fromTheme("mouse-line"_L1);
176
}
177
break;
178
179
case ControllerType::Justifier:
180
{
181
Ui::ControllerBindingWidget_Justifier ui;
182
ui.setupUi(m_bindings_widget);
183
bindBindingWidgets(m_bindings_widget);
184
m_icon = QIcon::fromTheme("guncon-line"_L1);
185
}
186
break;
187
188
case ControllerType::None:
189
{
190
m_icon = QIcon::fromTheme("controller-strike-line"_L1);
191
}
192
break;
193
194
default:
195
{
196
createBindingWidgets(m_bindings_widget);
197
m_icon = QIcon::fromTheme("controller-line"_L1);
198
}
199
break;
200
}
201
202
m_ui.stackedWidget->addWidget(m_bindings_widget);
203
m_ui.stackedWidget->setCurrentWidget(m_bindings_widget);
204
205
if (has_settings)
206
{
207
m_settings_widget = new ControllerCustomSettingsWidget(this);
208
m_ui.stackedWidget->addWidget(m_settings_widget);
209
}
210
211
if (has_macros)
212
{
213
m_macros_widget = new ControllerMacroWidget(this);
214
m_ui.stackedWidget->addWidget(m_macros_widget);
215
}
216
217
updateHeaderToolButtons();
218
219
// no need to do this on first init, only changes
220
if (!is_initializing)
221
m_dialog->updateListDescription(m_port_number, this);
222
}
223
224
void ControllerBindingWidget::updateHeaderToolButtons()
225
{
226
const QWidget* current_widget = m_ui.stackedWidget->currentWidget();
227
const QSignalBlocker bindings_sb(m_ui.bindings);
228
const QSignalBlocker settings_sb(m_ui.settings);
229
const QSignalBlocker macros_sb(m_ui.macros);
230
231
const bool is_bindings = (current_widget == m_bindings_widget);
232
m_ui.bindings->setChecked(is_bindings);
233
m_ui.automaticBinding->setEnabled(is_bindings);
234
m_ui.clearBindings->setEnabled(is_bindings);
235
m_ui.macros->setChecked(current_widget == m_macros_widget);
236
m_ui.settings->setChecked((current_widget == m_settings_widget));
237
}
238
239
void ControllerBindingWidget::onTypeChanged()
240
{
241
bool ok;
242
const int index = m_ui.controllerType->currentData().toInt(&ok);
243
if (!ok || index < 0 || index >= static_cast<int>(ControllerType::Count))
244
return;
245
246
m_controller_info = &Controller::GetControllerInfo(static_cast<ControllerType>(index));
247
248
SettingsInterface* sif = m_dialog->getEditingSettingsInterface();
249
if (sif)
250
{
251
sif->SetStringValue(m_config_section.c_str(), "Type", m_controller_info->name);
252
QtHost::SaveGameSettings(sif, false);
253
g_core_thread->reloadGameSettings();
254
}
255
else
256
{
257
Core::SetBaseStringSettingValue(m_config_section.c_str(), "Type", m_controller_info->name);
258
Host::CommitBaseSettingChanges();
259
g_core_thread->applySettings();
260
}
261
262
populateWidgets();
263
}
264
265
void ControllerBindingWidget::onAutomaticBindingClicked()
266
{
267
QMenu* const menu = QtUtils::NewPopupMenu(this);
268
bool added = false;
269
270
for (const InputDeviceListModel::Device& dev : g_core_thread->getInputDeviceListModel()->getDeviceList())
271
{
272
// we set it as data, because the device list could get invalidated while the menu is up
273
menu->addAction(InputDeviceListModel::getIconForKey(dev.key),
274
QStringLiteral("%1 (%2)").arg(dev.identifier).arg(dev.display_name),
275
[this, device = dev.identifier]() { doDeviceAutomaticBinding(device); });
276
added = true;
277
}
278
279
if (added)
280
{
281
menu->addAction(tr("Multiple devices..."), this,
282
&ControllerBindingWidget::onMultipleDeviceAutomaticBindingTriggered);
283
}
284
else
285
{
286
QAction* const action = menu->addAction(tr("No devices available"));
287
action->setEnabled(false);
288
}
289
290
menu->popup(QCursor::pos());
291
}
292
293
void ControllerBindingWidget::onClearBindingsClicked()
294
{
295
if (QtUtils::MessageBoxQuestion(
296
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
const auto lock = Core::GetSettingsLock();
306
InputManager::ClearPortBindings(*Core::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
QtUtils::AsyncMessageBox(
347
this, QMessageBox::Critical, tr("Automatic Mapping Failed"),
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
const auto lock = Core::GetSettingsLock();
357
result = InputManager::MapController(*Core::GetBaseSettingsLayer(), m_port_number, mapping, true);
358
}
359
else
360
{
361
result = InputManager::MapController(*m_dialog->getEditingSettingsInterface(), m_port_number, mapping, true);
362
QtHost::SaveGameSettings(m_dialog->getEditingSettingsInterface(), false);
363
g_core_thread->reloadInputBindings();
364
}
365
366
// force a refresh after mapping
367
if (result)
368
saveAndRefresh();
369
}
370
371
void ControllerBindingWidget::onMultipleDeviceAutomaticBindingTriggered()
372
{
373
QDialog* const dialog = new MultipleDeviceAutobindDialog(this, m_dialog, m_port_number);
374
dialog->setAttribute(Qt::WA_DeleteOnClose);
375
376
// force a refresh after mapping
377
connect(dialog, &QDialog::accepted, this, [this] { onTypeChanged(); });
378
379
dialog->open();
380
}
381
382
void ControllerBindingWidget::saveAndRefresh()
383
{
384
onTypeChanged();
385
QtHost::QueueSettingsSave();
386
g_core_thread->applySettings();
387
}
388
389
void ControllerBindingWidget::createBindingWidgets(QWidget* parent)
390
{
391
SettingsInterface* sif = getDialog()->getEditingSettingsInterface();
392
DebugAssert(m_controller_info);
393
394
QGroupBox* axis_gbox = nullptr;
395
QGridLayout* axis_layout = nullptr;
396
QGroupBox* button_gbox = nullptr;
397
QGridLayout* button_layout = nullptr;
398
399
QScrollArea* scrollarea = new QScrollArea(parent);
400
QWidget* scrollarea_widget = new QWidget(scrollarea);
401
scrollarea->setWidget(scrollarea_widget);
402
scrollarea->setWidgetResizable(true);
403
scrollarea->setFrameShape(QFrame::StyledPanel);
404
scrollarea->setFrameShadow(QFrame::Sunken);
405
406
// We do axes and buttons separately, so we can figure out how many columns to use.
407
constexpr int NUM_AXIS_COLUMNS = 2;
408
int column = 0;
409
int row = 0;
410
for (const Controller::ControllerBindingInfo& bi : m_controller_info->bindings)
411
{
412
if (bi.type == InputBindingInfo::Type::Axis || bi.type == InputBindingInfo::Type::HalfAxis ||
413
bi.type == InputBindingInfo::Type::Pointer || bi.type == InputBindingInfo::Type::RelativePointer ||
414
bi.type == InputBindingInfo::Type::Device || bi.type == InputBindingInfo::Type::Motor ||
415
bi.type == InputBindingInfo::Type::LED)
416
{
417
if (!axis_gbox)
418
{
419
axis_gbox = new QGroupBox(tr("Axes"), scrollarea_widget);
420
axis_layout = new QGridLayout(axis_gbox);
421
}
422
423
QGroupBox* const gbox =
424
new QGroupBox(QtUtils::StringViewToQString(m_controller_info->GetBindingDisplayName(bi)), axis_gbox);
425
QVBoxLayout* const temp = new QVBoxLayout(gbox);
426
QWidget* const widget = new InputBindingWidget(gbox, sif, bi.type, getConfigSection(), bi.name);
427
428
temp->addWidget(widget);
429
axis_layout->addWidget(gbox, row, column);
430
if ((++column) == NUM_AXIS_COLUMNS)
431
{
432
column = 0;
433
row++;
434
}
435
}
436
}
437
438
if (axis_gbox)
439
axis_layout->addItem(new QSpacerItem(1, 1, QSizePolicy::Minimum, QSizePolicy::Expanding), ++row, 0);
440
441
const int num_button_columns = axis_layout ? 2 : 4;
442
row = 0;
443
column = 0;
444
for (const Controller::ControllerBindingInfo& bi : m_controller_info->bindings)
445
{
446
if (bi.type == InputBindingInfo::Type::Button)
447
{
448
if (!button_gbox)
449
{
450
button_gbox = new QGroupBox(tr("Buttons"), scrollarea_widget);
451
button_layout = new QGridLayout(button_gbox);
452
}
453
454
QGroupBox* gbox =
455
new QGroupBox(QtUtils::StringViewToQString(m_controller_info->GetBindingDisplayName(bi)), button_gbox);
456
QVBoxLayout* temp = new QVBoxLayout(gbox);
457
InputBindingWidget* widget = new InputBindingWidget(gbox, sif, bi.type, getConfigSection(), bi.name);
458
temp->addWidget(widget);
459
button_layout->addWidget(gbox, row, column);
460
if ((++column) == num_button_columns)
461
{
462
column = 0;
463
row++;
464
}
465
}
466
}
467
468
if (button_gbox)
469
button_layout->addItem(new QSpacerItem(1, 1, QSizePolicy::Minimum, QSizePolicy::Expanding), ++row, 0);
470
471
if (!axis_gbox && !button_gbox)
472
{
473
delete scrollarea_widget;
474
delete scrollarea;
475
return;
476
}
477
478
QHBoxLayout* layout = new QHBoxLayout(scrollarea_widget);
479
if (axis_gbox)
480
layout->addWidget(axis_gbox, 1);
481
if (button_gbox)
482
layout->addWidget(button_gbox, 1);
483
484
QHBoxLayout* main_layout = new QHBoxLayout(parent);
485
main_layout->addWidget(scrollarea);
486
}
487
488
void ControllerBindingWidget::bindBindingWidgets(QWidget* parent)
489
{
490
SettingsInterface* sif = getDialog()->getEditingSettingsInterface();
491
DebugAssert(m_controller_info);
492
493
const std::string& config_section = getConfigSection();
494
for (const Controller::ControllerBindingInfo& bi : m_controller_info->bindings)
495
{
496
if (bi.type == InputBindingInfo::Type::Axis || bi.type == InputBindingInfo::Type::HalfAxis ||
497
bi.type == InputBindingInfo::Type::Button || bi.type == InputBindingInfo::Type::Pointer ||
498
bi.type == InputBindingInfo::Type::RelativePointer || bi.type == InputBindingInfo::Type::Motor ||
499
bi.type == InputBindingInfo::Type::LED)
500
{
501
InputBindingWidget* widget = parent->findChild<InputBindingWidget*>(QString::fromUtf8(bi.name));
502
if (!widget)
503
{
504
ERROR_LOG("No widget found for '{}' ({})", bi.name, m_controller_info->name);
505
continue;
506
}
507
508
widget->initialize(sif, bi.type, config_section, bi.name);
509
}
510
}
511
}
512
513
//////////////////////////////////////////////////////////////////////////
514
515
ControllerMacroWidget::ControllerMacroWidget(ControllerBindingWidget* parent) : QSplitter(parent)
516
{
517
setChildrenCollapsible(false);
518
setWindowTitle(tr("Controller Port %1 Macros").arg(parent->getPortNumber() + 1u));
519
createWidgets(parent);
520
}
521
522
ControllerMacroWidget::~ControllerMacroWidget() = default;
523
524
void ControllerMacroWidget::updateListItem(u32 index)
525
{
526
QString summary = m_macros[index]->getSummary();
527
QListWidgetItem* item = m_macroList->item(static_cast<int>(index));
528
item->setText(tr("Macro %1\n%2").arg(index + 1).arg(summary));
529
item->setToolTip(summary);
530
}
531
532
void ControllerMacroWidget::createWidgets(ControllerBindingWidget* bwidget)
533
{
534
m_macroList = new QListWidget(this);
535
m_macroList->setIconSize(QSize(32, 32));
536
m_macroList->setMinimumWidth(150);
537
addWidget(m_macroList);
538
setStretchFactor(0, 1);
539
540
m_container = new QStackedWidget(this);
541
addWidget(m_container);
542
setStretchFactor(1, 3);
543
544
for (u32 i = 0; i < m_macros.size(); i++)
545
{
546
m_macros[i] = new ControllerMacroEditWidget(this, bwidget, i);
547
m_container->addWidget(m_macros[i]);
548
549
QListWidgetItem* item = new QListWidgetItem();
550
item->setIcon(QIcon::fromTheme("flashlight-line"_L1));
551
m_macroList->addItem(item);
552
updateListItem(i);
553
}
554
555
m_macroList->setCurrentRow(0);
556
m_container->setCurrentIndex(0);
557
558
connect(m_macroList, &QListWidget::currentRowChanged, m_container, &QStackedWidget::setCurrentIndex);
559
}
560
561
//////////////////////////////////////////////////////////////////////////
562
563
ControllerMacroEditWidget::ControllerMacroEditWidget(ControllerMacroWidget* parent, ControllerBindingWidget* bwidget,
564
u32 index)
565
: QWidget(parent), m_parent(parent), m_bwidget(bwidget), m_index(index)
566
{
567
m_ui.setupUi(this);
568
m_ui.increaseFrequency->setIcon(style()->standardIcon(QStyle::SP_ArrowUp));
569
m_ui.decreateFrequency->setIcon(style()->standardIcon(QStyle::SP_ArrowDown));
570
571
ControllerSettingsWindow* dialog = m_bwidget->getDialog();
572
const std::string& section = m_bwidget->getConfigSection();
573
const Controller::ControllerInfo* cinfo = m_bwidget->getControllerInfo();
574
DebugAssert(cinfo);
575
576
// load binds (single string joined by &)
577
const std::string binds_string(
578
dialog->getStringValue(section.c_str(), TinyString::from_format("Macro{}Binds", index + 1u), ""));
579
const std::vector<std::string_view> buttons_split(StringUtil::SplitString(binds_string, '&', true));
580
581
for (const std::string_view& button : buttons_split)
582
{
583
for (const Controller::ControllerBindingInfo& bi : cinfo->bindings)
584
{
585
if (button == bi.name)
586
{
587
m_binds.push_back(&bi);
588
break;
589
}
590
}
591
}
592
593
// populate list view
594
for (const Controller::ControllerBindingInfo& bi : cinfo->bindings)
595
{
596
if (bi.type == InputBindingInfo::Type::Motor)
597
continue;
598
599
QListWidgetItem* item = new QListWidgetItem();
600
item->setText(QtUtils::StringViewToQString(cinfo->GetBindingDisplayName(bi)));
601
item->setCheckState((std::find(m_binds.begin(), m_binds.end(), &bi) != m_binds.end()) ? Qt::Checked :
602
Qt::Unchecked);
603
m_ui.bindList->addItem(item);
604
}
605
606
ControllerSettingWidgetBinder::BindWidgetToInputProfileNormalized(
607
dialog->getEditingSettingsInterface(), m_ui.pressure, section, fmt::format("Macro{}Pressure", index + 1u), 100.0f,
608
1.0f);
609
ControllerSettingWidgetBinder::BindWidgetToInputProfileNormalized(
610
dialog->getEditingSettingsInterface(), m_ui.deadzone, section, fmt::format("Macro{}Deadzone", index + 1u), 100.0f,
611
0.0f);
612
connect(m_ui.pressure, &QSlider::valueChanged, this, &ControllerMacroEditWidget::onPressureChanged);
613
connect(m_ui.deadzone, &QSlider::valueChanged, this, &ControllerMacroEditWidget::onDeadzoneChanged);
614
onPressureChanged();
615
onDeadzoneChanged();
616
617
m_frequency = dialog->getIntValue(section.c_str(), TinyString::from_format("Macro{}Frequency", index + 1u), 0);
618
ControllerSettingWidgetBinder::BindWidgetToInputProfileBool(dialog->getEditingSettingsInterface(), m_ui.triggerToggle,
619
section.c_str(), fmt::format("Macro{}Toggle", index + 1u),
620
false);
621
updateFrequencyText();
622
623
m_ui.trigger->initialize(dialog->getEditingSettingsInterface(), InputBindingInfo::Type::Macro, section,
624
fmt::format("Macro{}", index + 1u));
625
626
connect(m_ui.increaseFrequency, &QAbstractButton::clicked, this, [this]() { modFrequency(1); });
627
connect(m_ui.decreateFrequency, &QAbstractButton::clicked, this, [this]() { modFrequency(-1); });
628
connect(m_ui.setFrequency, &QAbstractButton::clicked, this, &ControllerMacroEditWidget::onSetFrequencyClicked);
629
connect(m_ui.bindList, &QListWidget::itemChanged, this, &ControllerMacroEditWidget::updateBinds);
630
}
631
632
ControllerMacroEditWidget::~ControllerMacroEditWidget() = default;
633
634
QString ControllerMacroEditWidget::getSummary() const
635
{
636
SmallString str;
637
for (const Controller::ControllerBindingInfo* bi : m_binds)
638
{
639
if (!str.empty())
640
str.append('/');
641
str.append(bi->name);
642
}
643
return str.empty() ? tr("Not Configured") : QString::fromUtf8(str.c_str(), static_cast<int>(str.length()));
644
}
645
646
void ControllerMacroEditWidget::onPressureChanged()
647
{
648
m_ui.pressureValue->setText(tr("%1%").arg(m_ui.pressure->value()));
649
}
650
651
void ControllerMacroEditWidget::onDeadzoneChanged()
652
{
653
m_ui.deadzoneValue->setText(tr("%1%").arg(m_ui.deadzone->value()));
654
}
655
656
void ControllerMacroEditWidget::onSetFrequencyClicked()
657
{
658
bool okay;
659
int new_freq = QInputDialog::getInt(this, tr("Set Frequency"), tr("Frequency: "), static_cast<int>(m_frequency), 0,
660
std::numeric_limits<int>::max(), 1, &okay);
661
if (!okay)
662
return;
663
664
m_frequency = static_cast<u32>(new_freq);
665
updateFrequency();
666
}
667
668
void ControllerMacroEditWidget::modFrequency(s32 delta)
669
{
670
if (delta < 0 && m_frequency == 0)
671
return;
672
673
m_frequency = static_cast<u32>(static_cast<s32>(m_frequency) + delta);
674
updateFrequency();
675
}
676
677
void ControllerMacroEditWidget::updateFrequency()
678
{
679
m_bwidget->getDialog()->setIntValue(m_bwidget->getConfigSection().c_str(),
680
fmt::format("Macro{}Frequency", m_index + 1u).c_str(),
681
static_cast<s32>(m_frequency));
682
updateFrequencyText();
683
}
684
685
void ControllerMacroEditWidget::updateFrequencyText()
686
{
687
if (m_frequency == 0)
688
m_ui.frequencyText->setText(tr("Macro will not repeat."));
689
else
690
m_ui.frequencyText->setText(tr("Macro will toggle buttons every %n frame(s).", nullptr, m_frequency));
691
}
692
693
void ControllerMacroEditWidget::updateBinds()
694
{
695
ControllerSettingsWindow* dialog = m_bwidget->getDialog();
696
const Controller::ControllerInfo* cinfo = m_bwidget->getControllerInfo();
697
DebugAssert(cinfo);
698
699
std::vector<const Controller::ControllerBindingInfo*> new_binds;
700
u32 bind_index = 0;
701
for (const Controller::ControllerBindingInfo& bi : cinfo->bindings)
702
{
703
if (bi.type == InputBindingInfo::Type::Motor)
704
continue;
705
706
const QListWidgetItem* item = m_ui.bindList->item(static_cast<int>(bind_index));
707
bind_index++;
708
709
if (!item)
710
{
711
// shouldn't happen
712
continue;
713
}
714
715
if (item->checkState() == Qt::Checked)
716
new_binds.push_back(&bi);
717
}
718
if (m_binds == new_binds)
719
return;
720
721
m_binds = std::move(new_binds);
722
723
std::string binds_string;
724
for (const Controller::ControllerBindingInfo* bi : m_binds)
725
{
726
if (!binds_string.empty())
727
binds_string.append(" & ");
728
binds_string.append(bi->name);
729
}
730
731
const std::string& section = m_bwidget->getConfigSection();
732
const std::string key(fmt::format("Macro{}Binds", m_index + 1u));
733
if (binds_string.empty())
734
dialog->clearSettingValue(section.c_str(), key.c_str());
735
else
736
dialog->setStringValue(section.c_str(), key.c_str(), binds_string.c_str());
737
738
m_parent->updateListItem(m_index);
739
}
740
741
//////////////////////////////////////////////////////////////////////////
742
743
static void createSettingWidgets(SettingsInterface* const sif, QWidget* parent_widget, QGridLayout* layout,
744
const std::string& section, std::span<const SettingInfo> settings,
745
const char* tr_context)
746
{
747
int current_row = 0;
748
749
for (const SettingInfo& si : settings)
750
{
751
std::string key_name = si.name;
752
753
switch (si.type)
754
{
755
case SettingInfo::Type::Boolean:
756
{
757
QCheckBox* cb = new QCheckBox(qApp->translate(tr_context, si.display_name), parent_widget);
758
cb->setObjectName(si.name);
759
ControllerSettingWidgetBinder::BindWidgetToInputProfileBool(sif, cb, section, std::move(key_name),
760
si.BooleanDefaultValue());
761
layout->addWidget(cb, current_row, 0, 1, 4);
762
current_row++;
763
}
764
break;
765
766
case SettingInfo::Type::Integer:
767
{
768
QSpinBox* sb = new QSpinBox(parent_widget);
769
sb->setObjectName(si.name);
770
sb->setMinimum(si.IntegerMinValue());
771
sb->setMaximum(si.IntegerMaxValue());
772
sb->setSingleStep(si.IntegerStepValue());
773
ControllerSettingWidgetBinder::BindWidgetToInputProfileInt(sif, sb, section, std::move(key_name),
774
si.IntegerDefaultValue());
775
layout->addWidget(new QLabel(qApp->translate(tr_context, si.display_name), parent_widget), current_row, 0);
776
layout->addWidget(sb, current_row, 1, 1, 3);
777
current_row++;
778
}
779
break;
780
781
case SettingInfo::Type::IntegerList:
782
{
783
QComboBox* cb = new QComboBox(parent_widget);
784
cb->setObjectName(si.name);
785
for (u32 j = 0; si.options[j] != nullptr; j++)
786
cb->addItem(qApp->translate(tr_context, si.options[j]));
787
ControllerSettingWidgetBinder::BindWidgetToInputProfileInt(sif, cb, section, std::move(key_name),
788
si.IntegerDefaultValue(), si.IntegerMinValue());
789
layout->addWidget(new QLabel(qApp->translate(tr_context, si.display_name), parent_widget), current_row, 0);
790
layout->addWidget(cb, current_row, 1, 1, 3);
791
current_row++;
792
}
793
break;
794
795
case SettingInfo::Type::Float:
796
{
797
QDoubleSpinBox* sb = new QDoubleSpinBox(parent_widget);
798
sb->setObjectName(si.name);
799
if (si.multiplier != 0.0f && si.multiplier != 1.0f)
800
{
801
const float multiplier = si.multiplier;
802
sb->setMinimum(si.FloatMinValue() * multiplier);
803
sb->setMaximum(si.FloatMaxValue() * multiplier);
804
sb->setSingleStep(si.FloatStepValue() * multiplier);
805
if (std::abs(si.multiplier - 100.0f) < 0.01f)
806
{
807
sb->setDecimals(0);
808
sb->setSuffix("%"_L1);
809
}
810
811
ControllerSettingWidgetBinder::BindWidgetToInputProfileNormalized(sif, sb, section, std::move(key_name),
812
si.multiplier, si.FloatDefaultValue());
813
}
814
else
815
{
816
sb->setMinimum(si.FloatMinValue());
817
sb->setMaximum(si.FloatMaxValue());
818
sb->setSingleStep(si.FloatStepValue());
819
820
ControllerSettingWidgetBinder::BindWidgetToInputProfileFloat(sif, sb, section, std::move(key_name),
821
si.FloatDefaultValue());
822
}
823
layout->addWidget(new QLabel(qApp->translate(tr_context, si.display_name), parent_widget), current_row, 0);
824
layout->addWidget(sb, current_row, 1, 1, 3);
825
current_row++;
826
}
827
break;
828
829
case SettingInfo::Type::String:
830
{
831
QLineEdit* le = new QLineEdit(parent_widget);
832
le->setObjectName(si.name);
833
ControllerSettingWidgetBinder::BindWidgetToInputProfileString(sif, le, section, std::move(key_name),
834
si.StringDefaultValue());
835
layout->addWidget(new QLabel(qApp->translate(tr_context, si.display_name), parent_widget), 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(parent_widget);
844
le->setObjectName(si.name);
845
QPushButton* browse_button =
846
new QPushButton(qApp->translate("ControllerCustomSettingsWidget", "Browse..."), parent_widget);
847
ControllerSettingWidgetBinder::BindWidgetToInputProfileString(sif, le, section, std::move(key_name),
848
si.StringDefaultValue());
849
QObject::connect(browse_button, &QPushButton::clicked, [le, root = parent_widget]() {
850
QString path = QDir::toNativeSeparators(
851
QFileDialog::getOpenFileName(root, qApp->translate("ControllerCustomSettingsWidget", "Select File")));
852
if (!path.isEmpty())
853
le->setText(path);
854
});
855
856
QHBoxLayout* hbox = new QHBoxLayout();
857
hbox->addWidget(le, 1);
858
hbox->addWidget(browse_button);
859
860
layout->addWidget(new QLabel(qApp->translate(tr_context, si.display_name), parent_widget), current_row, 0);
861
layout->addLayout(hbox, current_row, 1, 1, 3);
862
current_row++;
863
}
864
break;
865
}
866
867
QLabel* label = new QLabel(si.description ? qApp->translate(tr_context, si.description) : QString(), parent_widget);
868
label->setWordWrap(true);
869
layout->addWidget(label, current_row++, 0, 1, 4);
870
871
layout->addItem(new QSpacerItem(1, 10, QSizePolicy::Minimum, QSizePolicy::Fixed), current_row++, 0, 1, 4);
872
}
873
}
874
875
static void restoreDefaultSettingWidgets(QWidget* parent_widget, std::span<const SettingInfo> settings)
876
{
877
for (const SettingInfo& si : settings)
878
{
879
switch (si.type)
880
{
881
case SettingInfo::Type::Boolean:
882
{
883
QCheckBox* widget = parent_widget->findChild<QCheckBox*>(si.name);
884
if (widget)
885
widget->setChecked(si.BooleanDefaultValue());
886
}
887
break;
888
889
case SettingInfo::Type::Integer:
890
{
891
QSpinBox* widget = parent_widget->findChild<QSpinBox*>(si.name);
892
if (widget)
893
widget->setValue(si.IntegerDefaultValue());
894
}
895
break;
896
897
case SettingInfo::Type::IntegerList:
898
{
899
QComboBox* widget = parent_widget->findChild<QComboBox*>(si.name);
900
if (widget)
901
widget->setCurrentIndex(si.IntegerDefaultValue() - si.IntegerMinValue());
902
}
903
break;
904
905
case SettingInfo::Type::Float:
906
{
907
QDoubleSpinBox* widget = parent_widget->findChild<QDoubleSpinBox*>(si.name);
908
if (widget)
909
{
910
if (si.multiplier != 0.0f && si.multiplier != 1.0f)
911
widget->setValue(si.FloatDefaultValue() * si.multiplier);
912
else
913
widget->setValue(si.FloatDefaultValue());
914
}
915
}
916
break;
917
918
case SettingInfo::Type::String:
919
{
920
QLineEdit* widget = parent_widget->findChild<QLineEdit*>(si.name);
921
if (widget)
922
widget->setText(QString::fromUtf8(si.StringDefaultValue()));
923
}
924
break;
925
926
case SettingInfo::Type::Path:
927
{
928
QLineEdit* widget = parent_widget->findChild<QLineEdit*>(si.name);
929
if (widget)
930
widget->setText(QString::fromUtf8(si.StringDefaultValue()));
931
}
932
break;
933
}
934
}
935
}
936
937
ControllerCustomSettingsWidget::ControllerCustomSettingsWidget(ControllerBindingWidget* parent)
938
: QWidget(parent), m_parent(parent)
939
{
940
const Controller::ControllerInfo* cinfo = parent->getControllerInfo();
941
DebugAssert(cinfo);
942
if (cinfo->settings.empty())
943
return;
944
945
QScrollArea* sarea = new QScrollArea(this);
946
QWidget* swidget = new QWidget(sarea);
947
sarea->setWidget(swidget);
948
sarea->setWidgetResizable(true);
949
sarea->setFrameShape(QFrame::StyledPanel);
950
sarea->setFrameShadow(QFrame::Sunken);
951
952
QGridLayout* swidget_layout = new QGridLayout(swidget);
953
createSettingWidgets(parent->getDialog()->getEditingSettingsInterface(), swidget, swidget_layout,
954
parent->getConfigSection(), cinfo->settings, cinfo->name);
955
956
int current_row = swidget_layout->rowCount();
957
958
QHBoxLayout* bottom_hlayout = new QHBoxLayout();
959
QPushButton* restore_defaults = new QPushButton(tr("Restore Default Settings"), swidget);
960
restore_defaults->setIcon(QIcon::fromTheme("restart-line"_L1));
961
bottom_hlayout->addStretch(1);
962
bottom_hlayout->addWidget(restore_defaults);
963
swidget_layout->addLayout(bottom_hlayout, current_row++, 0, 1, 4);
964
connect(restore_defaults, &QPushButton::clicked, this, &ControllerCustomSettingsWidget::restoreDefaults);
965
966
swidget_layout->addItem(new QSpacerItem(1, 1, QSizePolicy::Minimum, QSizePolicy::Expanding), current_row++, 0, 1, 4);
967
968
QVBoxLayout* layout = new QVBoxLayout(this);
969
layout->setContentsMargins(0, 0, 0, 0);
970
layout->addWidget(sarea);
971
}
972
973
ControllerCustomSettingsWidget::~ControllerCustomSettingsWidget() = default;
974
975
void ControllerCustomSettingsWidget::restoreDefaults()
976
{
977
const Controller::ControllerInfo* cinfo = m_parent->getControllerInfo();
978
DebugAssert(cinfo);
979
980
restoreDefaultSettingWidgets(this, cinfo->settings);
981
}
982
983
ControllerCustomSettingsDialog::ControllerCustomSettingsDialog(QWidget* parent, SettingsInterface* sif,
984
const std::string& section,
985
std::span<const SettingInfo> settings,
986
const char* tr_context, const QString& window_title)
987
: QDialog(parent)
988
{
989
setMinimumWidth(500);
990
resize(minimumWidth(), 100);
991
setWindowTitle(window_title);
992
993
QGridLayout* layout = new QGridLayout(this);
994
createSettingWidgets(sif, this, layout, section, settings, tr_context);
995
996
QDialogButtonBox* bbox = new QDialogButtonBox(QDialogButtonBox::Close | QDialogButtonBox::RestoreDefaults, this);
997
bbox->button(QDialogButtonBox::Close)->setDefault(true);
998
connect(bbox, &QDialogButtonBox::rejected, this, &ControllerCustomSettingsDialog::accept);
999
connect(bbox->button(QDialogButtonBox::RestoreDefaults), &QPushButton::clicked, this,
1000
[this, settings]() { restoreDefaultSettingWidgets(this, settings); });
1001
layout->addWidget(bbox, layout->rowCount(), 0, 1, 4);
1002
}
1003
1004
ControllerCustomSettingsDialog::~ControllerCustomSettingsDialog() = default;
1005
1006
MultipleDeviceAutobindDialog::MultipleDeviceAutobindDialog(QWidget* parent, ControllerSettingsWindow* settings_window,
1007
u32 port)
1008
: QDialog(parent), m_settings_window(settings_window), m_port(port)
1009
{
1010
QVBoxLayout* layout = new QVBoxLayout(this);
1011
layout->addWidget(
1012
new QLabel(tr("Select the devices from the list below that you want to bind to this controller."), this));
1013
1014
m_list = new QListWidget(this);
1015
m_list->setSelectionMode(QListWidget::SingleSelection);
1016
layout->addWidget(m_list);
1017
1018
for (const InputDeviceListModel::Device& dev : g_core_thread->getInputDeviceListModel()->getDeviceList())
1019
{
1020
QListWidgetItem* item = new QListWidgetItem;
1021
item->setIcon(InputDeviceListModel::getIconForKey(dev.key));
1022
item->setText(QStringLiteral("%1 (%2)").arg(dev.identifier).arg(dev.display_name));
1023
item->setData(Qt::UserRole, dev.identifier);
1024
item->setFlags(item->flags() | Qt::ItemIsUserCheckable);
1025
item->setCheckState(Qt::Unchecked);
1026
m_list->addItem(item);
1027
}
1028
1029
QDialogButtonBox* bb = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this);
1030
connect(bb, &QDialogButtonBox::accepted, this, &MultipleDeviceAutobindDialog::doAutomaticBinding);
1031
connect(bb, &QDialogButtonBox::rejected, this, &QDialog::reject);
1032
layout->addWidget(bb);
1033
}
1034
1035
MultipleDeviceAutobindDialog::~MultipleDeviceAutobindDialog() = default;
1036
1037
void MultipleDeviceAutobindDialog::doAutomaticBinding()
1038
{
1039
auto lock = Core::GetSettingsLock();
1040
const bool global = (!m_settings_window || m_settings_window->isEditingGlobalSettings());
1041
SettingsInterface* si = global ? Core::GetBaseSettingsLayer() : m_settings_window->getEditingSettingsInterface();
1042
1043
// first device should clear mappings
1044
bool tried_any = false;
1045
bool mapped_any = false;
1046
const int count = m_list->count();
1047
for (int i = 0; i < count; i++)
1048
{
1049
const QListWidgetItem* item = m_list->item(i);
1050
if (item->checkState() != Qt::Checked)
1051
continue;
1052
1053
tried_any = true;
1054
1055
const QString identifier = item->data(Qt::UserRole).toString();
1056
std::vector<std::pair<GenericInputBinding, std::string>> mapping =
1057
InputManager::GetGenericBindingMapping(identifier.toStdString());
1058
if (mapping.empty())
1059
{
1060
lock.unlock();
1061
QtUtils::MessageBoxCritical(
1062
this, tr("Automatic Mapping Failed"),
1063
tr("No generic bindings were generated for device '%1'. The controller/source may not "
1064
"support automatic mapping.")
1065
.arg(identifier));
1066
lock.lock();
1067
continue;
1068
}
1069
1070
mapped_any |= InputManager::MapController(*si, m_port, mapping, !mapped_any);
1071
}
1072
1073
lock.unlock();
1074
1075
if (!tried_any)
1076
{
1077
QtUtils::AsyncMessageBox(this, QMessageBox::Critical, tr("Automatic Mapping Failed"),
1078
tr("No devices were selected."));
1079
return;
1080
}
1081
1082
if (mapped_any)
1083
{
1084
if (global)
1085
{
1086
QtHost::SaveGameSettings(si, false);
1087
g_core_thread->reloadGameSettings(false);
1088
}
1089
else
1090
{
1091
QtHost::QueueSettingsSave();
1092
g_core_thread->reloadInputBindings();
1093
}
1094
accept();
1095
}
1096
else
1097
{
1098
reject();
1099
}
1100
}
1101
1102