1 // Copyright 2017 Dolphin Emulator Project
2 // Licensed under GPLv2+
3 // Refer to the license.txt file included.
4
5 #include "DolphinQt/Config/Mapping/MappingCommon.h"
6
7 #include <tuple>
8 #include <vector>
9
10 #include <QApplication>
11 #include <QPushButton>
12 #include <QRegExp>
13 #include <QString>
14 #include <QTimer>
15
16 #include "DolphinQt/QtUtils/BlockUserInputFilter.h"
17 #include "InputCommon/ControlReference/ControlReference.h"
18
19 #include "Common/Thread.h"
20
21 namespace MappingCommon
22 {
23 constexpr auto INPUT_DETECT_INITIAL_TIME = std::chrono::seconds(3);
24 constexpr auto INPUT_DETECT_CONFIRMATION_TIME = std::chrono::milliseconds(500);
25 constexpr auto INPUT_DETECT_MAXIMUM_TIME = std::chrono::seconds(5);
26
27 constexpr auto OUTPUT_TEST_TIME = std::chrono::seconds(2);
28
29 // Pressing inputs at the same time will result in the & operator vs a hotkey expression.
30 constexpr auto HOTKEY_VS_CONJUNCION_THRESHOLD = std::chrono::milliseconds(50);
31
32 // Some devices (e.g. DS4) provide an analog and digital input for the trigger.
33 // We prefer just the analog input for simultaneous digital+analog input detections.
34 constexpr auto SPURIOUS_TRIGGER_COMBO_THRESHOLD = std::chrono::milliseconds(150);
35
GetExpressionForControl(const QString & control_name,const ciface::Core::DeviceQualifier & control_device,const ciface::Core::DeviceQualifier & default_device,Quote quote)36 QString GetExpressionForControl(const QString& control_name,
37 const ciface::Core::DeviceQualifier& control_device,
38 const ciface::Core::DeviceQualifier& default_device, Quote quote)
39 {
40 QString expr;
41
42 // non-default device
43 if (control_device != default_device)
44 {
45 expr += QString::fromStdString(control_device.ToString());
46 expr += QLatin1Char{':'};
47 }
48
49 // append the control name
50 expr += control_name;
51
52 if (quote == Quote::On)
53 {
54 QRegExp reg(QStringLiteral("[a-zA-Z]+"));
55 if (!reg.exactMatch(expr))
56 expr = QStringLiteral("`%1`").arg(expr);
57 }
58
59 return expr;
60 }
61
DetectExpression(QPushButton * button,ciface::Core::DeviceContainer & device_container,const std::vector<std::string> & device_strings,const ciface::Core::DeviceQualifier & default_device,Quote quote)62 QString DetectExpression(QPushButton* button, ciface::Core::DeviceContainer& device_container,
63 const std::vector<std::string>& device_strings,
64 const ciface::Core::DeviceQualifier& default_device, Quote quote)
65 {
66 const auto filter = new BlockUserInputFilter(button);
67
68 button->installEventFilter(filter);
69 button->grabKeyboard();
70 button->grabMouse();
71
72 const auto old_text = button->text();
73 button->setText(QStringLiteral("..."));
74
75 // The button text won't be updated if we don't process events here
76 QApplication::processEvents();
77
78 // Avoid that the button press itself is registered as an event
79 Common::SleepCurrentThread(50);
80
81 auto detections =
82 device_container.DetectInput(device_strings, INPUT_DETECT_INITIAL_TIME,
83 INPUT_DETECT_CONFIRMATION_TIME, INPUT_DETECT_MAXIMUM_TIME);
84
85 RemoveSpuriousTriggerCombinations(&detections);
86
87 const auto timer = new QTimer(button);
88
89 button->connect(timer, &QTimer::timeout, [button, filter] {
90 button->releaseMouse();
91 button->releaseKeyboard();
92 button->removeEventFilter(filter);
93 });
94
95 // Prevent mappings of "space", "return", or mouse clicks from re-activating detection.
96 timer->start(500);
97
98 button->setText(old_text);
99
100 return BuildExpression(detections, default_device, quote);
101 }
102
TestOutput(QPushButton * button,OutputReference * reference)103 void TestOutput(QPushButton* button, OutputReference* reference)
104 {
105 const auto old_text = button->text();
106 button->setText(QStringLiteral("..."));
107
108 // The button text won't be updated if we don't process events here
109 QApplication::processEvents();
110
111 reference->State(1.0);
112 std::this_thread::sleep_for(OUTPUT_TEST_TIME);
113 reference->State(0.0);
114
115 button->setText(old_text);
116 }
117
RemoveSpuriousTriggerCombinations(std::vector<ciface::Core::DeviceContainer::InputDetection> * detections)118 void RemoveSpuriousTriggerCombinations(
119 std::vector<ciface::Core::DeviceContainer::InputDetection>* detections)
120 {
121 const auto is_spurious = [&](auto& detection) {
122 return std::any_of(detections->begin(), detections->end(), [&](auto& d) {
123 // This is a suprious digital detection if a "smooth" (analog) detection is temporally near.
124 return &d != &detection && d.smoothness > 1 &&
125 abs(d.press_time - detection.press_time) < SPURIOUS_TRIGGER_COMBO_THRESHOLD;
126 });
127 };
128
129 detections->erase(std::remove_if(detections->begin(), detections->end(), is_spurious),
130 detections->end());
131 }
132
133 QString
BuildExpression(const std::vector<ciface::Core::DeviceContainer::InputDetection> & detections,const ciface::Core::DeviceQualifier & default_device,Quote quote)134 BuildExpression(const std::vector<ciface::Core::DeviceContainer::InputDetection>& detections,
135 const ciface::Core::DeviceQualifier& default_device, Quote quote)
136 {
137 std::vector<const ciface::Core::DeviceContainer::InputDetection*> pressed_inputs;
138
139 QStringList alternations;
140
141 const auto get_control_expression = [&](auto& detection) {
142 // Return the parent-most name if there is one for better hotkey strings.
143 // Detection of L/R_Ctrl will be changed to just Ctrl.
144 // Users can manually map L_Ctrl if they so desire.
145 const auto input = (quote == Quote::On) ?
146 detection.device->GetParentMostInput(detection.input) :
147 detection.input;
148
149 ciface::Core::DeviceQualifier device_qualifier;
150 device_qualifier.FromDevice(detection.device.get());
151
152 return MappingCommon::GetExpressionForControl(QString::fromStdString(input->GetName()),
153 device_qualifier, default_device, quote);
154 };
155
156 bool new_alternation = false;
157
158 const auto handle_press = [&](auto& detection) {
159 pressed_inputs.emplace_back(&detection);
160 new_alternation = true;
161 };
162
163 const auto handle_release = [&]() {
164 if (!new_alternation)
165 return;
166
167 new_alternation = false;
168
169 QStringList alternation;
170 for (auto* input : pressed_inputs)
171 alternation.push_back(get_control_expression(*input));
172
173 const bool is_hotkey = pressed_inputs.size() >= 2 &&
174 (pressed_inputs[1]->press_time - pressed_inputs[0]->press_time) >
175 HOTKEY_VS_CONJUNCION_THRESHOLD;
176
177 if (is_hotkey)
178 {
179 alternations.push_back(QStringLiteral("@(%1)").arg(alternation.join(QLatin1Char('+'))));
180 }
181 else
182 {
183 alternation.sort();
184 alternations.push_back(alternation.join(QLatin1Char('&')));
185 }
186 };
187
188 for (auto& detection : detections)
189 {
190 // Remove since released inputs.
191 for (auto it = pressed_inputs.begin(); it != pressed_inputs.end();)
192 {
193 if (!((*it)->release_time > detection.press_time))
194 {
195 handle_release();
196 it = pressed_inputs.erase(it);
197 }
198 else
199 ++it;
200 }
201
202 handle_press(detection);
203 }
204
205 handle_release();
206
207 alternations.removeDuplicates();
208 return alternations.join(QLatin1Char('|'));
209 }
210
211 } // namespace MappingCommon
212