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