1 // Copyright 2017 Dolphin Emulator Project
2 // Licensed under GPLv2+
3 // Refer to the license.txt file included.
4 
5 #include "DolphinQt/Config/Mapping/MappingWindow.h"
6 
7 #include <QCheckBox>
8 #include <QComboBox>
9 #include <QDialogButtonBox>
10 #include <QGroupBox>
11 #include <QHBoxLayout>
12 #include <QPushButton>
13 #include <QTabWidget>
14 #include <QTimer>
15 #include <QVBoxLayout>
16 
17 #include "Core/Core.h"
18 
19 #include "Common/FileSearch.h"
20 #include "Common/FileUtil.h"
21 #include "Common/IniFile.h"
22 #include "Common/StringUtil.h"
23 
24 #include "DolphinQt/Config/Mapping/GCKeyboardEmu.h"
25 #include "DolphinQt/Config/Mapping/GCMicrophone.h"
26 #include "DolphinQt/Config/Mapping/GCPadEmu.h"
27 #include "DolphinQt/Config/Mapping/Hotkey3D.h"
28 #include "DolphinQt/Config/Mapping/HotkeyControllerProfile.h"
29 #include "DolphinQt/Config/Mapping/HotkeyDebugging.h"
30 #include "DolphinQt/Config/Mapping/HotkeyGeneral.h"
31 #include "DolphinQt/Config/Mapping/HotkeyGraphics.h"
32 #include "DolphinQt/Config/Mapping/HotkeyStates.h"
33 #include "DolphinQt/Config/Mapping/HotkeyStatesOther.h"
34 #include "DolphinQt/Config/Mapping/HotkeyTAS.h"
35 #include "DolphinQt/Config/Mapping/HotkeyWii.h"
36 #include "DolphinQt/Config/Mapping/WiimoteEmuExtension.h"
37 #include "DolphinQt/Config/Mapping/WiimoteEmuExtensionMotionInput.h"
38 #include "DolphinQt/Config/Mapping/WiimoteEmuExtensionMotionSimulation.h"
39 #include "DolphinQt/Config/Mapping/WiimoteEmuGeneral.h"
40 #include "DolphinQt/Config/Mapping/WiimoteEmuMotionControl.h"
41 #include "DolphinQt/Config/Mapping/WiimoteEmuMotionControlIMU.h"
42 #include "DolphinQt/QtUtils/ModalMessageBox.h"
43 #include "DolphinQt/QtUtils/WrapInScrollArea.h"
44 #include "DolphinQt/Settings.h"
45 
46 #include "InputCommon/ControllerEmu/ControllerEmu.h"
47 #include "InputCommon/ControllerInterface/ControllerInterface.h"
48 #include "InputCommon/ControllerInterface/Device.h"
49 #include "InputCommon/InputConfig.h"
50 
51 constexpr const char* PROFILES_DIR = "Profiles/";
52 
MappingWindow(QWidget * parent,Type type,int port_num)53 MappingWindow::MappingWindow(QWidget* parent, Type type, int port_num)
54     : QDialog(parent), m_port(port_num)
55 {
56   setWindowTitle(tr("Port %1").arg(port_num + 1));
57   setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint);
58 
59   CreateDevicesLayout();
60   CreateProfilesLayout();
61   CreateResetLayout();
62   CreateMainLayout();
63   ConnectWidgets();
64   SetMappingType(type);
65 
66   const auto timer = new QTimer(this);
67   connect(timer, &QTimer::timeout, this, [this] {
68     const auto lock = GetController()->GetStateLock();
69     emit Update();
70   });
71 
72   timer->start(1000 / INDICATOR_UPDATE_FREQ);
73 
74   const auto lock = GetController()->GetStateLock();
75   emit ConfigChanged();
76 }
77 
CreateDevicesLayout()78 void MappingWindow::CreateDevicesLayout()
79 {
80   m_devices_layout = new QHBoxLayout();
81   m_devices_box = new QGroupBox(tr("Device"));
82   m_devices_combo = new QComboBox();
83   m_devices_refresh = new QPushButton(tr("Refresh"));
84 
85   m_devices_combo->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Fixed);
86   m_devices_refresh->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
87 
88   m_devices_layout->addWidget(m_devices_combo);
89   m_devices_layout->addWidget(m_devices_refresh);
90 
91   m_devices_box->setLayout(m_devices_layout);
92 }
93 
CreateProfilesLayout()94 void MappingWindow::CreateProfilesLayout()
95 {
96   m_profiles_layout = new QHBoxLayout();
97   m_profiles_box = new QGroupBox(tr("Profile"));
98   m_profiles_combo = new QComboBox();
99   m_profiles_load = new QPushButton(tr("Load"));
100   m_profiles_save = new QPushButton(tr("Save"));
101   m_profiles_delete = new QPushButton(tr("Delete"));
102 
103   auto* button_layout = new QHBoxLayout();
104 
105   m_profiles_combo->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Fixed);
106   m_profiles_combo->setMinimumWidth(100);
107   m_profiles_combo->setEditable(true);
108 
109   m_profiles_layout->addWidget(m_profiles_combo);
110   button_layout->addWidget(m_profiles_load);
111   button_layout->addWidget(m_profiles_save);
112   button_layout->addWidget(m_profiles_delete);
113   m_profiles_layout->addLayout(button_layout);
114 
115   m_profiles_box->setLayout(m_profiles_layout);
116 }
117 
CreateResetLayout()118 void MappingWindow::CreateResetLayout()
119 {
120   m_reset_layout = new QHBoxLayout();
121   m_reset_box = new QGroupBox(tr("Reset"));
122   m_reset_clear = new QPushButton(tr("Clear"));
123   m_reset_default = new QPushButton(tr("Default"));
124 
125   m_reset_box->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
126 
127   m_reset_layout->addWidget(m_reset_default);
128   m_reset_layout->addWidget(m_reset_clear);
129 
130   m_reset_box->setLayout(m_reset_layout);
131 }
132 
CreateMainLayout()133 void MappingWindow::CreateMainLayout()
134 {
135   m_main_layout = new QVBoxLayout();
136   m_config_layout = new QHBoxLayout();
137   m_tab_widget = new QTabWidget();
138   m_button_box = new QDialogButtonBox(QDialogButtonBox::Close);
139 
140   m_config_layout->addWidget(m_devices_box);
141   m_config_layout->addWidget(m_reset_box);
142   m_config_layout->addWidget(m_profiles_box);
143 
144   m_main_layout->addLayout(m_config_layout);
145   m_main_layout->addWidget(m_tab_widget);
146   m_main_layout->addWidget(m_button_box);
147 
148   setLayout(m_main_layout);
149 }
150 
ConnectWidgets()151 void MappingWindow::ConnectWidgets()
152 {
153   connect(&Settings::Instance(), &Settings::DevicesChanged, this,
154           &MappingWindow::OnGlobalDevicesChanged);
155   connect(this, &MappingWindow::ConfigChanged, this, &MappingWindow::OnGlobalDevicesChanged);
156   connect(m_devices_combo, qOverload<int>(&QComboBox::currentIndexChanged), this,
157           &MappingWindow::OnSelectDevice);
158 
159   connect(m_devices_refresh, &QPushButton::clicked, this, &MappingWindow::RefreshDevices);
160 
161   connect(m_reset_clear, &QPushButton::clicked, this, &MappingWindow::OnClearFieldsPressed);
162   connect(m_reset_default, &QPushButton::clicked, this, &MappingWindow::OnDefaultFieldsPressed);
163   connect(m_profiles_save, &QPushButton::clicked, this, &MappingWindow::OnSaveProfilePressed);
164   connect(m_profiles_load, &QPushButton::clicked, this, &MappingWindow::OnLoadProfilePressed);
165   connect(m_profiles_delete, &QPushButton::clicked, this, &MappingWindow::OnDeleteProfilePressed);
166 
167   connect(m_profiles_combo, qOverload<int>(&QComboBox::currentIndexChanged), this,
168           &MappingWindow::OnSelectProfile);
169   connect(m_profiles_combo, &QComboBox::editTextChanged, this,
170           &MappingWindow::OnProfileTextChanged);
171 
172   // We currently use the "Close" button as an "Accept" button so we must save on reject.
173   connect(this, &QDialog::rejected, [this] { emit Save(); });
174   connect(m_button_box, &QDialogButtonBox::rejected, this, &QDialog::reject);
175 }
176 
UpdateProfileIndex()177 void MappingWindow::UpdateProfileIndex()
178 {
179   // Make sure currentIndex and currentData are accurate when the user manually types a name.
180 
181   const auto current_text = m_profiles_combo->currentText();
182   const int text_index = m_profiles_combo->findText(current_text);
183   m_profiles_combo->setCurrentIndex(text_index);
184 
185   if (text_index == -1)
186     m_profiles_combo->setCurrentText(current_text);
187 }
188 
UpdateProfileButtonState()189 void MappingWindow::UpdateProfileButtonState()
190 {
191   // Make sure save/delete buttons are disabled for built-in profiles
192 
193   bool builtin = false;
194   if (m_profiles_combo->findText(m_profiles_combo->currentText()) != -1)
195   {
196     const QString profile_path = m_profiles_combo->currentData().toString();
197     builtin = profile_path.startsWith(QString::fromStdString(File::GetSysDirectory()));
198   }
199 
200   m_profiles_save->setEnabled(!builtin);
201   m_profiles_delete->setEnabled(!builtin);
202 }
203 
OnSelectProfile(int)204 void MappingWindow::OnSelectProfile(int)
205 {
206   UpdateProfileButtonState();
207 }
208 
OnProfileTextChanged(const QString &)209 void MappingWindow::OnProfileTextChanged(const QString&)
210 {
211   UpdateProfileButtonState();
212 }
213 
OnDeleteProfilePressed()214 void MappingWindow::OnDeleteProfilePressed()
215 {
216   UpdateProfileIndex();
217 
218   const QString profile_name = m_profiles_combo->currentText();
219   const QString profile_path = m_profiles_combo->currentData().toString();
220 
221   if (m_profiles_combo->currentIndex() == -1 || !File::Exists(profile_path.toStdString()))
222   {
223     ModalMessageBox error(this);
224     error.setIcon(QMessageBox::Critical);
225     error.setWindowTitle(tr("Error"));
226     error.setText(tr("The profile '%1' does not exist").arg(profile_name));
227     error.exec();
228     return;
229   }
230 
231   ModalMessageBox confirm(this);
232 
233   confirm.setIcon(QMessageBox::Warning);
234   confirm.setWindowTitle(tr("Confirm"));
235   confirm.setText(tr("Are you sure that you want to delete '%1'?").arg(profile_name));
236   confirm.setInformativeText(tr("This cannot be undone!"));
237   confirm.setStandardButtons(QMessageBox::Yes | QMessageBox::Cancel);
238 
239   if (confirm.exec() != QMessageBox::Yes)
240   {
241     return;
242   }
243 
244   m_profiles_combo->removeItem(m_profiles_combo->currentIndex());
245   m_profiles_combo->setCurrentIndex(-1);
246 
247   File::Delete(profile_path.toStdString());
248 
249   ModalMessageBox result(this);
250   result.setIcon(QMessageBox::Information);
251   result.setWindowModality(Qt::WindowModal);
252   result.setWindowTitle(tr("Success"));
253   result.setText(tr("Successfully deleted '%1'.").arg(profile_name));
254 }
255 
OnLoadProfilePressed()256 void MappingWindow::OnLoadProfilePressed()
257 {
258   UpdateProfileIndex();
259 
260   if (m_profiles_combo->currentIndex() == -1)
261   {
262     ModalMessageBox error(this);
263     error.setIcon(QMessageBox::Critical);
264     error.setWindowTitle(tr("Error"));
265     error.setText(tr("The profile '%1' does not exist").arg(m_profiles_combo->currentText()));
266     error.exec();
267     return;
268   }
269 
270   const QString profile_path = m_profiles_combo->currentData().toString();
271 
272   IniFile ini;
273   ini.Load(profile_path.toStdString());
274 
275   m_controller->LoadConfig(ini.GetOrCreateSection("Profile"));
276   m_controller->UpdateReferences(g_controller_interface);
277 
278   const auto lock = GetController()->GetStateLock();
279   emit ConfigChanged();
280 }
281 
OnSaveProfilePressed()282 void MappingWindow::OnSaveProfilePressed()
283 {
284   const QString profile_name = m_profiles_combo->currentText();
285 
286   if (profile_name.isEmpty())
287     return;
288 
289   const std::string profile_path = File::GetUserPath(D_CONFIG_IDX) + PROFILES_DIR +
290                                    m_config->GetProfileName() + "/" + profile_name.toStdString() +
291                                    ".ini";
292 
293   File::CreateFullPath(profile_path);
294 
295   IniFile ini;
296 
297   m_controller->SaveConfig(ini.GetOrCreateSection("Profile"));
298   ini.Save(profile_path);
299 
300   if (m_profiles_combo->findText(profile_name) == -1)
301   {
302     PopulateProfileSelection();
303     m_profiles_combo->setCurrentIndex(m_profiles_combo->findText(profile_name));
304   }
305 }
306 
OnSelectDevice(int)307 void MappingWindow::OnSelectDevice(int)
308 {
309   if (IsMappingAllDevices())
310     return;
311 
312   // Original string is stored in the "user-data".
313   const auto device = m_devices_combo->currentData().toString().toStdString();
314 
315   m_controller->SetDefaultDevice(device);
316   m_controller->UpdateReferences(g_controller_interface);
317 }
318 
IsMappingAllDevices() const319 bool MappingWindow::IsMappingAllDevices() const
320 {
321   return m_devices_combo->currentIndex() == m_devices_combo->count() - 1;
322 }
323 
RefreshDevices()324 void MappingWindow::RefreshDevices()
325 {
326   Core::RunAsCPUThread([&] { g_controller_interface.RefreshDevices(); });
327 }
328 
OnGlobalDevicesChanged()329 void MappingWindow::OnGlobalDevicesChanged()
330 {
331   const QSignalBlocker blocker(m_devices_combo);
332 
333   m_devices_combo->clear();
334 
335   for (const auto& name : g_controller_interface.GetAllDeviceStrings())
336   {
337     const auto qname = QString::fromStdString(name);
338     m_devices_combo->addItem(qname, qname);
339   }
340 
341   m_devices_combo->insertSeparator(m_devices_combo->count());
342 
343   const auto default_device = m_controller->GetDefaultDevice().ToString();
344 
345   if (!default_device.empty())
346   {
347     const auto default_device_index =
348         m_devices_combo->findText(QString::fromStdString(default_device));
349 
350     if (default_device_index != -1)
351     {
352       m_devices_combo->setCurrentIndex(default_device_index);
353     }
354     else
355     {
356       // Selected device is not currently attached.
357       const auto qname = QString::fromStdString(default_device);
358       m_devices_combo->addItem(QLatin1Char{'['} + tr("disconnected") + QStringLiteral("] ") + qname,
359                                qname);
360       m_devices_combo->setCurrentIndex(m_devices_combo->count() - 1);
361     }
362   }
363 
364   m_devices_combo->addItem(tr("All devices"));
365 }
366 
SetMappingType(MappingWindow::Type type)367 void MappingWindow::SetMappingType(MappingWindow::Type type)
368 {
369   MappingWidget* widget;
370 
371   switch (type)
372   {
373   case Type::MAPPING_GC_KEYBOARD:
374     widget = new GCKeyboardEmu(this);
375     AddWidget(tr("GameCube Keyboard"), widget);
376     setWindowTitle(tr("GameCube Keyboard at Port %1").arg(GetPort() + 1));
377     break;
378   case Type::MAPPING_GC_BONGOS:
379   case Type::MAPPING_GC_STEERINGWHEEL:
380   case Type::MAPPING_GC_DANCEMAT:
381   case Type::MAPPING_GCPAD:
382     widget = new GCPadEmu(this);
383     setWindowTitle(tr("GameCube Controller at Port %1").arg(GetPort() + 1));
384     AddWidget(tr("GameCube Controller"), widget);
385     break;
386   case Type::MAPPING_GC_MICROPHONE:
387     widget = new GCMicrophone(this);
388     setWindowTitle(tr("GameCube Microphone Slot %1")
389                        .arg(GetPort() == 0 ? QLatin1Char{'A'} : QLatin1Char{'B'}));
390     AddWidget(tr("Microphone"), widget);
391     break;
392   case Type::MAPPING_WIIMOTE_EMU:
393   {
394     auto* extension = new WiimoteEmuExtension(this);
395     auto* extension_motion_input = new WiimoteEmuExtensionMotionInput(this);
396     auto* extension_motion_simulation = new WiimoteEmuExtensionMotionSimulation(this);
397     widget = new WiimoteEmuGeneral(this, extension);
398     setWindowTitle(tr("Wii Remote %1").arg(GetPort() + 1));
399     AddWidget(tr("General and Options"), widget);
400     AddWidget(tr("Motion Simulation"), new WiimoteEmuMotionControl(this));
401     AddWidget(tr("Motion Input"), new WiimoteEmuMotionControlIMU(this));
402     AddWidget(tr("Extension"), extension);
403     m_extension_motion_simulation_tab =
404         AddWidget(EXTENSION_MOTION_SIMULATION_TAB_NAME, extension_motion_simulation);
405     m_extension_motion_input_tab =
406         AddWidget(EXTENSION_MOTION_INPUT_TAB_NAME, extension_motion_input);
407     // Hide tabs by default. "Nunchuk" selection triggers an event to show them.
408     ShowExtensionMotionTabs(false);
409     break;
410   }
411   case Type::MAPPING_HOTKEYS:
412   {
413     widget = new HotkeyGeneral(this);
414     AddWidget(tr("General"), widget);
415     // i18n: TAS is short for tool-assisted speedrun. Read http://tasvideos.org/ for details.
416     // Frame advance is an example of a typical TAS tool.
417     AddWidget(tr("TAS Tools"), new HotkeyTAS(this));
418 
419     AddWidget(tr("Debugging"), new HotkeyDebugging(this));
420 
421     AddWidget(tr("Wii and Wii Remote"), new HotkeyWii(this));
422     AddWidget(tr("Controller Profile"), new HotkeyControllerProfile(this));
423     AddWidget(tr("Graphics"), new HotkeyGraphics(this));
424     // i18n: Stereoscopic 3D
425     AddWidget(tr("3D"), new Hotkey3D(this));
426     AddWidget(tr("Save and Load State"), new HotkeyStates(this));
427     AddWidget(tr("Other State Management"), new HotkeyStatesOther(this));
428     setWindowTitle(tr("Hotkey Settings"));
429     break;
430   }
431   default:
432     return;
433   }
434 
435   widget->LoadSettings();
436 
437   m_config = widget->GetConfig();
438 
439   m_controller = m_config->GetController(GetPort());
440 
441   PopulateProfileSelection();
442 }
443 
PopulateProfileSelection()444 void MappingWindow::PopulateProfileSelection()
445 {
446   m_profiles_combo->clear();
447 
448   const std::string profiles_path =
449       File::GetUserPath(D_CONFIG_IDX) + PROFILES_DIR + m_config->GetProfileName();
450   for (const auto& filename : Common::DoFileSearch({profiles_path}, {".ini"}))
451   {
452     std::string basename;
453     SplitPath(filename, nullptr, &basename, nullptr);
454     m_profiles_combo->addItem(QString::fromStdString(basename), QString::fromStdString(filename));
455   }
456 
457   m_profiles_combo->insertSeparator(m_profiles_combo->count());
458 
459   const std::string builtin_profiles_path =
460       File::GetSysDirectory() + PROFILES_DIR + m_config->GetProfileName();
461   for (const auto& filename : Common::DoFileSearch({builtin_profiles_path}, {".ini"}))
462   {
463     std::string basename;
464     SplitPath(filename, nullptr, &basename, nullptr);
465     // i18n: "Stock" refers to input profiles included with Dolphin
466     m_profiles_combo->addItem(tr("%1 (Stock)").arg(QString::fromStdString(basename)),
467                               QString::fromStdString(filename));
468   }
469 
470   m_profiles_combo->setCurrentIndex(-1);
471 }
472 
AddWidget(const QString & name,QWidget * widget)473 QWidget* MappingWindow::AddWidget(const QString& name, QWidget* widget)
474 {
475   QWidget* wrapper = GetWrappedWidget(widget, this, 150, 210);
476   m_tab_widget->addTab(wrapper, name);
477   return wrapper;
478 }
479 
GetPort() const480 int MappingWindow::GetPort() const
481 {
482   return m_port;
483 }
484 
GetController() const485 ControllerEmu::EmulatedController* MappingWindow::GetController() const
486 {
487   return m_controller;
488 }
489 
OnDefaultFieldsPressed()490 void MappingWindow::OnDefaultFieldsPressed()
491 {
492   m_controller->LoadDefaults(g_controller_interface);
493   m_controller->UpdateReferences(g_controller_interface);
494 
495   const auto lock = GetController()->GetStateLock();
496   emit ConfigChanged();
497   emit Save();
498 }
499 
OnClearFieldsPressed()500 void MappingWindow::OnClearFieldsPressed()
501 {
502   // Loading an empty inifile section clears everything.
503   IniFile::Section sec;
504 
505   // Keep the currently selected device.
506   const auto default_device = m_controller->GetDefaultDevice();
507   m_controller->LoadConfig(&sec);
508   m_controller->SetDefaultDevice(default_device);
509 
510   m_controller->UpdateReferences(g_controller_interface);
511 
512   const auto lock = GetController()->GetStateLock();
513   emit ConfigChanged();
514   emit Save();
515 }
516 
ShowExtensionMotionTabs(bool show)517 void MappingWindow::ShowExtensionMotionTabs(bool show)
518 {
519   if (show)
520   {
521     m_tab_widget->addTab(m_extension_motion_simulation_tab, EXTENSION_MOTION_SIMULATION_TAB_NAME);
522     m_tab_widget->addTab(m_extension_motion_input_tab, EXTENSION_MOTION_INPUT_TAB_NAME);
523   }
524   else
525   {
526     m_tab_widget->removeTab(5);
527     m_tab_widget->removeTab(4);
528   }
529 }
530