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