inputbindingdialog.cpp (13282B)
1 // SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin <stenzek@gmail.com> 2 // SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0) 3 4 #include "inputbindingdialog.h" 5 #include "controllersettingwidgetbinder.h" 6 #include "inputbindingwidgets.h" 7 #include "qthost.h" 8 #include "qtutils.h" 9 10 #include "common/bitutils.h" 11 12 #include <QtCore/QTimer> 13 #include <QtGui/QKeyEvent> 14 #include <QtGui/QMouseEvent> 15 #include <QtGui/QWheelEvent> 16 17 InputBindingDialog::InputBindingDialog(SettingsInterface* sif, InputBindingInfo::Type bind_type, 18 std::string section_name, std::string key_name, 19 std::vector<std::string> bindings, QWidget* parent) 20 : QDialog(parent), m_sif(sif), m_bind_type(bind_type), m_section_name(std::move(section_name)), 21 m_key_name(std::move(key_name)), m_bindings(std::move(bindings)) 22 { 23 m_ui.setupUi(this); 24 m_ui.title->setText( 25 tr("Bindings for %1 %2").arg(QString::fromStdString(m_section_name)).arg(QString::fromStdString(m_key_name))); 26 m_ui.buttonBox->button(QDialogButtonBox::Close)->setText(tr("Close")); 27 28 connect(m_ui.addBinding, &QPushButton::clicked, this, &InputBindingDialog::onAddBindingButtonClicked); 29 connect(m_ui.removeBinding, &QPushButton::clicked, this, &InputBindingDialog::onRemoveBindingButtonClicked); 30 connect(m_ui.clearBindings, &QPushButton::clicked, this, &InputBindingDialog::onClearBindingsButtonClicked); 31 connect(m_ui.buttonBox, &QDialogButtonBox::rejected, [this]() { done(0); }); 32 updateList(); 33 34 // Only show the sensitivity controls for binds where it's applicable. 35 if (bind_type == InputBindingInfo::Type::Button || bind_type == InputBindingInfo::Type::Axis || 36 bind_type == InputBindingInfo::Type::HalfAxis) 37 { 38 ControllerSettingWidgetBinder::BindWidgetToInputProfileNormalized(sif, m_ui.sensitivity, m_section_name, 39 fmt::format("{}Scale", m_key_name), 100.0f, 1.0f); 40 ControllerSettingWidgetBinder::BindWidgetToInputProfileNormalized( 41 sif, m_ui.deadzone, m_section_name, fmt::format("{}Deadzone", m_key_name), 100.0f, 0.0f); 42 43 connect(m_ui.sensitivity, &QSlider::valueChanged, this, &InputBindingDialog::onSensitivityChanged); 44 connect(m_ui.resetSensitivity, &QToolButton::clicked, this, &InputBindingDialog::onResetSensitivityClicked); 45 connect(m_ui.deadzone, &QSlider::valueChanged, this, &InputBindingDialog::onDeadzoneChanged); 46 connect(m_ui.resetDeadzone, &QToolButton::clicked, this, &InputBindingDialog::onResetDeadzoneClicked); 47 48 onSensitivityChanged(m_ui.sensitivity->value()); 49 onDeadzoneChanged(m_ui.deadzone->value()); 50 } 51 else 52 { 53 m_ui.verticalLayout->removeWidget(m_ui.sensitivityWidget); 54 } 55 } 56 57 InputBindingDialog::~InputBindingDialog() 58 { 59 Q_ASSERT(!isListeningForInput()); 60 } 61 62 bool InputBindingDialog::eventFilter(QObject* watched, QEvent* event) 63 { 64 const QEvent::Type event_type = event->type(); 65 66 // if the key is being released, set the input 67 if (event_type == QEvent::KeyRelease || event_type == QEvent::MouseButtonRelease) 68 { 69 addNewBinding(); 70 stopListeningForInput(); 71 return true; 72 } 73 else if (event_type == QEvent::KeyPress) 74 { 75 const QKeyEvent* key_event = static_cast<const QKeyEvent*>(event); 76 m_new_bindings.push_back(InputManager::MakeHostKeyboardKey(QtUtils::KeyEventToCode(key_event))); 77 return true; 78 } 79 else if (event_type == QEvent::MouseButtonPress || event_type == QEvent::MouseButtonDblClick) 80 { 81 // double clicks get triggered if we click bind, then click again quickly. 82 unsigned button_index = CountTrailingZeros(static_cast<u32>(static_cast<const QMouseEvent*>(event)->button())); 83 m_new_bindings.push_back(InputManager::MakePointerButtonKey(0, button_index)); 84 return true; 85 } 86 else if (event_type == QEvent::Wheel) 87 { 88 const QPoint delta_angle(static_cast<QWheelEvent*>(event)->angleDelta()); 89 const float dx = std::clamp(static_cast<float>(delta_angle.x()) / QtUtils::MOUSE_WHEEL_DELTA, -1.0f, 1.0f); 90 if (dx != 0.0f) 91 { 92 InputBindingKey key(InputManager::MakePointerAxisKey(0, InputPointerAxis::WheelX)); 93 key.modifier = dx < 0.0f ? InputModifier::Negate : InputModifier::None; 94 m_new_bindings.push_back(key); 95 } 96 97 const float dy = std::clamp(static_cast<float>(delta_angle.y()) / QtUtils::MOUSE_WHEEL_DELTA, -1.0f, 1.0f); 98 if (dy != 0.0f) 99 { 100 InputBindingKey key(InputManager::MakePointerAxisKey(0, InputPointerAxis::WheelY)); 101 key.modifier = dy < 0.0f ? InputModifier::Negate : InputModifier::None; 102 m_new_bindings.push_back(key); 103 } 104 105 if (dx != 0.0f || dy != 0.0f) 106 { 107 addNewBinding(); 108 stopListeningForInput(); 109 } 110 111 return true; 112 } 113 else if (event_type == QEvent::MouseMove && m_mouse_mapping_enabled) 114 { 115 // if we've moved more than a decent distance from the center of the widget, bind it. 116 // this is so we don't accidentally bind to the mouse if you bump it while reaching for your pad. 117 static constexpr const s32 THRESHOLD = 50; 118 const QPoint diff(static_cast<QMouseEvent*>(event)->globalPosition().toPoint() - m_input_listen_start_position); 119 bool has_one = false; 120 121 if (std::abs(diff.x()) >= THRESHOLD) 122 { 123 InputBindingKey key(InputManager::MakePointerAxisKey(0, InputPointerAxis::X)); 124 key.modifier = diff.x() < 0 ? InputModifier::Negate : InputModifier::None; 125 m_new_bindings.push_back(key); 126 has_one = true; 127 } 128 if (std::abs(diff.y()) >= THRESHOLD) 129 { 130 InputBindingKey key(InputManager::MakePointerAxisKey(0, InputPointerAxis::Y)); 131 key.modifier = diff.y() < 0 ? InputModifier::Negate : InputModifier::None; 132 m_new_bindings.push_back(key); 133 has_one = true; 134 } 135 136 if (has_one) 137 { 138 addNewBinding(); 139 stopListeningForInput(); 140 return true; 141 } 142 } 143 144 return false; 145 } 146 147 void InputBindingDialog::onInputListenTimerTimeout() 148 { 149 m_input_listen_remaining_seconds--; 150 if (m_input_listen_remaining_seconds == 0) 151 { 152 stopListeningForInput(); 153 return; 154 } 155 156 m_ui.status->setText(tr("Push Button/Axis... [%1]").arg(m_input_listen_remaining_seconds)); 157 } 158 159 void InputBindingDialog::startListeningForInput(u32 timeout_in_seconds) 160 { 161 m_value_ranges.clear(); 162 m_new_bindings.clear(); 163 m_mouse_mapping_enabled = InputBindingWidget::isMouseMappingEnabled(m_sif); 164 m_input_listen_start_position = QCursor::pos(); 165 m_input_listen_timer = new QTimer(this); 166 m_input_listen_timer->setSingleShot(false); 167 m_input_listen_timer->start(1000); 168 169 m_input_listen_timer->connect(m_input_listen_timer, &QTimer::timeout, this, 170 &InputBindingDialog::onInputListenTimerTimeout); 171 m_input_listen_remaining_seconds = timeout_in_seconds; 172 m_ui.status->setText(tr("Push Button/Axis... [%1]").arg(m_input_listen_remaining_seconds)); 173 m_ui.addBinding->setEnabled(false); 174 m_ui.removeBinding->setEnabled(false); 175 m_ui.clearBindings->setEnabled(false); 176 m_ui.buttonBox->setEnabled(false); 177 178 installEventFilter(this); 179 grabKeyboard(); 180 grabMouse(); 181 setMouseTracking(true); 182 hookInputManager(); 183 } 184 185 void InputBindingDialog::stopListeningForInput() 186 { 187 m_ui.status->clear(); 188 m_ui.addBinding->setEnabled(true); 189 m_ui.removeBinding->setEnabled(true); 190 m_ui.clearBindings->setEnabled(true); 191 m_ui.buttonBox->setEnabled(true); 192 193 delete m_input_listen_timer; 194 m_input_listen_timer = nullptr; 195 196 unhookInputManager(); 197 releaseMouse(); 198 releaseKeyboard(); 199 setMouseTracking(false); 200 removeEventFilter(this); 201 } 202 203 void InputBindingDialog::addNewBinding() 204 { 205 if (m_new_bindings.empty()) 206 return; 207 208 const std::string new_binding( 209 InputManager::ConvertInputBindingKeysToString(m_bind_type, m_new_bindings.data(), m_new_bindings.size())); 210 if (!new_binding.empty()) 211 { 212 if (std::find(m_bindings.begin(), m_bindings.end(), new_binding) != m_bindings.end()) 213 return; 214 215 m_ui.bindingList->addItem(QString::fromStdString(new_binding)); 216 m_bindings.push_back(std::move(new_binding)); 217 saveListToSettings(); 218 } 219 } 220 221 void InputBindingDialog::onAddBindingButtonClicked() 222 { 223 if (isListeningForInput()) 224 stopListeningForInput(); 225 226 startListeningForInput(TIMEOUT_FOR_BINDING); 227 } 228 229 void InputBindingDialog::onRemoveBindingButtonClicked() 230 { 231 const int row = m_ui.bindingList->currentRow(); 232 if (row < 0 || static_cast<size_t>(row) >= m_bindings.size()) 233 return; 234 235 m_bindings.erase(m_bindings.begin() + row); 236 delete m_ui.bindingList->takeItem(row); 237 saveListToSettings(); 238 } 239 240 void InputBindingDialog::onClearBindingsButtonClicked() 241 { 242 m_bindings.clear(); 243 m_ui.bindingList->clear(); 244 saveListToSettings(); 245 } 246 247 void InputBindingDialog::updateList() 248 { 249 m_ui.bindingList->clear(); 250 for (const std::string& binding : m_bindings) 251 m_ui.bindingList->addItem(QString::fromStdString(binding)); 252 } 253 254 void InputBindingDialog::saveListToSettings() 255 { 256 if (m_sif) 257 { 258 if (!m_bindings.empty()) 259 m_sif->SetStringList(m_section_name.c_str(), m_key_name.c_str(), m_bindings); 260 else 261 m_sif->DeleteValue(m_section_name.c_str(), m_key_name.c_str()); 262 QtHost::SaveGameSettings(m_sif, false); 263 g_emu_thread->reloadGameSettings(); 264 } 265 else 266 { 267 if (!m_bindings.empty()) 268 Host::SetBaseStringListSettingValue(m_section_name.c_str(), m_key_name.c_str(), m_bindings); 269 else 270 Host::DeleteBaseSettingValue(m_section_name.c_str(), m_key_name.c_str()); 271 Host::CommitBaseSettingChanges(); 272 if (m_bind_type == InputBindingInfo::Type::Pointer) 273 g_emu_thread->updateControllerSettings(); 274 g_emu_thread->reloadInputBindings(); 275 } 276 } 277 278 void InputBindingDialog::inputManagerHookCallback(InputBindingKey key, float value) 279 { 280 if (!isListeningForInput()) 281 return; 282 283 float initial_value = value; 284 float min_value = value; 285 auto it = std::find_if(m_value_ranges.begin(), m_value_ranges.end(), 286 [key](const auto& it) { return it.first.bits == key.bits; }); 287 if (it != m_value_ranges.end()) 288 { 289 initial_value = it->second.first; 290 min_value = it->second.second = std::min(it->second.second, value); 291 } 292 else 293 { 294 m_value_ranges.emplace_back(key, std::make_pair(initial_value, min_value)); 295 } 296 297 const float abs_value = std::abs(value); 298 const bool reverse_threshold = (key.source_subtype == InputSubclass::ControllerAxis && initial_value > 0.5f); 299 300 for (InputBindingKey& other_key : m_new_bindings) 301 { 302 if (other_key.MaskDirection() == key.MaskDirection()) 303 { 304 // for pedals, we wait for it to go back to near its starting point to commit the binding 305 if ((reverse_threshold ? ((initial_value - value) <= 0.25f) : (abs_value < 0.5f))) 306 { 307 // did we go the full range? 308 if (reverse_threshold && initial_value > 0.5f && min_value <= -0.5f) 309 other_key.modifier = InputModifier::FullAxis; 310 311 // if this key is in our new binding list, it's a "release", and we're done 312 addNewBinding(); 313 stopListeningForInput(); 314 return; 315 } 316 317 // otherwise, keep waiting 318 return; 319 } 320 } 321 322 // new binding, add it to the list, but wait for a decent distance first, and then wait for release 323 if ((reverse_threshold ? (abs_value < 0.5f) : (abs_value >= 0.5f))) 324 { 325 InputBindingKey key_to_add = key; 326 key_to_add.modifier = (value < 0.0f && !reverse_threshold) ? InputModifier::Negate : InputModifier::None; 327 key_to_add.invert = reverse_threshold; 328 m_new_bindings.push_back(key_to_add); 329 } 330 } 331 332 void InputBindingDialog::onSensitivityChanged(int value) 333 { 334 m_ui.sensitivityValue->setText(tr("%1%").arg(value)); 335 } 336 337 void InputBindingDialog::onResetDeadzoneClicked() 338 { 339 m_ui.deadzone->setValue(0); 340 341 // May as well remove from the config completely, since it's the default. 342 const TinyString key = TinyString::from_format("{}Deadzone", m_key_name); 343 if (m_sif) 344 { 345 m_sif->DeleteValue(m_section_name.c_str(), key); 346 QtHost::SaveGameSettings(m_sif, false); 347 g_emu_thread->reloadGameSettings(false); 348 } 349 else 350 { 351 Host::DeleteBaseSettingValue(m_section_name.c_str(), key); 352 Host::CommitBaseSettingChanges(); 353 g_emu_thread->applySettings(false); 354 } 355 } 356 357 void InputBindingDialog::onDeadzoneChanged(int value) 358 { 359 m_ui.deadzoneValue->setText(tr("%1%").arg(value)); 360 } 361 362 void InputBindingDialog::onResetSensitivityClicked() 363 { 364 m_ui.sensitivity->setValue(100); 365 366 // May as well remove from the config completely, since it's the default. 367 const TinyString key = TinyString::from_format("{}Scale", m_key_name); 368 if (m_sif) 369 { 370 m_sif->DeleteValue(m_section_name.c_str(), key); 371 QtHost::SaveGameSettings(m_sif, false); 372 g_emu_thread->reloadGameSettings(false); 373 } 374 else 375 { 376 Host::DeleteBaseSettingValue(m_section_name.c_str(), key); 377 Host::CommitBaseSettingChanges(); 378 g_emu_thread->applySettings(false); 379 } 380 } 381 382 void InputBindingDialog::hookInputManager() 383 { 384 InputManager::SetHook([this](InputBindingKey key, float value) { 385 QMetaObject::invokeMethod(this, "inputManagerHookCallback", Qt::QueuedConnection, Q_ARG(InputBindingKey, key), 386 Q_ARG(float, value)); 387 return InputInterceptHook::CallbackResult::StopProcessingEvent; 388 }); 389 } 390 391 void InputBindingDialog::unhookInputManager() 392 { 393 InputManager::RemoveHook(); 394 }