Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
stenzek
GitHub Repository: stenzek/duckstation
Path: blob/master/src/duckstation-qt/controllerbindingwidgets.cpp
4246 views
1
// SPDX-FileCopyrightText: 2019-2025 Connor McLaughlin <[email protected]>
2
// SPDX-License-Identifier: CC-BY-NC-ND-4.0
3
4
#include "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
{
504
if (!axis_gbox)
505
{
506
axis_gbox = new QGroupBox(tr("Axes"), scrollarea_widget);
507
axis_layout = new QGridLayout(axis_gbox);
508
}
509
510
QGroupBox* gbox =
511
new QGroupBox(QtUtils::StringViewToQString(m_controller_info->GetBindingDisplayName(bi)), axis_gbox);
512
QVBoxLayout* temp = new QVBoxLayout(gbox);
513
QWidget* widget;
514
if (bi.type != InputBindingInfo::Type::Motor)
515
widget = new InputBindingWidget(gbox, sif, bi.type, getConfigSection(), bi.name);
516
else
517
widget = new InputVibrationBindingWidget(gbox, getDialog(), getConfigSection(), bi.name);
518
519
temp->addWidget(widget);
520
axis_layout->addWidget(gbox, row, column);
521
if ((++column) == NUM_AXIS_COLUMNS)
522
{
523
column = 0;
524
row++;
525
}
526
}
527
}
528
529
if (axis_gbox)
530
axis_layout->addItem(new QSpacerItem(1, 1, QSizePolicy::Minimum, QSizePolicy::Expanding), ++row, 0);
531
532
const int num_button_columns = axis_layout ? 2 : 4;
533
row = 0;
534
column = 0;
535
for (const Controller::ControllerBindingInfo& bi : m_controller_info->bindings)
536
{
537
if (bi.type == InputBindingInfo::Type::Button)
538
{
539
if (!button_gbox)
540
{
541
button_gbox = new QGroupBox(tr("Buttons"), scrollarea_widget);
542
button_layout = new QGridLayout(button_gbox);
543
}
544
545
QGroupBox* gbox =
546
new QGroupBox(QtUtils::StringViewToQString(m_controller_info->GetBindingDisplayName(bi)), button_gbox);
547
QVBoxLayout* temp = new QVBoxLayout(gbox);
548
InputBindingWidget* widget = new InputBindingWidget(gbox, sif, bi.type, getConfigSection(), bi.name);
549
temp->addWidget(widget);
550
button_layout->addWidget(gbox, row, column);
551
if ((++column) == num_button_columns)
552
{
553
column = 0;
554
row++;
555
}
556
}
557
}
558
559
if (button_gbox)
560
button_layout->addItem(new QSpacerItem(1, 1, QSizePolicy::Minimum, QSizePolicy::Expanding), ++row, 0);
561
562
if (!axis_gbox && !button_gbox)
563
{
564
delete scrollarea_widget;
565
delete scrollarea;
566
return;
567
}
568
569
QHBoxLayout* layout = new QHBoxLayout(scrollarea_widget);
570
if (axis_gbox)
571
layout->addWidget(axis_gbox, 1);
572
if (button_gbox)
573
layout->addWidget(button_gbox, 1);
574
575
QHBoxLayout* main_layout = new QHBoxLayout(parent);
576
main_layout->addWidget(scrollarea);
577
}
578
579
void ControllerBindingWidget::bindBindingWidgets(QWidget* parent)
580
{
581
SettingsInterface* sif = getDialog()->getEditingSettingsInterface();
582
DebugAssert(m_controller_info);
583
584
const std::string& config_section = getConfigSection();
585
for (const Controller::ControllerBindingInfo& bi : m_controller_info->bindings)
586
{
587
if (bi.type == InputBindingInfo::Type::Axis || bi.type == InputBindingInfo::Type::HalfAxis ||
588
bi.type == InputBindingInfo::Type::Button || bi.type == InputBindingInfo::Type::Pointer ||
589
bi.type == InputBindingInfo::Type::RelativePointer)
590
{
591
InputBindingWidget* widget = parent->findChild<InputBindingWidget*>(QString::fromUtf8(bi.name));
592
if (!widget)
593
{
594
ERROR_LOG("No widget found for '{}' ({})", bi.name, m_controller_info->name);
595
continue;
596
}
597
598
widget->initialize(sif, bi.type, config_section, bi.name);
599
}
600
else if (bi.type == InputBindingInfo::Type::Motor)
601
{
602
InputVibrationBindingWidget* widget = parent->findChild<InputVibrationBindingWidget*>(QString::fromUtf8(bi.name));
603
if (widget)
604
widget->setKey(getDialog(), config_section, bi.name);
605
}
606
}
607
}
608
609
//////////////////////////////////////////////////////////////////////////
610
611
ControllerMacroWidget::ControllerMacroWidget(ControllerBindingWidget* parent) : QWidget(parent)
612
{
613
m_ui.setupUi(this);
614
setWindowTitle(tr("Controller Port %1 Macros").arg(parent->getPortNumber() + 1u));
615
createWidgets(parent);
616
}
617
618
ControllerMacroWidget::~ControllerMacroWidget() = default;
619
620
void ControllerMacroWidget::updateListItem(u32 index)
621
{
622
m_ui.portList->item(static_cast<int>(index))
623
->setText(tr("Macro %1\n%2").arg(index + 1).arg(m_macros[index]->getSummary()));
624
}
625
626
void ControllerMacroWidget::createWidgets(ControllerBindingWidget* parent)
627
{
628
for (u32 i = 0; i < NUM_MACROS; i++)
629
{
630
m_macros[i] = new ControllerMacroEditWidget(this, parent, i);
631
m_ui.container->addWidget(m_macros[i]);
632
633
QListWidgetItem* item = new QListWidgetItem();
634
item->setIcon(QIcon::fromTheme(QStringLiteral("flashlight-line")));
635
m_ui.portList->addItem(item);
636
updateListItem(i);
637
}
638
639
m_ui.portList->setCurrentRow(0);
640
m_ui.container->setCurrentIndex(0);
641
642
connect(m_ui.portList, &QListWidget::currentRowChanged, m_ui.container, &QStackedWidget::setCurrentIndex);
643
}
644
645
//////////////////////////////////////////////////////////////////////////
646
647
ControllerMacroEditWidget::ControllerMacroEditWidget(ControllerMacroWidget* parent, ControllerBindingWidget* bwidget,
648
u32 index)
649
: QWidget(parent), m_parent(parent), m_bwidget(bwidget), m_index(index)
650
{
651
m_ui.setupUi(this);
652
653
ControllerSettingsWindow* dialog = m_bwidget->getDialog();
654
const std::string& section = m_bwidget->getConfigSection();
655
const Controller::ControllerInfo* cinfo = m_bwidget->getControllerInfo();
656
DebugAssert(cinfo);
657
658
// load binds (single string joined by &)
659
const std::string binds_string(
660
dialog->getStringValue(section.c_str(), TinyString::from_format("Macro{}Binds", index + 1u), ""));
661
const std::vector<std::string_view> buttons_split(StringUtil::SplitString(binds_string, '&', true));
662
663
for (const std::string_view& button : buttons_split)
664
{
665
for (const Controller::ControllerBindingInfo& bi : cinfo->bindings)
666
{
667
if (button == bi.name)
668
{
669
m_binds.push_back(&bi);
670
break;
671
}
672
}
673
}
674
675
// populate list view
676
for (const Controller::ControllerBindingInfo& bi : cinfo->bindings)
677
{
678
if (bi.type == InputBindingInfo::Type::Motor)
679
continue;
680
681
QListWidgetItem* item = new QListWidgetItem();
682
item->setText(QtUtils::StringViewToQString(cinfo->GetBindingDisplayName(bi)));
683
item->setCheckState((std::find(m_binds.begin(), m_binds.end(), &bi) != m_binds.end()) ? Qt::Checked :
684
Qt::Unchecked);
685
m_ui.bindList->addItem(item);
686
}
687
688
ControllerSettingWidgetBinder::BindWidgetToInputProfileNormalized(
689
dialog->getEditingSettingsInterface(), m_ui.pressure, section, fmt::format("Macro{}Pressure", index + 1u), 100.0f,
690
1.0f);
691
ControllerSettingWidgetBinder::BindWidgetToInputProfileNormalized(
692
dialog->getEditingSettingsInterface(), m_ui.deadzone, section, fmt::format("Macro{}Deadzone", index + 1u), 100.0f,
693
0.0f);
694
connect(m_ui.pressure, &QSlider::valueChanged, this, &ControllerMacroEditWidget::onPressureChanged);
695
connect(m_ui.deadzone, &QSlider::valueChanged, this, &ControllerMacroEditWidget::onDeadzoneChanged);
696
onPressureChanged();
697
onDeadzoneChanged();
698
699
m_frequency = dialog->getIntValue(section.c_str(), TinyString::from_format("Macro{}Frequency", index + 1u), 0);
700
ControllerSettingWidgetBinder::BindWidgetToInputProfileBool(dialog->getEditingSettingsInterface(), m_ui.triggerToggle,
701
section.c_str(), fmt::format("Macro{}Toggle", index + 1u),
702
false);
703
updateFrequencyText();
704
705
m_ui.trigger->initialize(dialog->getEditingSettingsInterface(), InputBindingInfo::Type::Macro, section,
706
fmt::format("Macro{}", index + 1u));
707
708
connect(m_ui.increaseFrequency, &QAbstractButton::clicked, this, [this]() { modFrequency(1); });
709
connect(m_ui.decreateFrequency, &QAbstractButton::clicked, this, [this]() { modFrequency(-1); });
710
connect(m_ui.setFrequency, &QAbstractButton::clicked, this, &ControllerMacroEditWidget::onSetFrequencyClicked);
711
connect(m_ui.bindList, &QListWidget::itemChanged, this, &ControllerMacroEditWidget::updateBinds);
712
}
713
714
ControllerMacroEditWidget::~ControllerMacroEditWidget() = default;
715
716
QString ControllerMacroEditWidget::getSummary() const
717
{
718
SmallString str;
719
for (const Controller::ControllerBindingInfo* bi : m_binds)
720
{
721
if (!str.empty())
722
str.append('/');
723
str.append(bi->name);
724
}
725
return str.empty() ? tr("Not Configured") : QString::fromUtf8(str.c_str(), static_cast<int>(str.length()));
726
}
727
728
void ControllerMacroEditWidget::onPressureChanged()
729
{
730
m_ui.pressureValue->setText(tr("%1%").arg(m_ui.pressure->value()));
731
}
732
733
void ControllerMacroEditWidget::onDeadzoneChanged()
734
{
735
m_ui.deadzoneValue->setText(tr("%1%").arg(m_ui.deadzone->value()));
736
}
737
738
void ControllerMacroEditWidget::onSetFrequencyClicked()
739
{
740
bool okay;
741
int new_freq = QInputDialog::getInt(this, tr("Set Frequency"), tr("Frequency: "), static_cast<int>(m_frequency), 0,
742
std::numeric_limits<int>::max(), 1, &okay);
743
if (!okay)
744
return;
745
746
m_frequency = static_cast<u32>(new_freq);
747
updateFrequency();
748
}
749
750
void ControllerMacroEditWidget::modFrequency(s32 delta)
751
{
752
if (delta < 0 && m_frequency == 0)
753
return;
754
755
m_frequency = static_cast<u32>(static_cast<s32>(m_frequency) + delta);
756
updateFrequency();
757
}
758
759
void ControllerMacroEditWidget::updateFrequency()
760
{
761
m_bwidget->getDialog()->setIntValue(m_bwidget->getConfigSection().c_str(),
762
fmt::format("Macro{}Frequency", m_index + 1u).c_str(),
763
static_cast<s32>(m_frequency));
764
updateFrequencyText();
765
}
766
767
void ControllerMacroEditWidget::updateFrequencyText()
768
{
769
if (m_frequency == 0)
770
m_ui.frequencyText->setText(tr("Macro will not repeat."));
771
else
772
m_ui.frequencyText->setText(tr("Macro will toggle buttons every %1 frames.").arg(m_frequency));
773
}
774
775
void ControllerMacroEditWidget::updateBinds()
776
{
777
ControllerSettingsWindow* dialog = m_bwidget->getDialog();
778
const Controller::ControllerInfo* cinfo = m_bwidget->getControllerInfo();
779
DebugAssert(cinfo);
780
781
std::vector<const Controller::ControllerBindingInfo*> new_binds;
782
u32 bind_index = 0;
783
for (const Controller::ControllerBindingInfo& bi : cinfo->bindings)
784
{
785
if (bi.type == InputBindingInfo::Type::Motor)
786
continue;
787
788
const QListWidgetItem* item = m_ui.bindList->item(static_cast<int>(bind_index));
789
bind_index++;
790
791
if (!item)
792
{
793
// shouldn't happen
794
continue;
795
}
796
797
if (item->checkState() == Qt::Checked)
798
new_binds.push_back(&bi);
799
}
800
if (m_binds == new_binds)
801
return;
802
803
m_binds = std::move(new_binds);
804
805
std::string binds_string;
806
for (const Controller::ControllerBindingInfo* bi : m_binds)
807
{
808
if (!binds_string.empty())
809
binds_string.append(" & ");
810
binds_string.append(bi->name);
811
}
812
813
const std::string& section = m_bwidget->getConfigSection();
814
const std::string key(fmt::format("Macro{}Binds", m_index + 1u));
815
if (binds_string.empty())
816
dialog->clearSettingValue(section.c_str(), key.c_str());
817
else
818
dialog->setStringValue(section.c_str(), key.c_str(), binds_string.c_str());
819
820
m_parent->updateListItem(m_index);
821
}
822
823
//////////////////////////////////////////////////////////////////////////
824
825
static void createSettingWidgets(SettingsInterface* const sif, QWidget* parent_widget, QGridLayout* layout,
826
const std::string& section, std::span<const SettingInfo> settings,
827
const char* tr_context)
828
{
829
int current_row = 0;
830
831
for (const SettingInfo& si : settings)
832
{
833
std::string key_name = si.name;
834
835
switch (si.type)
836
{
837
case SettingInfo::Type::Boolean:
838
{
839
QCheckBox* cb = new QCheckBox(qApp->translate(tr_context, si.display_name), parent_widget);
840
cb->setObjectName(QString::fromUtf8(si.name));
841
ControllerSettingWidgetBinder::BindWidgetToInputProfileBool(sif, cb, section, std::move(key_name),
842
si.BooleanDefaultValue());
843
layout->addWidget(cb, current_row, 0, 1, 4);
844
current_row++;
845
}
846
break;
847
848
case SettingInfo::Type::Integer:
849
{
850
QSpinBox* sb = new QSpinBox(parent_widget);
851
sb->setObjectName(QString::fromUtf8(si.name));
852
sb->setMinimum(si.IntegerMinValue());
853
sb->setMaximum(si.IntegerMaxValue());
854
sb->setSingleStep(si.IntegerStepValue());
855
ControllerSettingWidgetBinder::BindWidgetToInputProfileInt(sif, sb, section, std::move(key_name),
856
si.IntegerDefaultValue());
857
layout->addWidget(new QLabel(qApp->translate(tr_context, si.display_name), parent_widget), current_row, 0);
858
layout->addWidget(sb, current_row, 1, 1, 3);
859
current_row++;
860
}
861
break;
862
863
case SettingInfo::Type::IntegerList:
864
{
865
QComboBox* cb = new QComboBox(parent_widget);
866
cb->setObjectName(QString::fromUtf8(si.name));
867
for (u32 j = 0; si.options[j] != nullptr; j++)
868
cb->addItem(qApp->translate(tr_context, si.options[j]));
869
ControllerSettingWidgetBinder::BindWidgetToInputProfileInt(sif, cb, section, std::move(key_name),
870
si.IntegerDefaultValue(), si.IntegerMinValue());
871
layout->addWidget(new QLabel(qApp->translate(tr_context, si.display_name), parent_widget), current_row, 0);
872
layout->addWidget(cb, current_row, 1, 1, 3);
873
current_row++;
874
}
875
break;
876
877
case SettingInfo::Type::Float:
878
{
879
QDoubleSpinBox* sb = new QDoubleSpinBox(parent_widget);
880
sb->setObjectName(QString::fromUtf8(si.name));
881
if (si.multiplier != 0.0f && si.multiplier != 1.0f)
882
{
883
const float multiplier = si.multiplier;
884
sb->setMinimum(si.FloatMinValue() * multiplier);
885
sb->setMaximum(si.FloatMaxValue() * multiplier);
886
sb->setSingleStep(si.FloatStepValue() * multiplier);
887
if (std::abs(si.multiplier - 100.0f) < 0.01f)
888
{
889
sb->setDecimals(0);
890
sb->setSuffix(QStringLiteral("%"));
891
}
892
893
ControllerSettingWidgetBinder::BindWidgetToInputProfileNormalized(sif, sb, section, std::move(key_name),
894
si.multiplier, si.FloatDefaultValue());
895
}
896
else
897
{
898
sb->setMinimum(si.FloatMinValue());
899
sb->setMaximum(si.FloatMaxValue());
900
sb->setSingleStep(si.FloatStepValue());
901
902
ControllerSettingWidgetBinder::BindWidgetToInputProfileFloat(sif, sb, section, std::move(key_name),
903
si.FloatDefaultValue());
904
}
905
layout->addWidget(new QLabel(qApp->translate(tr_context, si.display_name), parent_widget), current_row, 0);
906
layout->addWidget(sb, current_row, 1, 1, 3);
907
current_row++;
908
}
909
break;
910
911
case SettingInfo::Type::String:
912
{
913
QLineEdit* le = new QLineEdit(parent_widget);
914
le->setObjectName(QString::fromUtf8(si.name));
915
ControllerSettingWidgetBinder::BindWidgetToInputProfileString(sif, le, section, std::move(key_name),
916
si.StringDefaultValue());
917
layout->addWidget(new QLabel(qApp->translate(tr_context, si.display_name), parent_widget), current_row, 0);
918
layout->addWidget(le, current_row, 1, 1, 3);
919
current_row++;
920
}
921
break;
922
923
case SettingInfo::Type::Path:
924
{
925
QLineEdit* le = new QLineEdit(parent_widget);
926
le->setObjectName(QString::fromUtf8(si.name));
927
QPushButton* browse_button =
928
new QPushButton(qApp->translate("ControllerCustomSettingsWidget", "Browse..."), parent_widget);
929
ControllerSettingWidgetBinder::BindWidgetToInputProfileString(sif, le, section, std::move(key_name),
930
si.StringDefaultValue());
931
QObject::connect(browse_button, &QPushButton::clicked, [le, root = QtUtils::GetRootWidget(parent_widget)]() {
932
QString path = QDir::toNativeSeparators(
933
QFileDialog::getOpenFileName(root, qApp->translate("ControllerCustomSettingsWidget", "Select File")));
934
if (!path.isEmpty())
935
le->setText(path);
936
});
937
938
QHBoxLayout* hbox = new QHBoxLayout();
939
hbox->addWidget(le, 1);
940
hbox->addWidget(browse_button);
941
942
layout->addWidget(new QLabel(qApp->translate(tr_context, si.display_name), parent_widget), current_row, 0);
943
layout->addLayout(hbox, current_row, 1, 1, 3);
944
current_row++;
945
}
946
break;
947
}
948
949
QLabel* label = new QLabel(si.description ? qApp->translate(tr_context, si.description) : QString(), parent_widget);
950
label->setWordWrap(true);
951
layout->addWidget(label, current_row++, 0, 1, 4);
952
953
layout->addItem(new QSpacerItem(1, 10, QSizePolicy::Minimum, QSizePolicy::Fixed), current_row++, 0, 1, 4);
954
}
955
}
956
957
static void restoreDefaultSettingWidgets(QWidget* parent_widget, std::span<const SettingInfo> settings)
958
{
959
for (const SettingInfo& si : settings)
960
{
961
const QString key(QString::fromStdString(si.name));
962
963
switch (si.type)
964
{
965
case SettingInfo::Type::Boolean:
966
{
967
QCheckBox* widget = parent_widget->findChild<QCheckBox*>(QString::fromStdString(si.name));
968
if (widget)
969
widget->setChecked(si.BooleanDefaultValue());
970
}
971
break;
972
973
case SettingInfo::Type::Integer:
974
{
975
QSpinBox* widget = parent_widget->findChild<QSpinBox*>(QString::fromStdString(si.name));
976
if (widget)
977
widget->setValue(si.IntegerDefaultValue());
978
}
979
break;
980
981
case SettingInfo::Type::IntegerList:
982
{
983
QComboBox* widget = parent_widget->findChild<QComboBox*>(QString::fromStdString(si.name));
984
if (widget)
985
widget->setCurrentIndex(si.IntegerDefaultValue() - si.IntegerMinValue());
986
}
987
break;
988
989
case SettingInfo::Type::Float:
990
{
991
QDoubleSpinBox* widget = parent_widget->findChild<QDoubleSpinBox*>(QString::fromStdString(si.name));
992
if (widget)
993
{
994
if (si.multiplier != 0.0f && si.multiplier != 1.0f)
995
widget->setValue(si.FloatDefaultValue() * si.multiplier);
996
else
997
widget->setValue(si.FloatDefaultValue());
998
}
999
}
1000
break;
1001
1002
case SettingInfo::Type::String:
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
case SettingInfo::Type::Path:
1011
{
1012
QLineEdit* widget = parent_widget->findChild<QLineEdit*>(QString::fromStdString(si.name));
1013
if (widget)
1014
widget->setText(QString::fromUtf8(si.StringDefaultValue()));
1015
}
1016
break;
1017
}
1018
}
1019
}
1020
1021
ControllerCustomSettingsWidget::ControllerCustomSettingsWidget(ControllerBindingWidget* parent)
1022
: QWidget(parent), m_parent(parent)
1023
{
1024
const Controller::ControllerInfo* cinfo = parent->getControllerInfo();
1025
DebugAssert(cinfo);
1026
if (cinfo->settings.empty())
1027
return;
1028
1029
QScrollArea* sarea = new QScrollArea(this);
1030
QWidget* swidget = new QWidget(sarea);
1031
sarea->setWidget(swidget);
1032
sarea->setWidgetResizable(true);
1033
sarea->setFrameShape(QFrame::StyledPanel);
1034
sarea->setFrameShadow(QFrame::Sunken);
1035
1036
QGridLayout* swidget_layout = new QGridLayout(swidget);
1037
createSettingWidgets(parent->getDialog()->getEditingSettingsInterface(), swidget, swidget_layout,
1038
parent->getConfigSection(), cinfo->settings, cinfo->name);
1039
1040
int current_row = swidget_layout->rowCount();
1041
1042
QHBoxLayout* bottom_hlayout = new QHBoxLayout();
1043
QPushButton* restore_defaults = new QPushButton(tr("Restore Default Settings"), swidget);
1044
restore_defaults->setIcon(QIcon::fromTheme(QStringLiteral("restart-line")));
1045
bottom_hlayout->addStretch(1);
1046
bottom_hlayout->addWidget(restore_defaults);
1047
swidget_layout->addLayout(bottom_hlayout, current_row++, 0, 1, 4);
1048
connect(restore_defaults, &QPushButton::clicked, this, &ControllerCustomSettingsWidget::restoreDefaults);
1049
1050
swidget_layout->addItem(new QSpacerItem(1, 1, QSizePolicy::Minimum, QSizePolicy::Expanding), current_row++, 0, 1, 4);
1051
1052
QVBoxLayout* layout = new QVBoxLayout(this);
1053
layout->setContentsMargins(0, 0, 0, 0);
1054
layout->addWidget(sarea);
1055
}
1056
1057
ControllerCustomSettingsWidget::~ControllerCustomSettingsWidget() = default;
1058
1059
void ControllerCustomSettingsWidget::restoreDefaults()
1060
{
1061
const Controller::ControllerInfo* cinfo = m_parent->getControllerInfo();
1062
DebugAssert(cinfo);
1063
1064
restoreDefaultSettingWidgets(this, cinfo->settings);
1065
}
1066
1067
ControllerCustomSettingsDialog::ControllerCustomSettingsDialog(QWidget* parent, SettingsInterface* sif,
1068
const std::string& section,
1069
std::span<const SettingInfo> settings,
1070
const char* tr_context, const QString& window_title)
1071
: QDialog(parent)
1072
{
1073
setMinimumWidth(500);
1074
resize(minimumWidth(), 100);
1075
setWindowTitle(window_title);
1076
1077
QGridLayout* layout = new QGridLayout(this);
1078
createSettingWidgets(sif, this, layout, section, settings, tr_context);
1079
1080
QDialogButtonBox* bbox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::RestoreDefaults, this);
1081
connect(bbox, &QDialogButtonBox::accepted, this, &ControllerCustomSettingsDialog::accept);
1082
connect(bbox->button(QDialogButtonBox::RestoreDefaults), &QPushButton::clicked, this,
1083
[this, settings]() { restoreDefaultSettingWidgets(this, settings); });
1084
layout->addWidget(bbox, layout->rowCount(), 0, 1, 4);
1085
}
1086
1087
ControllerCustomSettingsDialog::~ControllerCustomSettingsDialog() = default;
1088
1089