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 }