@@ -17,6 +17,7 @@ with this program. If not, see <https://www.gnu.org/licenses/>
1717*/
1818
1919#include " joypad-ui.h"
20+ #include " joypad-actions.h"
2021
2122#include < obs-frontend-api.h>
2223#include < obs-module.h>
@@ -54,14 +55,24 @@ with this program. If not, see <https://www.gnu.org/licenses/>
5455#include < QSpinBox>
5556
5657#include < algorithm>
58+ #include < atomic>
5759#include < cmath>
60+ #include < mutex>
5861#include < vector>
5962
6063namespace {
6164constexpr int kDeviceIdRole = Qt::UserRole;
6265constexpr int kDeviceStableIdRole = Qt::UserRole + 1 ;
6366constexpr int kDeviceTypeIdRole = Qt::UserRole + 2 ;
6467
68+ std::atomic<int > g_binding_dialog_open_count{0 };
69+ struct DialogTestState {
70+ bool enabled = false ;
71+ JoypadBinding binding;
72+ };
73+ std::mutex g_dialog_test_mutex;
74+ DialogTestState g_dialog_test_state;
75+
6576inline QString L (const char *key)
6677{
6778 return QString::fromUtf8 (obs_module_text (key));
@@ -339,6 +350,26 @@ QString input_label_from_binding(const JoypadBinding &binding)
339350 return L (" JoypadToOBS.Common.ButtonNumber" ).arg (binding.button );
340351}
341352
353+ double map_axis_raw_to_percent_for_test (const JoypadBinding &binding, double raw)
354+ {
355+ double minv = binding.axis_min_value ;
356+ double maxv = binding.axis_max_value ;
357+ if (maxv <= minv) {
358+ minv = 0.0 ;
359+ maxv = 1024.0 ;
360+ }
361+ double percent = ((raw - minv) / (maxv - minv)) * 100.0 ;
362+ if (binding.axis_inverted ) {
363+ percent = 100.0 - percent;
364+ }
365+ percent = std::clamp (percent, 0.0 , 100.0 );
366+ double base = std::clamp (percent / 100.0 , 0.0 , 1.0 );
367+ double gamma = binding.slider_gamma > 0.0 ? binding.slider_gamma : 0.6 ;
368+ gamma = std::clamp (gamma, 0.1 , 50.0 );
369+ double curved = std::pow (base, gamma);
370+ return std::clamp (curved * 100.0 , 0.0 , 100.0 );
371+ }
372+
342373class JoypadBindingDialog : public QDialog {
343374public:
344375 JoypadBindingDialog (QWidget *parent, JoypadConfigStore *config, JoypadInputManager *input,
@@ -348,6 +379,7 @@ class JoypadBindingDialog : public QDialog {
348379 input_(input),
349380 existing_(existing)
350381 {
382+ g_binding_dialog_open_count.fetch_add (1 , std::memory_order_relaxed);
351383 setWindowTitle (L (" JoypadToOBS.Dialog.AddTitle" ));
352384 setModal (true );
353385
@@ -476,6 +508,7 @@ class JoypadBindingDialog : public QDialog {
476508 volume_spin_ = new QDoubleSpinBox (action_group);
477509 volume_allow_above_unity_ = new QCheckBox (L (" JoypadToOBS.Field.AllowAboveDb" ), action_group);
478510 invert_axis_checkbox_ = new QCheckBox (L (" JoypadToOBS.Field.InvertAxis" ), action_group);
511+ test_mode_checkbox_ = new QCheckBox (L (" JoypadToOBS.Field.TestMode" ), action_group);
479512 volume_spin_->setRange (-60.0 , 20.0 );
480513 volume_spin_->setSingleStep (1.0 );
481514 volume_spin_->setValue (0.0 );
@@ -489,6 +522,7 @@ class JoypadBindingDialog : public QDialog {
489522 action_layout->addWidget (volume_spin_, 2 , 1 );
490523 action_layout->addWidget (volume_allow_above_unity_, 3 , 0 , 1 , 2 );
491524 action_layout->addWidget (invert_axis_checkbox_, 4 , 0 , 1 , 2 );
525+ action_layout->addWidget (test_mode_checkbox_, 5 , 0 , 1 , 2 );
492526
493527 layout->addWidget (action_group);
494528 layout->addWidget (target_group);
@@ -498,8 +532,19 @@ class JoypadBindingDialog : public QDialog {
498532
499533 connect (buttons, &QDialogButtonBox::accepted, this , &JoypadBindingDialog::accept);
500534 connect (buttons, &QDialogButtonBox::rejected, this , &JoypadBindingDialog::reject);
535+ connect (test_mode_checkbox_, &QCheckBox::toggled, this , [this ](bool ) { PublishTestBinding (); });
501536 connect (listen_button_, &QPushButton::clicked, this , &JoypadBindingDialog::OnListen);
502537 connect (action_combo_, &QComboBox::currentIndexChanged, this , &JoypadBindingDialog::UpdateActionUi);
538+ connect (action_combo_, &QComboBox::currentIndexChanged, this , [this ](int ) { PublishTestBinding (); });
539+ connect (device_combo_, &QComboBox::currentIndexChanged, this , [this ](int ) { PublishTestBinding (); });
540+ connect (scene_combo_, &QComboBox::currentIndexChanged, this , [this ](int ) { PublishTestBinding (); });
541+ connect (source_combo_, &QComboBox::currentIndexChanged, this , [this ](int ) { PublishTestBinding (); });
542+ connect (filter_combo_, &QComboBox::currentIndexChanged, this , [this ](int ) { PublishTestBinding (); });
543+ connect (bool_checkbox_, &QCheckBox::toggled, this , [this ](bool ) { PublishTestBinding (); });
544+ connect (use_current_scene_, &QCheckBox::toggled, this , [this ](bool ) { PublishTestBinding (); });
545+ connect (axis_both_checkbox_, &QCheckBox::toggled, this , [this ](bool ) { PublishTestBinding (); });
546+ connect (axis_threshold_combo_, &QComboBox::currentIndexChanged, this ,
547+ [this ](int ) { PublishTestBinding (); });
503548 connect (volume_spin_, QOverload<double >::of (&QDoubleSpinBox::valueChanged), this , [this ](double value) {
504549 if (CurrentAction () != JoypadActionType::SetSourceVolumePercent) {
505550 return ;
@@ -513,9 +558,11 @@ class JoypadBindingDialog : public QDialog {
513558 axis_value_slider_->setValue ((int )percent);
514559 axis_live_value_label_->setText (L (" JoypadToOBS.Common.PercentValue" ).arg (percent, 0 , ' f' , 0 ) +
515560 " " + L (" JoypadToOBS.Common.DbValue" ).arg (db, 0 , ' f' , 1 ));
561+ PublishTestBinding ();
516562 });
517563 connect (invert_axis_checkbox_, &QCheckBox::toggled, this , [this ](bool ) {
518564 if (CurrentAction () != JoypadActionType::SetSourceVolumePercent) {
565+ PublishTestBinding ();
519566 return ;
520567 }
521568 if (!learned_event_.is_axis ) {
@@ -526,19 +573,24 @@ class JoypadBindingDialog : public QDialog {
526573 axis_value_slider_->setValue ((int )percent);
527574 axis_live_value_label_->setText (L (" JoypadToOBS.Common.PercentValue" ).arg (percent, 0 , ' f' , 0 ) +
528575 " " + L (" JoypadToOBS.Common.DbValue" ).arg (db, 0 , ' f' , 1 ));
576+ PublishTestBinding ();
577+ });
578+ connect (volume_allow_above_unity_, &QCheckBox::toggled, this , [this ](bool checked) {
579+ binding_.allow_above_unity = checked;
580+ PublishTestBinding ();
529581 });
530- connect (volume_allow_above_unity_, &QCheckBox::toggled, this ,
531- [this ](bool checked) { binding_.allow_above_unity = checked; });
532582 connect (source_combo_, &QComboBox::currentIndexChanged, this , &JoypadBindingDialog::ReloadFilters);
533583 connect (axis_set_min_button_, &QPushButton::clicked, this , [this ]() {
534584 binding_.axis_min_value = last_axis_value_;
535585 axis_min_label_->setText (L (" JoypadToOBS.Field.AxisMinValue" ) + " : " +
536586 QString::number (binding_.axis_min_value , ' f' , 2 ));
587+ PublishTestBinding ();
537588 });
538589 connect (axis_set_max_button_, &QPushButton::clicked, this , [this ]() {
539590 binding_.axis_max_value = last_axis_value_;
540591 axis_max_label_->setText (L (" JoypadToOBS.Field.AxisMaxValue" ) + " : " +
541592 QString::number (binding_.axis_max_value , ' f' , 2 ));
593+ PublishTestBinding ();
542594 });
543595 if (input_) {
544596 axis_handler_id_ = input_->AddOnAxisChanged ([this ](const JoypadEvent &event) {
@@ -604,13 +656,18 @@ class JoypadBindingDialog : public QDialog {
604656 UpdateActionUi ();
605657 UpdateAxisUi (false );
606658 }
659+ PublishTestBinding ();
607660 refresh_timer_ = new QTimer (this );
608661 connect (refresh_timer_, &QTimer::timeout, this , &JoypadBindingDialog::RefreshDeviceList);
609662 refresh_timer_->start (1000 );
610663 }
611664
612665 ~JoypadBindingDialog () override
613666 {
667+ {
668+ std::lock_guard<std::mutex> lock (g_dialog_test_mutex);
669+ g_dialog_test_state.enabled = false ;
670+ }
614671 if (refresh_timer_) {
615672 refresh_timer_->stop ();
616673 refresh_timer_ = nullptr ;
@@ -622,20 +679,43 @@ class JoypadBindingDialog : public QDialog {
622679 axis_handler_id_ = 0 ;
623680 }
624681 }
682+ g_binding_dialog_open_count.fetch_sub (1 , std::memory_order_relaxed);
625683 }
626684
627685 JoypadBinding Binding () const { return binding_; }
628686
629687protected:
630688 void accept () override
631689 {
632- if (!ReadBinding ()) {
690+ if (!ReadBinding (true )) {
633691 return ;
634692 }
635693 QDialog::accept ();
636694 }
637695
638696private:
697+ void PublishTestBinding ()
698+ {
699+ if (!test_mode_checkbox_ || !test_mode_checkbox_->isChecked ()) {
700+ std::lock_guard<std::mutex> lock (g_dialog_test_mutex);
701+ g_dialog_test_state.enabled = false ;
702+ return ;
703+ }
704+ if (!ReadBinding (false )) {
705+ std::lock_guard<std::mutex> lock (g_dialog_test_mutex);
706+ g_dialog_test_state.enabled = false ;
707+ return ;
708+ }
709+ JoypadBinding test = binding_;
710+ if (test.action == JoypadActionType::SetSourceVolumePercent) {
711+ double percent = learned_event_.is_axis ? MapRawToPercent (last_axis_value_) : 50.0 ;
712+ test.volume_value = std::clamp (percent, 0.0 , 100.0 );
713+ }
714+ std::lock_guard<std::mutex> lock (g_dialog_test_mutex);
715+ g_dialog_test_state.enabled = true ;
716+ g_dialog_test_state.binding = test;
717+ }
718+
639719 void RefreshDeviceList ()
640720 {
641721 if (!input_) {
@@ -1058,6 +1138,7 @@ class JoypadBindingDialog : public QDialog {
10581138 }
10591139 }
10601140 SelectDevice (event);
1141+ PublishTestBinding ();
10611142 },
10621143 Qt::QueuedConnection);
10631144 });
@@ -1085,9 +1166,9 @@ class JoypadBindingDialog : public QDialog {
10851166 }
10861167 }
10871168
1088- bool ReadBinding ()
1169+ bool ReadBinding (bool require_input )
10891170 {
1090- if (!learned_event_.is_axis && learned_event_.button <= 0 ) {
1171+ if (require_input && !learned_event_.is_axis && learned_event_.button <= 0 ) {
10911172 button_label_->setText (L (" JoypadToOBS.Common.PressButtonOrAxisFirst" ));
10921173 return false ;
10931174 }
@@ -1098,7 +1179,8 @@ class JoypadBindingDialog : public QDialog {
10981179 binding_.device_type_id = device_combo_->currentData (kDeviceTypeIdRole ).toString ().toStdString ();
10991180 binding_.device_name = device_combo_->currentText ().toStdString ();
11001181
1101- if (CurrentAction () == JoypadActionType::SetSourceVolumePercent && !learned_event_.is_axis ) {
1182+ if (require_input && CurrentAction () == JoypadActionType::SetSourceVolumePercent &&
1183+ !learned_event_.is_axis ) {
11021184 button_label_->setText (L (" JoypadToOBS.Common.AxisOnlyForSlider" ));
11031185 return false ;
11041186 }
@@ -1242,13 +1324,80 @@ class JoypadBindingDialog : public QDialog {
12421324 QDoubleSpinBox *volume_spin_ = nullptr ;
12431325 QCheckBox *volume_allow_above_unity_ = nullptr ;
12441326 QCheckBox *invert_axis_checkbox_ = nullptr ;
1327+ QCheckBox *test_mode_checkbox_ = nullptr ;
12451328 int axis_handler_id_ = 0 ;
12461329 QTimer *refresh_timer_ = nullptr ;
12471330 bool is_listening_ = false ;
12481331};
12491332
12501333} // namespace
12511334
1335+ bool JoypadUiIsBindingDialogOpen ()
1336+ {
1337+ return g_binding_dialog_open_count.load (std::memory_order_relaxed) > 0 ;
1338+ }
1339+
1340+ bool JoypadUiEmulateBindingDialogAction (const JoypadEvent &event, JoypadActionEngine *actions)
1341+ {
1342+ DialogTestState state;
1343+ {
1344+ std::lock_guard<std::mutex> lock (g_dialog_test_mutex);
1345+ if (!g_dialog_test_state.enabled ) {
1346+ return false ;
1347+ }
1348+ state = g_dialog_test_state;
1349+ }
1350+
1351+ if (!actions) {
1352+ return true ;
1353+ }
1354+
1355+ const JoypadBinding &binding = state.binding ;
1356+ JoypadBinding adjusted = binding;
1357+
1358+ if (binding.input_type == JoypadInputType::Button) {
1359+ if (event.is_axis ) {
1360+ return true ;
1361+ }
1362+ if (binding.button > 0 && event.button != binding.button ) {
1363+ return true ;
1364+ }
1365+ actions->Execute (adjusted);
1366+ return true ;
1367+ }
1368+
1369+ if (!event.is_axis ) {
1370+ return true ;
1371+ }
1372+ if (binding.axis_index >= 0 && event.axis_index != binding.axis_index ) {
1373+ return true ;
1374+ }
1375+
1376+ double value = binding.axis_inverted ? -event.axis_value : event.axis_value ;
1377+ double abs_value = std::fabs (value);
1378+ if (binding.action == JoypadActionType::SetSourceVolumePercent) {
1379+ adjusted.volume_value = map_axis_raw_to_percent_for_test (binding, event.axis_raw_value );
1380+ actions->Execute (adjusted);
1381+ return true ;
1382+ }
1383+ if (binding.axis_direction != JoypadAxisDirection::Both) {
1384+ int dir = value >= 0.0 ? 1 : -1 ;
1385+ if (dir != (int )binding.axis_direction ) {
1386+ return true ;
1387+ }
1388+ }
1389+ if (abs_value < binding.axis_threshold ) {
1390+ return true ;
1391+ }
1392+ if (binding.action == JoypadActionType::AdjustSourceVolume) {
1393+ double sign = value >= 0.0 ? 1.0 : -1.0 ;
1394+ double intensity = std::clamp (abs_value, 0.0 , 1.0 );
1395+ adjusted.volume_value = std::fabs (binding.volume_value ) * intensity * sign;
1396+ }
1397+ actions->Execute (adjusted);
1398+ return true ;
1399+ }
1400+
12521401JoypadToolsDialog::JoypadToolsDialog (QWidget *parent, JoypadConfigStore *config, JoypadInputManager *input)
12531402 : QDialog(parent),
12541403 config_(config),
0 commit comments