duckstation

duckstation, but archived from the revision just before upstream changed it to a proprietary software project, this version is the libre one
git clone https://git.neptards.moe/u3shit/duckstation.git
Log | Files | Refs | README | LICENSE

inputbindingwidgets.cpp (15079B)


      1 // SPDX-FileCopyrightText: 2019-2022 Connor McLaughlin <stenzek@gmail.com>
      2 // SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0)
      3 
      4 #include "inputbindingwidgets.h"
      5 #include "controllersettingswindow.h"
      6 #include "inputbindingdialog.h"
      7 #include "qthost.h"
      8 #include "qtutils.h"
      9 
     10 #include "core/host.h"
     11 
     12 #include "common/bitutils.h"
     13 
     14 #include <QtCore/QTimer>
     15 #include <QtGui/QKeyEvent>
     16 #include <QtGui/QMouseEvent>
     17 #include <QtGui/QWheelEvent>
     18 #include <QtWidgets/QInputDialog>
     19 #include <QtWidgets/QMessageBox>
     20 #include <cmath>
     21 #include <sstream>
     22 
     23 InputBindingWidget::InputBindingWidget(QWidget* parent) : QPushButton(parent)
     24 {
     25   connect(this, &QPushButton::clicked, this, &InputBindingWidget::onClicked);
     26 }
     27 
     28 InputBindingWidget::InputBindingWidget(QWidget* parent, SettingsInterface* sif, InputBindingInfo::Type bind_type,
     29                                        std::string section_name, std::string key_name)
     30   : QPushButton(parent)
     31 {
     32   setMinimumWidth(225);
     33   setMaximumWidth(225);
     34 
     35   connect(this, &QPushButton::clicked, this, &InputBindingWidget::onClicked);
     36 
     37   initialize(sif, bind_type, std::move(section_name), std::move(key_name));
     38 }
     39 
     40 InputBindingWidget::~InputBindingWidget()
     41 {
     42   Q_ASSERT(!isListeningForInput());
     43 }
     44 
     45 bool InputBindingWidget::isMouseMappingEnabled(SettingsInterface* sif)
     46 {
     47   return (sif ? sif->GetBoolValue("UI", "EnableMouseMapping", false) :
     48                 Host::GetBaseBoolSettingValue("UI", "EnableMouseMapping", false)) &&
     49          !InputManager::IsUsingRawInput();
     50 }
     51 
     52 void InputBindingWidget::initialize(SettingsInterface* sif, InputBindingInfo::Type bind_type, std::string section_name,
     53                                     std::string key_name)
     54 {
     55   m_sif = sif;
     56   m_bind_type = bind_type;
     57   m_section_name = std::move(section_name);
     58   m_key_name = std::move(key_name);
     59   reloadBinding();
     60 }
     61 
     62 void InputBindingWidget::updateText()
     63 {
     64   if (m_bindings.empty())
     65   {
     66     setText(QString());
     67   }
     68   else if (m_bindings.size() > 1)
     69   {
     70     setText(tr("%n bindings", "", static_cast<int>(m_bindings.size())));
     71 
     72     // keep the full thing for the tooltip
     73     std::stringstream ss;
     74     bool first = true;
     75     for (const std::string& binding : m_bindings)
     76     {
     77       if (first)
     78         first = false;
     79       else
     80         ss << "\n";
     81       ss << binding;
     82     }
     83     setToolTip(QString::fromStdString(ss.str()));
     84   }
     85   else
     86   {
     87     QString binding_text(QString::fromStdString(m_bindings[0]));
     88     setToolTip(binding_text);
     89 
     90     // fix up accelerators, and if it's too long, ellipsise it
     91     if (binding_text.contains('&'))
     92       binding_text = binding_text.replace(QStringLiteral("&"), QStringLiteral("&&"));
     93     if (binding_text.length() > 35)
     94       binding_text = binding_text.left(35).append(QStringLiteral("..."));
     95     setText(binding_text);
     96   }
     97 }
     98 
     99 bool InputBindingWidget::eventFilter(QObject* watched, QEvent* event)
    100 {
    101   const QEvent::Type event_type = event->type();
    102 
    103   // if the key is being released, set the input
    104   if (event_type == QEvent::KeyRelease || (event_type == QEvent::MouseButtonRelease && m_mouse_mapping_enabled))
    105   {
    106     setNewBinding();
    107     stopListeningForInput();
    108     return true;
    109   }
    110   else if (event_type == QEvent::KeyPress)
    111   {
    112     const QKeyEvent* key_event = static_cast<const QKeyEvent*>(event);
    113     m_new_bindings.push_back(InputManager::MakeHostKeyboardKey(QtUtils::KeyEventToCode(key_event)));
    114     return true;
    115   }
    116   else if ((event_type == QEvent::MouseButtonPress || event_type == QEvent::MouseButtonDblClick) &&
    117            m_mouse_mapping_enabled)
    118   {
    119     // double clicks get triggered if we click bind, then click again quickly.
    120     const u32 button_index = CountTrailingZeros(static_cast<u32>(static_cast<const QMouseEvent*>(event)->button()));
    121     m_new_bindings.push_back(InputManager::MakePointerButtonKey(0, button_index));
    122     return true;
    123   }
    124   else if (event_type == QEvent::Wheel)
    125   {
    126     const QPoint delta_angle(static_cast<QWheelEvent*>(event)->angleDelta());
    127     const float dx = std::clamp(static_cast<float>(delta_angle.x()) / QtUtils::MOUSE_WHEEL_DELTA, -1.0f, 1.0f);
    128     if (dx != 0.0f)
    129     {
    130       InputBindingKey key(InputManager::MakePointerAxisKey(0, InputPointerAxis::WheelX));
    131       key.modifier = dx < 0.0f ? InputModifier::Negate : InputModifier::None;
    132       m_new_bindings.push_back(key);
    133     }
    134 
    135     const float dy = std::clamp(static_cast<float>(delta_angle.y()) / QtUtils::MOUSE_WHEEL_DELTA, -1.0f, 1.0f);
    136     if (dy != 0.0f)
    137     {
    138       InputBindingKey key(InputManager::MakePointerAxisKey(0, InputPointerAxis::WheelY));
    139       key.modifier = dy < 0.0f ? InputModifier::Negate : InputModifier::None;
    140       m_new_bindings.push_back(key);
    141     }
    142 
    143     if (dx != 0.0f || dy != 0.0f)
    144     {
    145       setNewBinding();
    146       stopListeningForInput();
    147     }
    148 
    149     return true;
    150   }
    151   else if (event_type == QEvent::MouseMove && m_mouse_mapping_enabled)
    152   {
    153     // if we've moved more than a decent distance from the center of the widget, bind it.
    154     // this is so we don't accidentally bind to the mouse if you bump it while reaching for your pad.
    155     static constexpr const s32 THRESHOLD = 50;
    156     const QPoint diff(static_cast<QMouseEvent*>(event)->globalPosition().toPoint() - m_input_listen_start_position);
    157     bool has_one = false;
    158 
    159     if (std::abs(diff.x()) >= THRESHOLD)
    160     {
    161       InputBindingKey key(InputManager::MakePointerAxisKey(0, InputPointerAxis::X));
    162       key.modifier = diff.x() < 0 ? InputModifier::Negate : InputModifier::None;
    163       m_new_bindings.push_back(key);
    164       has_one = true;
    165     }
    166     if (std::abs(diff.y()) >= THRESHOLD)
    167     {
    168       InputBindingKey key(InputManager::MakePointerAxisKey(0, InputPointerAxis::Y));
    169       key.modifier = diff.y() < 0 ? InputModifier::Negate : InputModifier::None;
    170       m_new_bindings.push_back(key);
    171       has_one = true;
    172     }
    173 
    174     if (has_one)
    175     {
    176       setNewBinding();
    177       stopListeningForInput();
    178       return true;
    179     }
    180   }
    181 
    182   return false;
    183 }
    184 
    185 bool InputBindingWidget::event(QEvent* event)
    186 {
    187   if (event->type() == QEvent::MouseButtonRelease)
    188   {
    189     QMouseEvent* mev = static_cast<QMouseEvent*>(event);
    190     if (mev->button() == Qt::LeftButton && mev->modifiers() & Qt::ShiftModifier)
    191     {
    192       openDialog();
    193       return false;
    194     }
    195   }
    196 
    197   return QPushButton::event(event);
    198 }
    199 
    200 void InputBindingWidget::mouseReleaseEvent(QMouseEvent* e)
    201 {
    202   if (e->button() == Qt::RightButton)
    203   {
    204     clearBinding();
    205     return;
    206   }
    207 
    208   QPushButton::mouseReleaseEvent(e);
    209 }
    210 
    211 void InputBindingWidget::setNewBinding()
    212 {
    213   if (m_new_bindings.empty())
    214     return;
    215 
    216   std::string new_binding(
    217     InputManager::ConvertInputBindingKeysToString(m_bind_type, m_new_bindings.data(), m_new_bindings.size()));
    218   if (!new_binding.empty())
    219   {
    220     if (m_sif)
    221     {
    222       m_sif->SetStringValue(m_section_name.c_str(), m_key_name.c_str(), new_binding.c_str());
    223       QtHost::SaveGameSettings(m_sif, false);
    224       g_emu_thread->reloadGameSettings();
    225     }
    226     else
    227     {
    228       Host::SetBaseStringSettingValue(m_section_name.c_str(), m_key_name.c_str(), new_binding.c_str());
    229       Host::CommitBaseSettingChanges();
    230       if (m_bind_type == InputBindingInfo::Type::Pointer)
    231         g_emu_thread->updateControllerSettings();
    232       g_emu_thread->reloadInputBindings();
    233     }
    234   }
    235 
    236   m_bindings.clear();
    237   m_bindings.push_back(std::move(new_binding));
    238 }
    239 
    240 void InputBindingWidget::clearBinding()
    241 {
    242   m_bindings.clear();
    243   if (m_sif)
    244   {
    245     m_sif->DeleteValue(m_section_name.c_str(), m_key_name.c_str());
    246     QtHost::SaveGameSettings(m_sif, false);
    247     g_emu_thread->reloadGameSettings();
    248   }
    249   else
    250   {
    251     Host::DeleteBaseSettingValue(m_section_name.c_str(), m_key_name.c_str());
    252     Host::CommitBaseSettingChanges();
    253     if (m_bind_type == InputBindingInfo::Type::Pointer)
    254       g_emu_thread->updateControllerSettings();
    255     g_emu_thread->reloadInputBindings();
    256   }
    257   reloadBinding();
    258 }
    259 
    260 void InputBindingWidget::reloadBinding()
    261 {
    262   m_bindings = m_sif ? m_sif->GetStringList(m_section_name.c_str(), m_key_name.c_str()) :
    263                        Host::GetBaseStringListSetting(m_section_name.c_str(), m_key_name.c_str());
    264   updateText();
    265 }
    266 
    267 void InputBindingWidget::onClicked()
    268 {
    269   if (m_bindings.size() > 1)
    270   {
    271     openDialog();
    272     return;
    273   }
    274 
    275   if (isListeningForInput())
    276     stopListeningForInput();
    277 
    278   startListeningForInput(TIMEOUT_FOR_SINGLE_BINDING);
    279 }
    280 
    281 void InputBindingWidget::onInputListenTimerTimeout()
    282 {
    283   m_input_listen_remaining_seconds--;
    284   if (m_input_listen_remaining_seconds == 0)
    285   {
    286     stopListeningForInput();
    287     return;
    288   }
    289 
    290   setText(tr("Push Button/Axis... [%1]").arg(m_input_listen_remaining_seconds));
    291 }
    292 
    293 void InputBindingWidget::startListeningForInput(u32 timeout_in_seconds)
    294 {
    295   m_value_ranges.clear();
    296   m_new_bindings.clear();
    297   m_mouse_mapping_enabled = isMouseMappingEnabled(m_sif);
    298   m_input_listen_start_position = QCursor::pos();
    299   m_input_listen_timer = new QTimer(this);
    300   m_input_listen_timer->setSingleShot(false);
    301   m_input_listen_timer->start(1000);
    302 
    303   m_input_listen_timer->connect(m_input_listen_timer, &QTimer::timeout, this,
    304                                 &InputBindingWidget::onInputListenTimerTimeout);
    305   m_input_listen_remaining_seconds = timeout_in_seconds;
    306   setText(tr("Push Button/Axis... [%1]").arg(m_input_listen_remaining_seconds));
    307 
    308   installEventFilter(this);
    309   grabKeyboard();
    310   grabMouse();
    311   setMouseTracking(true);
    312   hookInputManager();
    313 }
    314 
    315 void InputBindingWidget::stopListeningForInput()
    316 {
    317   reloadBinding();
    318   delete m_input_listen_timer;
    319   m_input_listen_timer = nullptr;
    320   std::vector<InputBindingKey>().swap(m_new_bindings);
    321 
    322   unhookInputManager();
    323   setMouseTracking(false);
    324   releaseMouse();
    325   releaseKeyboard();
    326   removeEventFilter(this);
    327 }
    328 
    329 void InputBindingWidget::inputManagerHookCallback(InputBindingKey key, float value)
    330 {
    331   if (!isListeningForInput())
    332     return;
    333 
    334   float initial_value = value;
    335   float min_value = value;
    336   auto it = std::find_if(m_value_ranges.begin(), m_value_ranges.end(),
    337                          [key](const auto& it) { return it.first.bits == key.bits; });
    338   if (it != m_value_ranges.end())
    339   {
    340     initial_value = it->second.first;
    341     min_value = it->second.second = std::min(it->second.second, value);
    342   }
    343   else
    344   {
    345     m_value_ranges.emplace_back(key, std::make_pair(initial_value, min_value));
    346   }
    347 
    348   const float abs_value = std::abs(value);
    349   const bool reverse_threshold = (key.source_subtype == InputSubclass::ControllerAxis && initial_value > 0.5f);
    350 
    351   for (InputBindingKey& other_key : m_new_bindings)
    352   {
    353     if (other_key.MaskDirection() == key.MaskDirection())
    354     {
    355       // for pedals, we wait for it to go back to near its starting point to commit the binding
    356       if ((reverse_threshold ? ((initial_value - value) <= 0.25f) : (abs_value < 0.5f)))
    357       {
    358         // did we go the full range?
    359         if (reverse_threshold && initial_value > 0.5f && min_value <= -0.5f)
    360           other_key.modifier = InputModifier::FullAxis;
    361 
    362         // if this key is in our new binding list, it's a "release", and we're done
    363         setNewBinding();
    364         stopListeningForInput();
    365         return;
    366       }
    367 
    368       // otherwise, keep waiting
    369       return;
    370     }
    371   }
    372 
    373   // new binding, add it to the list, but wait for a decent distance first, and then wait for release
    374   if ((reverse_threshold ? (abs_value < 0.5f) : (abs_value >= 0.5f)))
    375   {
    376     InputBindingKey key_to_add = key;
    377     key_to_add.modifier = (value < 0.0f && !reverse_threshold) ? InputModifier::Negate : InputModifier::None;
    378     key_to_add.invert = reverse_threshold;
    379     m_new_bindings.push_back(key_to_add);
    380   }
    381 }
    382 
    383 void InputBindingWidget::hookInputManager()
    384 {
    385   InputManager::SetHook([this](InputBindingKey key, float value) {
    386     QMetaObject::invokeMethod(this, "inputManagerHookCallback", Qt::QueuedConnection, Q_ARG(InputBindingKey, key),
    387                               Q_ARG(float, value));
    388     return InputInterceptHook::CallbackResult::StopProcessingEvent;
    389   });
    390 }
    391 
    392 void InputBindingWidget::unhookInputManager()
    393 {
    394   InputManager::RemoveHook();
    395 }
    396 
    397 void InputBindingWidget::openDialog()
    398 {
    399   InputBindingDialog binding_dialog(m_sif, m_bind_type, m_section_name, m_key_name, m_bindings,
    400                                     QtUtils::GetRootWidget(this));
    401   binding_dialog.exec();
    402   reloadBinding();
    403 }
    404 
    405 InputVibrationBindingWidget::InputVibrationBindingWidget(QWidget* parent)
    406 {
    407   connect(this, &QPushButton::clicked, this, &InputVibrationBindingWidget::onClicked);
    408 }
    409 
    410 InputVibrationBindingWidget::InputVibrationBindingWidget(QWidget* parent, ControllerSettingsWindow* dialog,
    411                                                          std::string section_name, std::string key_name)
    412 {
    413   setMinimumWidth(225);
    414   setMaximumWidth(225);
    415 
    416   connect(this, &QPushButton::clicked, this, &InputVibrationBindingWidget::onClicked);
    417 
    418   setKey(dialog, std::move(section_name), std::move(key_name));
    419 }
    420 
    421 InputVibrationBindingWidget::~InputVibrationBindingWidget()
    422 {
    423 }
    424 
    425 void InputVibrationBindingWidget::setKey(ControllerSettingsWindow* dialog, std::string section_name,
    426                                          std::string key_name)
    427 {
    428   m_dialog = dialog;
    429   m_section_name = std::move(section_name);
    430   m_key_name = std::move(key_name);
    431   m_binding = Host::GetBaseStringSettingValue(m_section_name.c_str(), m_key_name.c_str());
    432   setText(QString::fromStdString(m_binding));
    433 }
    434 
    435 void InputVibrationBindingWidget::clearBinding()
    436 {
    437   m_binding = {};
    438   Host::DeleteBaseSettingValue(m_section_name.c_str(), m_key_name.c_str());
    439   Host::CommitBaseSettingChanges();
    440   g_emu_thread->reloadInputBindings();
    441   setText(QString());
    442 }
    443 
    444 void InputVibrationBindingWidget::onClicked()
    445 {
    446   QInputDialog dialog(QtUtils::GetRootWidget(this));
    447 
    448   const QString full_key(
    449     QStringLiteral("%1/%2").arg(QString::fromStdString(m_section_name)).arg(QString::fromStdString(m_key_name)));
    450   const QString current(QString::fromStdString(m_binding));
    451   QStringList input_options(m_dialog->getVibrationMotors());
    452   if (!current.isEmpty() && input_options.indexOf(current) < 0)
    453   {
    454     input_options.append(current);
    455   }
    456   else if (input_options.isEmpty())
    457   {
    458     QMessageBox::critical(QtUtils::GetRootWidget(this), tr("Error"),
    459                           tr("No devices with vibration motors were detected."));
    460     return;
    461   }
    462 
    463   QInputDialog input_dialog(this);
    464   input_dialog.setWindowTitle(full_key);
    465   input_dialog.setLabelText(tr("Select vibration motor for %1.").arg(full_key));
    466   input_dialog.setInputMode(QInputDialog::TextInput);
    467   input_dialog.setOptions(QInputDialog::UseListViewForComboBoxItems);
    468   input_dialog.setComboBoxEditable(false);
    469   input_dialog.setComboBoxItems(std::move(input_options));
    470   input_dialog.setTextValue(current);
    471   if (input_dialog.exec() == 0)
    472     return;
    473 
    474   const QString new_value(input_dialog.textValue());
    475   m_binding = new_value.toStdString();
    476   Host::SetBaseStringSettingValue(m_section_name.c_str(), m_key_name.c_str(), m_binding.c_str());
    477   Host::CommitBaseSettingChanges();
    478   setText(new_value);
    479 }
    480 
    481 void InputVibrationBindingWidget::mouseReleaseEvent(QMouseEvent* e)
    482 {
    483   if (e->button() == Qt::RightButton)
    484   {
    485     clearBinding();
    486     return;
    487   }
    488 
    489   QPushButton::mouseReleaseEvent(e);
    490 }