diff --git a/pcsx2-qt/DisplayWidget.cpp b/pcsx2-qt/DisplayWidget.cpp index 90a93fe524..6fb5af8014 100644 --- a/pcsx2-qt/DisplayWidget.cpp +++ b/pcsx2-qt/DisplayWidget.cpp @@ -33,7 +33,9 @@ #include #include -#if !defined(_WIN32) && !defined(APPLE) +#if defined(_WIN32) +#include "common/RedtapeWindows.h" +#elif !defined(APPLE) #include #endif @@ -50,7 +52,13 @@ DisplayWidget::DisplayWidget(QWidget* parent) setMouseTracking(true); } -DisplayWidget::~DisplayWidget() = default; +DisplayWidget::~DisplayWidget() +{ +#ifdef _WIN32 + if (m_clip_mouse_enabled) + ClipCursor(nullptr); +#endif +} qreal DisplayWidget::devicePixelRatioFromScreen() const { @@ -110,27 +118,96 @@ std::optional DisplayWidget::getWindowInfo() return wi; } -void DisplayWidget::setRelativeMode(bool enabled) +void DisplayWidget::updateRelativeMode(bool master_enable) { - if (m_relative_mouse_enabled == enabled) + bool relative_mode = master_enable && InputManager::HasPointerAxisBinds(); + +#ifdef _WIN32 + // prefer ClipCursor() over warping movement when we're using raw input + bool clip_cursor = relative_mode && false /*InputManager::IsUsingRawInput()*/; + if (m_relative_mouse_enabled == relative_mode && m_clip_mouse_enabled == clip_cursor) return; - if (enabled) - { - m_relative_mouse_start_position = QCursor::pos(); + DevCon.WriteLn("updateRelativeMode(): relative=%s, clip=%s", relative_mode ? "yes" : "no", clip_cursor ? "yes" : "no"); - const QPoint center_pos = mapToGlobal(QPoint(width() / 2, height() / 2)); - QCursor::setPos(center_pos); - m_relative_mouse_last_position = center_pos; + if (!clip_cursor && m_clip_mouse_enabled) + { + m_clip_mouse_enabled = false; + ClipCursor(nullptr); + } +#else + if (m_relative_mouse_enabled == relative_mode) + return; + + DevCon.WriteLn("updateRelativeMode(): relative=%s", relative_mode ? "yes" : "no"); +#endif + + if (relative_mode) + { +#ifdef _WIN32 + m_relative_mouse_enabled = !clip_cursor; + m_clip_mouse_enabled = clip_cursor; +#else + m_relative_mouse_enabled = true; +#endif + m_relative_mouse_start_pos = QCursor::pos(); + updateCenterPos(); grabMouse(); } - else + else if (m_relative_mouse_enabled) { - QCursor::setPos(m_relative_mouse_start_position); + m_relative_mouse_enabled = false; + QCursor::setPos(m_relative_mouse_start_pos); releaseMouse(); } - m_relative_mouse_enabled = enabled; +} + +void DisplayWidget::updateCursor(bool master_enable) +{ +#ifdef _WIN32 + const bool hide = master_enable && (m_should_hide_cursor || m_relative_mouse_enabled || m_clip_mouse_enabled); +#else + const bool hide = master_enable && (m_should_hide_cursor || m_relative_mouse_enabled); +#endif + if (m_cursor_hidden == hide) + return; + + m_cursor_hidden = hide; + if (hide) + setCursor(Qt::BlankCursor); + else + unsetCursor(); +} + +void DisplayWidget::updateCenterPos() +{ +#ifdef _WIN32 + if (m_clip_mouse_enabled) + { + RECT rc; + if (GetWindowRect(reinterpret_cast(winId()), &rc)) + ClipCursor(&rc); + } + else if (m_relative_mouse_enabled) + { + RECT rc; + if (GetWindowRect(reinterpret_cast(winId()), &rc)) + { + m_relative_mouse_center_pos.setX(((rc.right - rc.left) / 2) + rc.left); + m_relative_mouse_center_pos.setY(((rc.bottom - rc.top) / 2) + rc.top); + SetCursorPos(m_relative_mouse_center_pos.x(), m_relative_mouse_center_pos.y()); + } + } +#else + if (m_relative_mouse_enabled) + { + // we do a round trip here because these coordinates are dpi-unscaled + m_relative_mouse_center_pos = mapToGlobal(QPoint((width() + 1) / 2, (height() + 1) / 2)); + QCursor::setPos(m_relative_mouse_center_pos); + m_relative_mouse_center_pos = QCursor::pos(); + } +#endif } QPaintEngine* DisplayWidget::paintEngine() const @@ -172,7 +249,10 @@ bool DisplayWidget::event(QEvent* event) m_keys_pressed_with_modifiers.push_back(key); } - emit windowKeyEvent(key, pressed); + Host::RunOnCPUThread([key, pressed]() { + InputManager::InvokeEvents(InputManager::MakeHostKeyboardKey(key), static_cast(pressed)); + }); + return true; } @@ -184,22 +264,39 @@ bool DisplayWidget::event(QEvent* event) { const qreal dpr = devicePixelRatioFromScreen(); const QPoint mouse_pos = mouse_event->pos(); - const int scaled_x = static_cast(static_cast(mouse_pos.x()) * dpr); - const int scaled_y = static_cast(static_cast(mouse_pos.y()) * dpr); - windowMouseMoveEvent(scaled_x, scaled_y); + const float scaled_x = static_cast(static_cast(mouse_pos.x()) * dpr); + const float scaled_y = static_cast(static_cast(mouse_pos.y()) * dpr); + InputManager::UpdatePointerAbsolutePosition(0, scaled_x, scaled_y); } else { - const QPoint center_pos = mapToGlobal(QPoint((width() + 1) / 2, (height() + 1) / 2)); - const QPoint mouse_pos = mapToGlobal(mouse_event->pos()); + // On windows, we use winapi here. The reason being that the coordinates in QCursor + // are un-dpi-scaled, so we lose precision at higher desktop scalings. + float dx = 0.0f, dy = 0.0f; - const int dx = mouse_pos.x() - center_pos.x(); - const int dy = mouse_pos.y() - center_pos.y(); - m_relative_mouse_last_position.setX(m_relative_mouse_last_position.x() + dx); - m_relative_mouse_last_position.setY(m_relative_mouse_last_position.y() + dy); - windowMouseMoveEvent(m_relative_mouse_last_position.x(), m_relative_mouse_last_position.y()); - QCursor::setPos(center_pos); +#ifndef _WIN32 + const QPoint mouse_pos = QCursor::pos(); + if (mouse_pos != m_relative_mouse_center_pos) + { + dx = static_cast(mouse_pos.x() - m_relative_mouse_center_pos.x()); + dy = static_cast(mouse_pos.y() - m_relative_mouse_center_pos.y()); + QCursor::setPos(m_relative_mouse_center_pos); + } +#else + POINT mouse_pos; + if (GetCursorPos(&mouse_pos)) + { + dx = static_cast(mouse_pos.x - m_relative_mouse_center_pos.x()); + dy = static_cast(mouse_pos.y - m_relative_mouse_center_pos.y()); + SetCursorPos(m_relative_mouse_center_pos.x(), m_relative_mouse_center_pos.y()); + } +#endif + + if (dx != 0.0f) + InputManager::UpdatePointerRelativeDelta(0, InputPointerAxis::X, dx); + if (dy != 0.0f) + InputManager::UpdatePointerRelativeDelta(0, InputPointerAxis::Y, dy); } return true; @@ -211,10 +308,16 @@ bool DisplayWidget::event(QEvent* event) { unsigned long button_index; if (_BitScanForward(&button_index, static_cast(static_cast(event)->button()))) - emit windowMouseButtonEvent(static_cast(button_index + 1u), event->type() != QEvent::MouseButtonRelease); + { + Host::RunOnCPUThread([button_index, pressed = (event->type() != QEvent::MouseButtonRelease)]() { + InputManager::InvokeEvents(InputManager::MakePointerButtonKey(0, button_index), static_cast(pressed)); + }); + } + // don't toggle fullscreen when we're bound.. that wouldn't end well. if (event->type() == QEvent::MouseButtonDblClick && static_cast(event)->button() == Qt::LeftButton && + !InputManager::HasAnyBindingsForKey(InputManager::MakePointerButtonKey(0, 0)) && Host::GetBoolSettingValue("UI", "DoubleClickTogglesFullscreen", true)) { g_emu_thread->toggleFullscreen(); @@ -225,8 +328,18 @@ bool DisplayWidget::event(QEvent* event) case QEvent::Wheel: { - const QWheelEvent* wheel_event = static_cast(event); - emit windowMouseWheelEvent(wheel_event->angleDelta()); + // wheel delta is 120 as in winapi + const QPoint delta_angle(static_cast(event)->angleDelta()); + constexpr float DELTA = 120.0f; + + const float dx = std::clamp(static_cast(delta_angle.x()) / DELTA, -1.0f, 1.0f); + if (dx != 0.0f) + InputManager::UpdatePointerRelativeDelta(0, InputPointerAxis::WheelX, dx); + + const float dy = std::clamp(static_cast(delta_angle.y()) / DELTA, -1.0f, 1.0f); + if (dy != 0.0f) + InputManager::UpdatePointerRelativeDelta(0, InputPointerAxis::WheelY, dy); + return true; } @@ -249,6 +362,13 @@ bool DisplayWidget::event(QEvent* event) emit windowResizedEvent(scaled_width, scaled_height, dpr); } + updateCenterPos(); + return true; + } + + case QEvent::Move: + { + updateCenterPos(); return true; } diff --git a/pcsx2-qt/DisplayWidget.h b/pcsx2-qt/DisplayWidget.h index 74106879c0..69ee0ecfcc 100644 --- a/pcsx2-qt/DisplayWidget.h +++ b/pcsx2-qt/DisplayWidget.h @@ -30,30 +30,37 @@ public: QPaintEngine* paintEngine() const override; + __fi void setShouldHideCursor(bool hide) { m_should_hide_cursor = hide; } + int scaledWindowWidth() const; int scaledWindowHeight() const; qreal devicePixelRatioFromScreen() const; std::optional getWindowInfo(); - void setRelativeMode(bool enabled); + void updateRelativeMode(bool master_enable); + void updateCursor(bool master_enable); Q_SIGNALS: void windowFocusEvent(); void windowResizedEvent(int width, int height, float scale); void windowRestoredEvent(); - void windowKeyEvent(int key_code, bool pressed); - void windowMouseMoveEvent(int x, int y); - void windowMouseButtonEvent(int button, bool pressed); - void windowMouseWheelEvent(const QPoint& angle_delta); protected: bool event(QEvent* event) override; private: - QPoint m_relative_mouse_start_position{}; - QPoint m_relative_mouse_last_position{}; + void updateCenterPos(); + + QPoint m_relative_mouse_start_pos{}; + QPoint m_relative_mouse_center_pos{}; bool m_relative_mouse_enabled = false; +#ifdef _WIN32 + bool m_clip_mouse_enabled = false; +#endif + bool m_should_hide_cursor = false; + bool m_cursor_hidden = false; + std::vector m_keys_pressed_with_modifiers; u32 m_last_window_width = 0; diff --git a/pcsx2-qt/EmuThread.cpp b/pcsx2-qt/EmuThread.cpp index b5fbaf99d1..60d7b64c11 100644 --- a/pcsx2-qt/EmuThread.cpp +++ b/pcsx2-qt/EmuThread.cpp @@ -419,6 +419,8 @@ void EmuThread::loadOurSettings() void EmuThread::checkForSettingChanges() { + QMetaObject::invokeMethod(g_main_window, &MainWindow::checkForSettingChanges, Qt::QueuedConnection); + if (VMManager::HasValidVM()) { const bool render_to_main = Host::GetBaseBoolSettingValue("UI", "RenderToMainWindow", true); @@ -585,29 +587,11 @@ void EmuThread::connectDisplaySignals(DisplayWidget* widget) connect(widget, &DisplayWidget::windowFocusEvent, this, &EmuThread::onDisplayWindowFocused); connect(widget, &DisplayWidget::windowResizedEvent, this, &EmuThread::onDisplayWindowResized); // connect(widget, &DisplayWidget::windowRestoredEvent, this, &EmuThread::redrawDisplayWindow); - connect(widget, &DisplayWidget::windowKeyEvent, this, &EmuThread::onDisplayWindowKeyEvent); - connect(widget, &DisplayWidget::windowMouseMoveEvent, this, &EmuThread::onDisplayWindowMouseMoveEvent); - connect(widget, &DisplayWidget::windowMouseButtonEvent, this, &EmuThread::onDisplayWindowMouseButtonEvent); - connect(widget, &DisplayWidget::windowMouseWheelEvent, this, &EmuThread::onDisplayWindowMouseWheelEvent); -} - -void EmuThread::onDisplayWindowMouseMoveEvent(int x, int y) {} - -void EmuThread::onDisplayWindowMouseButtonEvent(int button, bool pressed) -{ - InputManager::InvokeEvents(InputManager::MakeHostMouseButtonKey(button), pressed ? 1.0f : 0.0f); -} - -void EmuThread::onDisplayWindowMouseWheelEvent(const QPoint& delta_angle) {} - -void EmuThread::onDisplayWindowKeyEvent(int key, bool pressed) -{ - InputManager::InvokeEvents(InputManager::MakeHostKeyboardKey(key), pressed ? 1.0f : 0.0f); } void EmuThread::onDisplayWindowResized(int width, int height, float scale) { - if (!VMManager::HasValidVM()) + if (!s_host_display) return; GetMTGS().ResizeDisplayWindow(width, height, scale); diff --git a/pcsx2-qt/EmuThread.h b/pcsx2-qt/EmuThread.h index 9e3b4fe7f6..2c498fc147 100644 --- a/pcsx2-qt/EmuThread.h +++ b/pcsx2-qt/EmuThread.h @@ -142,12 +142,8 @@ private: private Q_SLOTS: void stopInThread(); void doBackgroundControllerPoll(); - void onDisplayWindowMouseMoveEvent(int x, int y); - void onDisplayWindowMouseButtonEvent(int button, bool pressed); - void onDisplayWindowMouseWheelEvent(const QPoint& delta_angle); void onDisplayWindowResized(int width, int height, float scale); void onDisplayWindowFocused(); - void onDisplayWindowKeyEvent(int key, bool pressed); private: QThread* m_ui_thread; diff --git a/pcsx2-qt/MainWindow.cpp b/pcsx2-qt/MainWindow.cpp index 42fb8322dd..5ce728f9e4 100644 --- a/pcsx2-qt/MainWindow.cpp +++ b/pcsx2-qt/MainWindow.cpp @@ -766,6 +766,11 @@ bool MainWindow::isRenderingToMain() const return (m_display_widget && m_display_widget->parent() == this); } +bool MainWindow::shouldHideMouseCursor() const +{ + return isRenderingFullscreen() && Host::GetBoolSettingValue("UI", "HideMouseCursor", false); +} + void MainWindow::switchToGameListView() { if (centralWidget() == m_game_list_widget) @@ -897,6 +902,12 @@ void MainWindow::requestExit() close(); } +void MainWindow::checkForSettingChanges() +{ + if (m_display_widget) + m_display_widget->updateRelativeMode(m_vm_valid && !m_vm_paused); +} + void Host::InvalidateSaveStateCache() { QMetaObject::invokeMethod(g_main_window, &MainWindow::invalidateSaveStateCache, Qt::QueuedConnection); @@ -1354,6 +1365,8 @@ void MainWindow::onVMPaused() updateWindowTitle(); updateStatusBarWidgetVisibility(); m_status_fps_widget->setText(tr("Paused")); + m_display_widget->updateRelativeMode(false); + m_display_widget->updateCursor(false); } void MainWindow::onVMResumed() @@ -1369,6 +1382,8 @@ void MainWindow::onVMResumed() updateWindowTitle(); updateStatusBarWidgetVisibility(); m_status_fps_widget->setText(m_last_fps_status); + m_display_widget->updateRelativeMode(true); + m_display_widget->updateCursor(true); if (m_display_widget) m_display_widget->setFocus(); } @@ -1381,7 +1396,16 @@ void MainWindow::onVMStopped() updateEmulationActions(false, false); updateWindowTitle(); updateStatusBarWidgetVisibility(); - switchToGameListView(); + + if (m_display_widget) + { + m_display_widget->updateRelativeMode(false); + m_display_widget->updateCursor(false); + } + else + { + switchToGameListView(); + } } void MainWindow::onGameChanged(const QString& path, const QString& serial, const QString& name, quint32 crc) @@ -1554,6 +1578,10 @@ DisplayWidget* MainWindow::createDisplay(bool fullscreen, bool render_to_main) updateWindowTitle(); m_display_widget->setFocus(); + m_display_widget->setShouldHideCursor(shouldHideMouseCursor()); + m_display_widget->updateRelativeMode(m_vm_valid && !m_vm_paused); + m_display_widget->updateCursor(m_vm_valid && !m_vm_paused); + host_display->DoneRenderContextCurrent(); return m_display_widget; } @@ -1676,6 +1704,9 @@ DisplayWidget* MainWindow::updateDisplay(bool fullscreen, bool render_to_main, b updateWindowTitle(); m_display_widget->setFocus(); + m_display_widget->setShouldHideCursor(shouldHideMouseCursor()); + m_display_widget->updateRelativeMode(m_vm_valid && !m_vm_paused); + m_display_widget->updateCursor(m_vm_valid && !m_vm_paused); QSignalBlocker blocker(m_ui.actionFullscreen); m_ui.actionFullscreen->setChecked(fullscreen); diff --git a/pcsx2-qt/MainWindow.h b/pcsx2-qt/MainWindow.h index f21aab79fb..584590f0b5 100644 --- a/pcsx2-qt/MainWindow.h +++ b/pcsx2-qt/MainWindow.h @@ -93,6 +93,7 @@ public Q_SLOTS: void runOnUIThread(const std::function& func); bool requestShutdown(bool allow_confirm = true, bool allow_save_to_state = true, bool block_until_done = false); void requestExit(); + void checkForSettingChanges(); private Q_SLOTS: void onUpdateCheckComplete(); @@ -184,6 +185,7 @@ private: bool isShowingGameList() const; bool isRenderingFullscreen() const; bool isRenderingToMain() const; + bool shouldHideMouseCursor() const; void switchToGameListView(); void switchToEmulationView(); diff --git a/pcsx2-qt/Settings/ControllerGlobalSettingsWidget.cpp b/pcsx2-qt/Settings/ControllerGlobalSettingsWidget.cpp index de04a28ffe..8d62453f50 100644 --- a/pcsx2-qt/Settings/ControllerGlobalSettingsWidget.cpp +++ b/pcsx2-qt/Settings/ControllerGlobalSettingsWidget.cpp @@ -35,6 +35,10 @@ ControllerGlobalSettingsWidget::ControllerGlobalSettingsWidget(QWidget* parent, SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.enableXInputSource, "InputSources", "XInput", false); ControllerSettingWidgetBinder::BindWidgetToInputProfileBool(sif, m_ui.multitapPort1, "Pad", "MultitapPort1", false); ControllerSettingWidgetBinder::BindWidgetToInputProfileBool(sif, m_ui.multitapPort2, "Pad", "MultitapPort2", false); + ControllerSettingWidgetBinder::BindWidgetToInputProfileBool(sif, m_ui.pointerXInvert, "Pad", "PointerXInvert", false); + ControllerSettingWidgetBinder::BindWidgetToInputProfileBool(sif, m_ui.pointerYInvert, "Pad", "PointerYInvert", false); + ControllerSettingWidgetBinder::BindWidgetToInputProfileFloat(sif, m_ui.pointerXScale, "Pad", "PointerXScale", 8.0f); + ControllerSettingWidgetBinder::BindWidgetToInputProfileFloat(sif, m_ui.pointerYScale, "Pad", "PointerYScale", 8.0f); if (dialog->isEditingProfile()) { @@ -56,6 +60,11 @@ ControllerGlobalSettingsWidget::ControllerGlobalSettingsWidget(QWidget* parent, for (QCheckBox* cb : {m_ui.multitapPort1, m_ui.multitapPort2}) connect(cb, &QCheckBox::stateChanged, this, [this]() { emit bindingSetupChanged(); }); + connect(m_ui.pointerXScale, &QSlider::valueChanged, this, [this](int value) { m_ui.pointerXScaleLabel->setText(QStringLiteral("%1").arg(value)); }); + connect(m_ui.pointerYScale, &QSlider::valueChanged, this, [this](int value) { m_ui.pointerYScaleLabel->setText(QStringLiteral("%1").arg(value)); }); + m_ui.pointerXScaleLabel->setText(QStringLiteral("%1").arg(m_ui.pointerXScale->value())); + m_ui.pointerYScaleLabel->setText(QStringLiteral("%1").arg(m_ui.pointerYScale->value())); + updateSDLOptionsEnabled(); } diff --git a/pcsx2-qt/Settings/ControllerGlobalSettingsWidget.ui b/pcsx2-qt/Settings/ControllerGlobalSettingsWidget.ui index ddb1249d54..a9479ceaf6 100644 --- a/pcsx2-qt/Settings/ControllerGlobalSettingsWidget.ui +++ b/pcsx2-qt/Settings/ControllerGlobalSettingsWidget.ui @@ -7,7 +7,7 @@ 0 0 900 - 500 + 675 @@ -42,20 +42,20 @@ - - - - DualShock 4 / DualSense Enhanced Mode - - - - + Enable SDL Input Source + + + + DualShock 4 / DualSense Enhanced Mode + + + @@ -86,6 +86,141 @@ + + + Mouse/Pointer Source + + + + + + + 0 + 0 + + + + + 150 + 0 + + + + 1 + + + 30 + + + Qt::Horizontal + + + + + + + Using raw input improves precision when you bind controller sticks to the mouse pointer. Also enables multiple mice to be used. + + + true + + + + + + + Invert + + + + + + + + 0 + 0 + + + + + 20 + 0 + + + + 10 + + + + + + + Invert + + + + + + + Vertical Sensitivity: + + + + + + + + 0 + 0 + + + + + 150 + 0 + + + + 1 + + + 30 + + + Qt::Horizontal + + + + + + + + 0 + 0 + + + + + 20 + 0 + + + + 10 + + + + + + + Horizontal Sensitivity: + + + + + + + Controller Multitap @@ -101,14 +236,14 @@ - + Multitap on Console Port 1 - + Multitap on Console Port 2 @@ -118,7 +253,7 @@ - + Profile Settings @@ -144,7 +279,7 @@ - + Qt::Vertical @@ -157,7 +292,7 @@ - + Detected Devices diff --git a/pcsx2-qt/Settings/ControllerSettingWidgetBinder.h b/pcsx2-qt/Settings/ControllerSettingWidgetBinder.h index a5dd9b0176..4d057e75cb 100644 --- a/pcsx2-qt/Settings/ControllerSettingWidgetBinder.h +++ b/pcsx2-qt/Settings/ControllerSettingWidgetBinder.h @@ -78,7 +78,7 @@ namespace ControllerSettingWidgetBinder if (sif) { const float value = sif->GetFloatValue(section.c_str(), key.c_str(), default_value); - Accessor::setBoolValue(widget, value); + Accessor::setFloatValue(widget, value); Accessor::connectValueChanged(widget, [sif, widget, section = std::move(section), key = std::move(key)]() { const float new_value = Accessor::getFloatValue(widget); @@ -90,7 +90,7 @@ namespace ControllerSettingWidgetBinder else { const float value = Host::GetBaseFloatSettingValue(section.c_str(), key.c_str(), default_value); - Accessor::setBoolValue(widget, value); + Accessor::setFloatValue(widget, value); Accessor::connectValueChanged(widget, [widget, section = std::move(section), key = std::move(key)]() { const float new_value = Accessor::getFloatValue(widget); diff --git a/pcsx2-qt/Settings/InputBindingDialog.cpp b/pcsx2-qt/Settings/InputBindingDialog.cpp index 8da4dce42b..952d71d296 100644 --- a/pcsx2-qt/Settings/InputBindingDialog.cpp +++ b/pcsx2-qt/Settings/InputBindingDialog.cpp @@ -55,7 +55,7 @@ bool InputBindingDialog::eventFilter(QObject* watched, QEvent* event) const QEvent::Type event_type = event->type(); // if the key is being released, set the input - if (event_type == QEvent::KeyRelease || event_type == QEvent::MouseButtonPress) + if (event_type == QEvent::KeyRelease || event_type == QEvent::MouseButtonRelease) { addNewBinding(); stopListeningForInput(); @@ -67,17 +67,43 @@ bool InputBindingDialog::eventFilter(QObject* watched, QEvent* event) m_new_bindings.push_back(InputManager::MakeHostKeyboardKey(key_event->key())); return true; } - else if (event_type == QEvent::MouseButtonPress) + else if (event_type == QEvent::MouseButtonPress || event_type == QEvent::MouseButtonDblClick) { + // double clicks get triggered if we click bind, then click again quickly. unsigned long button_index; if (_BitScanForward(&button_index, static_cast(static_cast(event)->button()))) - m_new_bindings.push_back(InputManager::MakeHostMouseButtonKey(button_index)); + m_new_bindings.push_back(InputManager::MakePointerButtonKey(0, button_index)); return true; } - else if (event_type == QEvent::MouseButtonDblClick) + else if (event_type == QEvent::MouseMove) { - // just eat double clicks - return true; + // if we've moved more than a decent distance from the center of the widget, bind it. + // this is so we don't accidentally bind to the mouse if you bump it while reaching for your pad. + static constexpr const s32 THRESHOLD = 50; + const QPoint diff(static_cast(event)->globalPos() - m_input_listen_start_position); + bool has_one = false; + + if (std::abs(diff.x()) >= THRESHOLD) + { + InputBindingKey key(InputManager::MakePointerAxisKey(0, InputPointerAxis::X)); + key.negative = (diff.x() < 0); + m_new_bindings.push_back(key); + has_one = true; + } + if (std::abs(diff.y()) >= THRESHOLD) + { + InputBindingKey key(InputManager::MakePointerAxisKey(0, InputPointerAxis::Y)); + key.negative = (diff.y() < 0); + m_new_bindings.push_back(key); + has_one = true; + } + + if (has_one) + { + addNewBinding(); + stopListeningForInput(); + return true; + } } return false; @@ -98,6 +124,7 @@ void InputBindingDialog::onInputListenTimerTimeout() void InputBindingDialog::startListeningForInput(u32 timeout_in_seconds) { m_new_bindings.clear(); + m_input_listen_start_position = QCursor::pos(); m_input_listen_timer = new QTimer(this); m_input_listen_timer->setSingleShot(false); m_input_listen_timer->start(1000); @@ -114,6 +141,7 @@ void InputBindingDialog::startListeningForInput(u32 timeout_in_seconds) installEventFilter(this); grabKeyboard(); grabMouse(); + setMouseTracking(true); hookInputManager(); } @@ -131,6 +159,7 @@ void InputBindingDialog::stopListeningForInput() unhookInputManager(); releaseMouse(); releaseKeyboard(); + setMouseTracking(false); removeEventFilter(this); } diff --git a/pcsx2-qt/Settings/InputBindingDialog.h b/pcsx2-qt/Settings/InputBindingDialog.h index 2f3b0ceeb8..48a35cea29 100644 --- a/pcsx2-qt/Settings/InputBindingDialog.h +++ b/pcsx2-qt/Settings/InputBindingDialog.h @@ -68,4 +68,5 @@ protected: QTimer* m_input_listen_timer = nullptr; u32 m_input_listen_remaining_seconds = 0; + QPoint m_input_listen_start_position{}; }; diff --git a/pcsx2-qt/Settings/InputBindingWidget.cpp b/pcsx2-qt/Settings/InputBindingWidget.cpp index 9926d1ab09..25487f3f0c 100644 --- a/pcsx2-qt/Settings/InputBindingWidget.cpp +++ b/pcsx2-qt/Settings/InputBindingWidget.cpp @@ -106,7 +106,7 @@ bool InputBindingWidget::eventFilter(QObject* watched, QEvent* event) const QEvent::Type event_type = event->type(); // if the key is being released, set the input - if (event_type == QEvent::KeyRelease || event_type == QEvent::MouseButtonPress) + if (event_type == QEvent::KeyRelease || event_type == QEvent::MouseButtonRelease) { setNewBinding(); stopListeningForInput(); @@ -118,17 +118,43 @@ bool InputBindingWidget::eventFilter(QObject* watched, QEvent* event) m_new_bindings.push_back(InputManager::MakeHostKeyboardKey(key_event->key())); return true; } - else if (event_type == QEvent::MouseButtonPress) + else if (event_type == QEvent::MouseButtonPress || event_type == QEvent::MouseButtonDblClick) { + // double clicks get triggered if we click bind, then click again quickly. unsigned long button_index; if (_BitScanForward(&button_index, static_cast(static_cast(event)->button()))) - m_new_bindings.push_back(InputManager::MakeHostMouseButtonKey(button_index)); + m_new_bindings.push_back(InputManager::MakePointerButtonKey(0, button_index)); return true; } - else if (event_type == QEvent::MouseButtonDblClick) + else if (event_type == QEvent::MouseMove) { - // just eat double clicks - return true; + // if we've moved more than a decent distance from the center of the widget, bind it. + // this is so we don't accidentally bind to the mouse if you bump it while reaching for your pad. + static constexpr const s32 THRESHOLD = 50; + const QPoint diff(static_cast(event)->globalPos() - m_input_listen_start_position); + bool has_one = false; + + if (std::abs(diff.x()) >= THRESHOLD) + { + InputBindingKey key(InputManager::MakePointerAxisKey(0, InputPointerAxis::X)); + key.negative = (diff.x() < 0); + m_new_bindings.push_back(key); + has_one = true; + } + if (std::abs(diff.y()) >= THRESHOLD) + { + InputBindingKey key(InputManager::MakePointerAxisKey(0, InputPointerAxis::Y)); + key.negative = (diff.y() < 0); + m_new_bindings.push_back(key); + has_one = true; + } + + if (has_one) + { + setNewBinding(); + stopListeningForInput(); + return true; + } } return false; @@ -240,6 +266,7 @@ void InputBindingWidget::onInputListenTimerTimeout() void InputBindingWidget::startListeningForInput(u32 timeout_in_seconds) { m_new_bindings.clear(); + m_input_listen_start_position = QCursor::pos(); m_input_listen_timer = new QTimer(this); m_input_listen_timer->setSingleShot(false); m_input_listen_timer->start(1000); @@ -252,6 +279,7 @@ void InputBindingWidget::startListeningForInput(u32 timeout_in_seconds) installEventFilter(this); grabKeyboard(); grabMouse(); + setMouseTracking(true); hookInputManager(); } @@ -263,6 +291,7 @@ void InputBindingWidget::stopListeningForInput() std::vector().swap(m_new_bindings); unhookInputManager(); + setMouseTracking(false); releaseMouse(); releaseKeyboard(); removeEventFilter(this); diff --git a/pcsx2-qt/Settings/InputBindingWidget.h b/pcsx2-qt/Settings/InputBindingWidget.h index 3f53b6be6e..2c970e2401 100644 --- a/pcsx2-qt/Settings/InputBindingWidget.h +++ b/pcsx2-qt/Settings/InputBindingWidget.h @@ -73,6 +73,7 @@ protected: std::vector m_new_bindings; QTimer* m_input_listen_timer = nullptr; u32 m_input_listen_remaining_seconds = 0; + QPoint m_input_listen_start_position{}; }; class InputVibrationBindingWidget : public QPushButton diff --git a/pcsx2-qt/Settings/InterfaceSettingsWidget.cpp b/pcsx2-qt/Settings/InterfaceSettingsWidget.cpp index 6da2b571fd..bce2d52ed1 100644 --- a/pcsx2-qt/Settings/InterfaceSettingsWidget.cpp +++ b/pcsx2-qt/Settings/InterfaceSettingsWidget.cpp @@ -117,7 +117,6 @@ InterfaceSettingsWidget::InterfaceSettingsWidget(SettingsDialog* dialog, QWidget m_ui.pauseOnStart->setDisabled(true); m_ui.pauseOnFocusLoss->setDisabled(true); m_ui.disableWindowResizing->setDisabled(true); - m_ui.hideMouseCursor->setDisabled(true); m_ui.language->setDisabled(true); } diff --git a/pcsx2-qt/Settings/InterfaceSettingsWidget.ui b/pcsx2-qt/Settings/InterfaceSettingsWidget.ui index f13a357d5f..df7553b97c 100644 --- a/pcsx2-qt/Settings/InterfaceSettingsWidget.ui +++ b/pcsx2-qt/Settings/InterfaceSettingsWidget.ui @@ -88,7 +88,7 @@ - Hide Mouse Cursor + Hide Cursor In Fullscreen diff --git a/pcsx2/Frontend/ImGuiManager.cpp b/pcsx2/Frontend/ImGuiManager.cpp index 7c76c46e5a..50c7d6d268 100644 --- a/pcsx2/Frontend/ImGuiManager.cpp +++ b/pcsx2/Frontend/ImGuiManager.cpp @@ -19,15 +19,18 @@ #include #include #include +#include #include "fmt/core.h" #include "common/StringUtil.h" +#include "common/Timer.h" #include "imgui.h" #include "Config.h" #include "Counters.h" #include "Frontend/ImGuiManager.h" +#include "Frontend/InputManager.h" #include "GS.h" #include "GS/GS.h" #include "Host.h" @@ -39,10 +42,21 @@ #include "VMManager.h" #endif -static void SetImGuiStyle(); -static bool LoadFontData(); -static void UnloadFontData(); -static bool AddImGuiFonts(); +namespace ImGuiManager +{ + static void SetStyle(); + static void SetKeyMap(); + static bool LoadFontData(); + static void UnloadFontData(); + static bool AddImGuiFonts(); + static ImFont* AddTextFont(float size); + static ImFont* AddFixedFont(float size); + static bool AddIconFonts(float size); + static void AcquirePendingOSDMessages(); + static void DrawOSDMessages(); + static void FormatProcessorStat(std::string& text, double usage, double time); + static void DrawPerformanceOverlay(); +} // namespace ImGuiManager static float s_global_scale = 1.0f; @@ -53,6 +67,17 @@ static std::vector s_standard_font_data; static std::vector s_fixed_font_data; static std::vector s_icon_font_data; +static Common::Timer s_last_render_time; + +#ifdef PCSX2_CORE +// cached copies of WantCaptureKeyboard/Mouse, used to know when to dispatch events +static std::atomic_bool s_imgui_wants_keyboard{false}; +static std::atomic_bool s_imgui_wants_mouse{false}; + +// mapping of host key -> imgui key +static std::unordered_map s_imgui_key_map; +#endif + bool ImGuiManager::Initialize() { if (!LoadFontData()) @@ -62,23 +87,30 @@ bool ImGuiManager::Initialize() } HostDisplay* display = Host::GetHostDisplay(); - - ImGui::CreateContext(); - ImGui::GetIO().IniFilename = nullptr; - s_global_scale = std::max(1.0f, display->GetWindowScale() * static_cast(EmuConfig.GS.OsdScale / 100.0)); - ImGui::GetIO().DisplayFramebufferScale = ImVec2(1, 1); // We already scale things ourselves, this would double-apply scaling - ImGui::GetIO().DisplaySize.x = static_cast(display->GetWindowWidth()); - ImGui::GetIO().DisplaySize.y = static_cast(display->GetWindowHeight()); - ImGui::GetStyle() = ImGuiStyle(); - ImGui::GetStyle().WindowMinSize = ImVec2(1.0f, 1.0f); - SetImGuiStyle(); - ImGui::GetStyle().ScaleAllSizes(s_global_scale); + ImGui::CreateContext(); + + ImGuiIO& io = ImGui::GetIO(); + io.IniFilename = nullptr; +#ifdef PCSX2_CORE + io.BackendFlags |= ImGuiBackendFlags_HasGamepad; + io.BackendUsingLegacyKeyArrays = 0; + io.BackendUsingLegacyNavInputArray = 0; + io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard | ImGuiConfigFlags_NavEnableGamepad; +#endif + + io.DisplayFramebufferScale = ImVec2(1, 1); // We already scale things ourselves, this would double-apply scaling + io.DisplaySize.x = static_cast(display->GetWindowWidth()); + io.DisplaySize.y = static_cast(display->GetWindowHeight()); + + SetKeyMap(); + SetStyle(); if (!display->CreateImGuiContext()) { pxFailRel("Failed to create ImGui device context"); + display->DestroyImGuiContext(); ImGui::DestroyContext(); UnloadFontData(); return false; @@ -93,6 +125,9 @@ bool ImGuiManager::Initialize() return false; } + // don't need the font data anymore, save some memory + ImGui::GetIO().Fonts->ClearTexData(); + NewFrame(); return true; } @@ -104,6 +139,10 @@ void ImGuiManager::Shutdown() display->DestroyImGuiContext(); if (ImGui::GetCurrentContext()) ImGui::DestroyContext(); + + s_standard_font = nullptr; + s_fixed_font = nullptr; + UnloadFontData(); } @@ -139,7 +178,7 @@ void ImGuiManager::UpdateScale() ImGui::GetStyle() = ImGuiStyle(); ImGui::GetStyle().WindowMinSize = ImVec2(1.0f, 1.0f); - SetImGuiStyle(); + SetStyle(); ImGui::GetStyle().ScaleAllSizes(scale); if (!AddImGuiFonts()) @@ -153,14 +192,24 @@ void ImGuiManager::UpdateScale() void ImGuiManager::NewFrame() { + ImGuiIO& io = ImGui::GetIO(); + io.DeltaTime = s_last_render_time.GetTimeSecondsAndReset(); + ImGui::NewFrame(); + +#ifdef PCSX2_CORE + s_imgui_wants_keyboard.store(io.WantCaptureKeyboard, std::memory_order_relaxed); + s_imgui_wants_mouse.store(io.WantCaptureMouse, std::memory_order_release); +#endif } -void SetImGuiStyle() +void ImGuiManager::SetStyle() { - ImGuiStyle* style = &ImGui::GetStyle(); - ImVec4* colors = style->Colors; + ImGuiStyle& style = ImGui::GetStyle(); + style = ImGuiStyle(); + style.WindowMinSize = ImVec2(1.0f, 1.0f); + ImVec4* colors = style.Colors; colors[ImGuiCol_Text] = ImVec4(0.95f, 0.96f, 0.98f, 1.00f); colors[ImGuiCol_TextDisabled] = ImVec4(0.36f, 0.42f, 0.47f, 1.00f); colors[ImGuiCol_WindowBg] = ImVec4(0.11f, 0.15f, 0.17f, 1.00f); @@ -209,9 +258,58 @@ void SetImGuiStyle() colors[ImGuiCol_NavWindowingHighlight] = ImVec4(1.00f, 1.00f, 1.00f, 0.70f); colors[ImGuiCol_NavWindowingDimBg] = ImVec4(0.80f, 0.80f, 0.80f, 0.20f); colors[ImGuiCol_ModalWindowDimBg] = ImVec4(0.80f, 0.80f, 0.80f, 0.35f); + + style.ScaleAllSizes(s_global_scale); } -bool LoadFontData() +void ImGuiManager::SetKeyMap() +{ +#ifdef PCSX2_CORE + struct KeyMapping + { + int index; + const char* name; + const char* alt_name; + }; + + static constexpr KeyMapping mapping[] = {{ImGuiKey_LeftArrow, "Left"}, {ImGuiKey_RightArrow, "Right"}, {ImGuiKey_UpArrow, "Up"}, + {ImGuiKey_DownArrow, "Down"}, {ImGuiKey_PageUp, "PageUp"}, {ImGuiKey_PageDown, "PageDown"}, {ImGuiKey_Home, "Home"}, + {ImGuiKey_End, "End"}, {ImGuiKey_Insert, "Insert"}, {ImGuiKey_Delete, "Delete"}, {ImGuiKey_Backspace, "Backspace"}, + {ImGuiKey_Space, "Space"}, {ImGuiKey_Enter, "Return"}, {ImGuiKey_Escape, "Escape"}, {ImGuiKey_LeftCtrl, "LeftCtrl", "Ctrl"}, + {ImGuiKey_LeftShift, "LeftShift", "Shift"}, {ImGuiKey_LeftAlt, "LeftAlt", "Alt"}, {ImGuiKey_LeftSuper, "LeftSuper", "Super"}, + {ImGuiKey_RightCtrl, "RightCtrl"}, {ImGuiKey_RightShift, "RightShift"}, {ImGuiKey_RightAlt, "RightAlt"}, + {ImGuiKey_RightSuper, "RightSuper"}, {ImGuiKey_Menu, "Menu"}, {ImGuiKey_0, "0"}, {ImGuiKey_1, "1"}, {ImGuiKey_2, "2"}, + {ImGuiKey_3, "3"}, {ImGuiKey_4, "4"}, {ImGuiKey_5, "5"}, {ImGuiKey_6, "6"}, {ImGuiKey_7, "7"}, {ImGuiKey_8, "8"}, {ImGuiKey_9, "9"}, + {ImGuiKey_A, "A"}, {ImGuiKey_B, "B"}, {ImGuiKey_C, "C"}, {ImGuiKey_D, "D"}, {ImGuiKey_E, "E"}, {ImGuiKey_F, "F"}, {ImGuiKey_G, "G"}, + {ImGuiKey_H, "H"}, {ImGuiKey_I, "I"}, {ImGuiKey_J, "J"}, {ImGuiKey_K, "K"}, {ImGuiKey_L, "L"}, {ImGuiKey_M, "M"}, {ImGuiKey_N, "N"}, + {ImGuiKey_O, "O"}, {ImGuiKey_P, "P"}, {ImGuiKey_Q, "Q"}, {ImGuiKey_R, "R"}, {ImGuiKey_S, "S"}, {ImGuiKey_T, "T"}, {ImGuiKey_U, "U"}, + {ImGuiKey_V, "V"}, {ImGuiKey_W, "W"}, {ImGuiKey_X, "X"}, {ImGuiKey_Y, "Y"}, {ImGuiKey_Z, "Z"}, {ImGuiKey_F1, "F1"}, + {ImGuiKey_F2, "F2"}, {ImGuiKey_F3, "F3"}, {ImGuiKey_F4, "F4"}, {ImGuiKey_F5, "F5"}, {ImGuiKey_F6, "F6"}, {ImGuiKey_F7, "F7"}, + {ImGuiKey_F8, "F8"}, {ImGuiKey_F9, "F9"}, {ImGuiKey_F10, "F10"}, {ImGuiKey_F11, "F11"}, {ImGuiKey_F12, "F12"}, + {ImGuiKey_Apostrophe, "Apostrophe"}, {ImGuiKey_Comma, "Comma"}, {ImGuiKey_Minus, "Minus"}, {ImGuiKey_Period, "Period"}, + {ImGuiKey_Slash, "Slash"}, {ImGuiKey_Semicolon, "Semicolon"}, {ImGuiKey_Equal, "Equal"}, {ImGuiKey_LeftBracket, "BracketLeft"}, + {ImGuiKey_Backslash, "Backslash"}, {ImGuiKey_RightBracket, "BracketRight"}, {ImGuiKey_GraveAccent, "QuoteLeft"}, + {ImGuiKey_CapsLock, "CapsLock"}, {ImGuiKey_ScrollLock, "ScrollLock"}, {ImGuiKey_NumLock, "NumLock"}, + {ImGuiKey_PrintScreen, "PrintScreen"}, {ImGuiKey_Pause, "Pause"}, {ImGuiKey_Keypad0, "Keypad0"}, {ImGuiKey_Keypad1, "Keypad1"}, + {ImGuiKey_Keypad2, "Keypad2"}, {ImGuiKey_Keypad3, "Keypad3"}, {ImGuiKey_Keypad4, "Keypad4"}, {ImGuiKey_Keypad5, "Keypad5"}, + {ImGuiKey_Keypad6, "Keypad6"}, {ImGuiKey_Keypad7, "Keypad7"}, {ImGuiKey_Keypad8, "Keypad8"}, {ImGuiKey_Keypad9, "Keypad9"}, + {ImGuiKey_KeypadDecimal, "KeypadPeriod"}, {ImGuiKey_KeypadDivide, "KeypadDivide"}, {ImGuiKey_KeypadMultiply, "KeypadMultiply"}, + {ImGuiKey_KeypadSubtract, "KeypadMinus"}, {ImGuiKey_KeypadAdd, "KeypadPlus"}, {ImGuiKey_KeypadEnter, "KeypadReturn"}, + {ImGuiKey_KeypadEqual, "KeypadEqual"}}; + + s_imgui_key_map.clear(); + for (const KeyMapping& km : mapping) + { + std::optional map(InputManager::ConvertHostKeyboardStringToCode(km.name)); + if (!map.has_value() && km.alt_name) + map = InputManager::ConvertHostKeyboardStringToCode(km.alt_name); + if (map.has_value()) + s_imgui_key_map[map.value()] = km.index; + } +#endif +} + +bool ImGuiManager::LoadFontData() { if (s_standard_font_data.empty()) { @@ -243,14 +341,14 @@ bool LoadFontData() return true; } -void UnloadFontData() +void ImGuiManager::UnloadFontData() { std::vector().swap(s_standard_font_data); std::vector().swap(s_fixed_font_data); std::vector().swap(s_icon_font_data); } -static ImFont* AddTextFont(float size /*= 15.0f*/) +ImFont* ImGuiManager::AddTextFont(float size) { static const ImWchar default_ranges[] = { // Basic Latin + Latin Supplement + Central European diacritics @@ -278,15 +376,15 @@ static ImFont* AddTextFont(float size /*= 15.0f*/) s_standard_font_data.data(), static_cast(s_standard_font_data.size()), size, &cfg, default_ranges); } -static ImFont* AddFixedFont(float size) +ImFont* ImGuiManager::AddFixedFont(float size) { ImFontConfig cfg; cfg.FontDataOwnedByAtlas = false; - return ImGui::GetIO().Fonts->AddFontFromMemoryTTF(s_fixed_font_data.data(), - static_cast(s_fixed_font_data.size()), size, &cfg, nullptr); + return ImGui::GetIO().Fonts->AddFontFromMemoryTTF( + s_fixed_font_data.data(), static_cast(s_fixed_font_data.size()), size, &cfg, nullptr); } -static bool AddIconFonts(float size) +bool ImGuiManager::AddIconFonts(float size) { static const ImWchar range_fa[] = {ICON_MIN_FA, ICON_MAX_FA, 0}; @@ -297,11 +395,11 @@ static bool AddIconFonts(float size) cfg.GlyphMaxAdvanceX = size * 0.75f; cfg.FontDataOwnedByAtlas = false; - return (ImGui::GetIO().Fonts->AddFontFromMemoryTTF(s_icon_font_data.data(), static_cast(s_icon_font_data.size()), - size * 0.75f, &cfg, range_fa) != nullptr); + return (ImGui::GetIO().Fonts->AddFontFromMemoryTTF( + s_icon_font_data.data(), static_cast(s_icon_font_data.size()), size * 0.75f, &cfg, range_fa) != nullptr); } -bool AddImGuiFonts() +bool ImGuiManager::AddImGuiFonts() { const float standard_font_size = std::ceil(15.0f * s_global_scale); @@ -387,7 +485,7 @@ void Host::ClearOSDMessages() s_osd_active_messages.clear(); } -static void AcquirePendingOSDMessages() +void ImGuiManager::AcquirePendingOSDMessages() { std::atomic_thread_fence(std::memory_order_consume); if (s_osd_posted_messages.empty()) @@ -403,10 +501,9 @@ static void AcquirePendingOSDMessages() { OSDMessage& new_msg = s_osd_posted_messages.front(); std::deque::iterator iter; - if (!new_msg.key.empty() && (iter = std::find_if(s_osd_active_messages.begin(), s_osd_active_messages.end(), - [&new_msg](const OSDMessage& other) { - return new_msg.key == other.key; - })) != s_osd_active_messages.end()) + if (!new_msg.key.empty() && + (iter = std::find_if(s_osd_active_messages.begin(), s_osd_active_messages.end(), + [&new_msg](const OSDMessage& other) { return new_msg.key == other.key; })) != s_osd_active_messages.end()) { iter->text = std::move(new_msg.text); iter->duration = new_msg.duration; @@ -426,7 +523,7 @@ static void AcquirePendingOSDMessages() } } -static void DrawOSDMessages() +void ImGuiManager::DrawOSDMessages() { ImFont* const font = ImGui::GetFont(); const float scale = s_global_scale; @@ -469,13 +566,13 @@ static void DrawOSDMessages() ImDrawList* dl = ImGui::GetBackgroundDrawList(); dl->AddRectFilled(pos, ImVec2(pos.x + size.x, pos.y + size.y), IM_COL32(0x21, 0x21, 0x21, alpha), rounding); dl->AddRect(pos, ImVec2(pos.x + size.x, pos.y + size.y), IM_COL32(0x48, 0x48, 0x48, alpha), rounding); - dl->AddText(font, font->FontSize, ImVec2(text_rect.x, text_rect.y), IM_COL32(0xff, 0xff, 0xff, alpha), - msg.text.c_str(), msg.text.c_str() + msg.text.length(), max_width, &text_rect); + dl->AddText(font, font->FontSize, ImVec2(text_rect.x, text_rect.y), IM_COL32(0xff, 0xff, 0xff, alpha), msg.text.c_str(), + msg.text.c_str() + msg.text.length(), max_width, &text_rect); position_y += size.y + spacing; } } -static void FormatProcessorStat(std::string& text, double usage, double time) +void ImGuiManager::FormatProcessorStat(std::string& text, double usage, double time) { // Some values, such as GPU (and even CPU to some extent) can be out of phase with the wall clock, // which the processor time is divided by to get a utilization percentage. Let's clamp it at 100%, @@ -486,7 +583,7 @@ static void FormatProcessorStat(std::string& text, double usage, double time) fmt::format_to(std::back_inserter(text), "{:.1f}% ({:.2f}ms)", usage, time); } -static void DrawPerformanceOverlay() +void ImGuiManager::DrawPerformanceOverlay() { const float scale = s_global_scale; const float shadow_offset = std::ceil(1.0f * scale); @@ -504,14 +601,11 @@ static void DrawPerformanceOverlay() #define DRAW_LINE(font, text, color) \ do \ { \ - text_size = \ - font->CalcTextSizeA(font->FontSize, std::numeric_limits::max(), -1.0f, (text), nullptr, nullptr); \ - dl->AddText( \ - font, font->FontSize, \ + text_size = font->CalcTextSizeA(font->FontSize, std::numeric_limits::max(), -1.0f, (text), nullptr, nullptr); \ + dl->AddText(font, font->FontSize, \ ImVec2(ImGui::GetIO().DisplaySize.x - margin - text_size.x + shadow_offset, position_y + shadow_offset), \ IM_COL32(0, 0, 0, 100), (text)); \ - dl->AddText(font, font->FontSize, ImVec2(ImGui::GetIO().DisplaySize.x - margin - text_size.x, position_y), color, \ - (text)); \ + dl->AddText(font, font->FontSize, ImVec2(ImGui::GetIO().DisplaySize.x - margin - text_size.x, position_y), color, (text)); \ position_y += text_size.y + spacing; \ } while (0) @@ -528,18 +622,20 @@ static void DrawPerformanceOverlay() { switch (PerformanceMetrics::GetInternalFPSMethod()) { - case PerformanceMetrics::InternalFPSMethod::GSPrivilegedRegister: - fmt::format_to(std::back_inserter(text), "G: {:.2f} [P] | V: {:.2f}", PerformanceMetrics::GetInternalFPS(), PerformanceMetrics::GetFPS()); - break; + case PerformanceMetrics::InternalFPSMethod::GSPrivilegedRegister: + fmt::format_to(std::back_inserter(text), "G: {:.2f} [P] | V: {:.2f}", PerformanceMetrics::GetInternalFPS(), + PerformanceMetrics::GetFPS()); + break; - case PerformanceMetrics::InternalFPSMethod::DISPFBBlit: - fmt::format_to(std::back_inserter(text), "G: {:.2f} [B] | V: {:.2f}", PerformanceMetrics::GetInternalFPS(), PerformanceMetrics::GetFPS()); - break; + case PerformanceMetrics::InternalFPSMethod::DISPFBBlit: + fmt::format_to(std::back_inserter(text), "G: {:.2f} [B] | V: {:.2f}", PerformanceMetrics::GetInternalFPS(), + PerformanceMetrics::GetFPS()); + break; - case PerformanceMetrics::InternalFPSMethod::None: - default: - fmt::format_to(std::back_inserter(text), "V: {:.2f}", PerformanceMetrics::GetFPS()); - break; + case PerformanceMetrics::InternalFPSMethod::None: + default: + fmt::format_to(std::back_inserter(text), "V: {:.2f}", PerformanceMetrics::GetFPS()); + break; } first = false; } @@ -651,6 +747,9 @@ static void DrawPerformanceOverlay() void ImGuiManager::RenderOSD() { + // acquire for IO.MousePos. + std::atomic_thread_fence(std::memory_order_acquire); + DrawPerformanceOverlay(); AcquirePendingOSDMessages(); @@ -671,3 +770,96 @@ ImFont* ImGuiManager::GetFixedFont() { return s_fixed_font; } + +#ifdef PCSX2_CORE + +void ImGuiManager::UpdateMousePosition(float x, float y) +{ + if (!ImGui::GetCurrentContext()) + return; + + ImGui::GetIO().MousePos = ImVec2(x, y); + std::atomic_thread_fence(std::memory_order_release); +} + +bool ImGuiManager::ProcessPointerButtonEvent(InputBindingKey key, float value) +{ + if (!ImGui::GetCurrentContext() || key.data >= std::size(ImGui::GetIO().MouseDown)) + return false; + + // still update state anyway + GetMTGS().RunOnGSThread([button = key.data, down = (value != 0.0f)]() { ImGui::GetIO().AddMouseButtonEvent(button, down); }); + + return s_imgui_wants_mouse.load(std::memory_order_acquire); +} + +bool ImGuiManager::ProcessPointerAxisEvent(InputBindingKey key, float value) +{ + if (!ImGui::GetCurrentContext() || value == 0.0f || key.data < static_cast(InputPointerAxis::WheelX)) + return false; + + // still update state anyway + const bool horizontal = (key.data == static_cast(InputPointerAxis::WheelX)); + GetMTGS().RunOnGSThread([wheel_x = horizontal ? value : 0.0f, wheel_y = horizontal ? 0.0f : value]() { + ImGui::GetIO().AddMouseWheelEvent(wheel_x, wheel_y); + }); + + return s_imgui_wants_mouse.load(std::memory_order_acquire); +} + +bool ImGuiManager::ProcessHostKeyEvent(InputBindingKey key, float value) +{ + decltype(s_imgui_key_map)::iterator iter; + if (!ImGui::GetCurrentContext() || (iter = s_imgui_key_map.find(key.data)) == s_imgui_key_map.end()) + return false; + + // still update state anyway + GetMTGS().RunOnGSThread([imkey = iter->second, down = (value != 0.0f)]() { ImGui::GetIO().AddKeyEvent(imkey, down); }); + + return s_imgui_wants_keyboard.load(std::memory_order_acquire); +} + +bool ImGuiManager::ProcessGenericInputEvent(GenericInputBinding key, float value) +{ + static constexpr ImGuiKey key_map[] = { + ImGuiKey_None, // Unknown, + ImGuiKey_GamepadDpadUp, // DPadUp + ImGuiKey_GamepadDpadRight, // DPadRight + ImGuiKey_GamepadDpadLeft, // DPadLeft + ImGuiKey_GamepadDpadDown, // DPadDown + ImGuiKey_None, // LeftStickUp + ImGuiKey_None, // LeftStickRight + ImGuiKey_None, // LeftStickDown + ImGuiKey_None, // LeftStickLeft + ImGuiKey_GamepadL3, // L3 + ImGuiKey_None, // RightStickUp + ImGuiKey_None, // RightStickRight + ImGuiKey_None, // RightStickDown + ImGuiKey_None, // RightStickLeft + ImGuiKey_GamepadR3, // R3 + ImGuiKey_GamepadFaceUp, // Triangle + ImGuiKey_GamepadFaceRight, // Circle + ImGuiKey_GamepadFaceDown, // Cross + ImGuiKey_GamepadFaceLeft, // Square + ImGuiKey_GamepadBack, // Select + ImGuiKey_GamepadStart, // Start + ImGuiKey_None, // System + ImGuiKey_GamepadL1, // L1 + ImGuiKey_GamepadL2, // L2 + ImGuiKey_GamepadR1, // R1 + ImGuiKey_GamepadL2, // R2 + }; + + if (!ImGui::GetCurrentContext() || !s_imgui_wants_keyboard.load(std::memory_order_acquire)) + return false; + + if (static_cast(key) >= std::size(key_map) || key_map[static_cast(key)] == ImGuiKey_None) + return false; + + GetMTGS().RunOnGSThread( + [key = key_map[static_cast(key)], value]() { ImGui::GetIO().AddKeyAnalogEvent(key, (value > 0.0f), value); }); + + return true; +} + +#endif // PCSX2_CORE \ No newline at end of file diff --git a/pcsx2/Frontend/ImGuiManager.h b/pcsx2/Frontend/ImGuiManager.h index 1fb928c9b6..10d931cf8c 100644 --- a/pcsx2/Frontend/ImGuiManager.h +++ b/pcsx2/Frontend/ImGuiManager.h @@ -17,6 +17,9 @@ struct ImFont; +union InputBindingKey; +enum class GenericInputBinding : u8; + namespace ImGuiManager { /// Initializes ImGui, creates fonts, etc. @@ -45,5 +48,25 @@ namespace ImGuiManager /// Returns the fixed-width font for external drawing. ImFont* GetFixedFont(); + +#ifdef PCSX2_CORE + /// Called on the UI or CPU thread in response to mouse movement. + void UpdateMousePosition(float x, float y); + + /// Called on the CPU thread in response to a mouse button press. + /// Returns true if ImGui intercepted the event, and regular handlers should not execute. + bool ProcessPointerButtonEvent(InputBindingKey key, float value); + + /// Called on the CPU thread in response to a mouse wheel movement. + /// Returns true if ImGui intercepted the event, and regular handlers should not execute. + bool ProcessPointerAxisEvent(InputBindingKey key, float value); + + /// Called on the CPU thread in response to a key press. + /// Returns true if ImGui intercepted the event, and regular handlers should not execute. + bool ProcessHostKeyEvent(InputBindingKey key, float value); + + /// Called on the CPU thread when any input event fires. Allows imgui to take over controller navigation. + bool ProcessGenericInputEvent(GenericInputBinding key, float value); +#endif } // namespace ImGuiManager diff --git a/pcsx2/Frontend/InputManager.cpp b/pcsx2/Frontend/InputManager.cpp index 92b8e09e04..0f255dc1af 100644 --- a/pcsx2/Frontend/InputManager.cpp +++ b/pcsx2/Frontend/InputManager.cpp @@ -16,10 +16,14 @@ #include "PrecompiledHeader.h" #include "Frontend/InputManager.h" #include "Frontend/InputSource.h" +#include "Frontend/ImGuiManager.h" #include "PAD/Host/PAD.h" #include "common/StringUtil.h" #include "common/Timer.h" #include "VMManager.h" + +#include "fmt/core.h" + #include #include #include @@ -36,7 +40,7 @@ enum : u32 { MAX_KEYS_PER_BINDING = 4, MAX_MOTORS_PER_PAD = 2, - FIRST_EXTERNAL_INPUT_SOURCE = static_cast(InputSourceType::Mouse) + 1u, + FIRST_EXTERNAL_INPUT_SOURCE = static_cast(InputSourceType::Pointer) + 1u, LAST_EXTERNAL_INPUT_SOURCE = static_cast(InputSourceType::Count), }; @@ -90,10 +94,8 @@ struct PadVibrationBinding // ------------------------------------------------------------------------ namespace InputManager { - static std::optional ParseHostKeyboardKey( - const std::string_view& source, const std::string_view& sub_binding); - static std::optional ParseHostMouseKey( - const std::string_view& source, const std::string_view& sub_binding); + static std::optional ParseHostKeyboardKey(const std::string_view& source, const std::string_view& sub_binding); + static std::optional ParsePointerKey(const std::string_view& source, const std::string_view& sub_binding); static std::vector SplitChord(const std::string_view& binding); static bool SplitBinding(const std::string_view& binding, std::string_view* source, std::string_view* sub_binding); @@ -105,8 +107,10 @@ namespace InputManager static void AddHotkeyBindings(SettingsInterface& si); static void AddPadBindings(SettingsInterface& si, u32 pad, const char* default_type); static void UpdateContinuedVibration(); + static void GenerateRelativeMouseEvents(); static bool DoEventHook(InputBindingKey key, float value); + static bool PreprocessEvent(InputBindingKey key, float value, GenericInputBinding generic_key); } // namespace InputManager // ------------------------------------------------------------------------ @@ -132,6 +136,24 @@ static std::array, static_cast(InputSourceType // ------------------------------------------------------------------------ static const HotkeyInfo* const s_hotkey_list[] = {g_vm_manager_hotkeys, g_gs_hotkeys, g_host_hotkeys}; +// ------------------------------------------------------------------------ +// Tracking host mouse movement and turning into relative events +// 4 axes: pointer left/right, wheel vertical/horizontal. Last/Next/Normalized. +// ------------------------------------------------------------------------ +static constexpr const std::array(InputPointerAxis::Count)> s_pointer_axis_names = { + {"X", "Y", "WheelX", "WheelY"}}; +static constexpr const std::array s_pointer_button_names = {{"LeftButton", "RightButton", "MiddleButton"}}; + +struct PointerAxisState +{ + std::atomic delta; + float last_value; +}; +static std::array(InputPointerAxis::Count)>, InputManager::MAX_POINTER_DEVICES> s_host_pointer_positions; +static std::array(InputPointerAxis::Count)>, InputManager::MAX_POINTER_DEVICES> + s_pointer_state; +static std::array(InputPointerAxis::Count)> s_pointer_axis_scale; + // ------------------------------------------------------------------------ // Binding Parsing // ------------------------------------------------------------------------ @@ -166,8 +188,7 @@ std::vector InputManager::SplitChord(const std::string_view& b return parts; } -bool InputManager::SplitBinding( - const std::string_view& binding, std::string_view* source, std::string_view* sub_binding) +bool InputManager::SplitBinding(const std::string_view& binding, std::string_view* source, std::string_view* sub_binding) { const std::string_view::size_type slash_pos = binding.find('/'); if (slash_pos == std::string_view::npos) @@ -192,9 +213,9 @@ std::optional InputManager::ParseInputBindingKey(const std::str { return ParseHostKeyboardKey(source, sub_binding); } - else if (StringUtil::StartsWith(source, "Mouse")) + else if (StringUtil::StartsWith(source, "Pointer")) { - return ParseHostMouseKey(source, sub_binding); + return ParsePointerKey(source, sub_binding); } else { @@ -241,17 +262,21 @@ std::string InputManager::ConvertInputBindingKeyToString(InputBindingKey key) { const std::optional str(ConvertHostKeyboardCodeToString(key.data)); if (str.has_value() && !str->empty()) - return StringUtil::StdStringFromFormat("Keyboard/%s", str->c_str()); + return fmt::format("Keyboard/{}", str->c_str()); } - else if (key.source_type == InputSourceType::Mouse) + else if (key.source_type == InputSourceType::Pointer) { - if (key.source_subtype == InputSubclass::MouseButton) - return StringUtil::StdStringFromFormat("Mouse%u/Button%u", key.source_index, key.data); - else if (key.source_subtype == InputSubclass::MousePointer) - return StringUtil::StdStringFromFormat("Mouse%u/Pointer%u", key.source_index, key.data); - else if (key.source_subtype == InputSubclass::MouseWheel) - return StringUtil::StdStringFromFormat( - "Mouse%u/Wheel%u%c", key.source_index, key.data, key.negative ? '-' : '+'); + if (key.source_subtype == InputSubclass::PointerButton) + { + if (key.data < s_pointer_button_names.size()) + return fmt::format("Pointer-{}/{}", u32{key.source_index}, s_pointer_button_names[key.data]); + else + return fmt::format("Pointer-{}/Button{}", u32{key.source_index}, key.data); + } + else if (key.source_subtype == InputSubclass::PointerAxis) + { + return fmt::format("Pointer-{}/{}{:c}", u32{key.source_index}, s_pointer_axis_names[key.data], key.negative ? '-' : '+'); + } } else if (key.source_type < InputSourceType::Count && s_input_sources[static_cast(key.source_type)]) { @@ -335,21 +360,23 @@ InputBindingKey InputManager::MakeHostKeyboardKey(s32 key_code) return key; } -InputBindingKey InputManager::MakeHostMouseButtonKey(s32 button_index) +InputBindingKey InputManager::MakePointerButtonKey(u32 index, u32 button_index) { InputBindingKey key = {}; - key.source_type = InputSourceType::Mouse; - key.source_subtype = InputSubclass::MouseButton; - key.data = static_cast(button_index); + key.source_index = index; + key.source_type = InputSourceType::Pointer; + key.source_subtype = InputSubclass::PointerButton; + key.data = button_index; return key; } -InputBindingKey InputManager::MakeHostMouseWheelKey(s32 axis_index) +InputBindingKey InputManager::MakePointerAxisKey(u32 index, InputPointerAxis axis) { InputBindingKey key = {}; - key.source_type = InputSourceType::Mouse; - key.source_subtype = InputSubclass::MouseWheel; - key.data = static_cast(axis_index); + key.data = static_cast(axis); + key.source_index = index; + key.source_type = InputSourceType::Pointer; + key.source_subtype = InputSubclass::PointerAxis; return key; } @@ -389,8 +416,7 @@ std::optional InputManager::ParseInputSourceString(const std::s return std::nullopt; } -std::optional InputManager::ParseHostKeyboardKey( - const std::string_view& source, const std::string_view& sub_binding) +std::optional InputManager::ParseHostKeyboardKey(const std::string_view& source, const std::string_view& sub_binding) { if (source != "Keyboard") return std::nullopt; @@ -405,14 +431,15 @@ std::optional InputManager::ParseHostKeyboardKey( return key; } -std::optional InputManager::ParseHostMouseKey( - const std::string_view& source, const std::string_view& sub_binding) +std::optional InputManager::ParsePointerKey(const std::string_view& source, const std::string_view& sub_binding) { - if (source != "Mouse") + const std::optional pointer_index = StringUtil::FromChars(source.substr(8)); + if (!pointer_index.has_value() || pointer_index.value() < 0) return std::nullopt; InputBindingKey key = {}; - key.source_type = InputSourceType::Mouse; + key.source_type = InputSourceType::Pointer; + key.source_index = static_cast(pointer_index.value()); if (StringUtil::StartsWith(sub_binding, "Button")) { @@ -420,15 +447,41 @@ std::optional InputManager::ParseHostMouseKey( if (!button_number.has_value() || button_number.value() < 0) return std::nullopt; - key.source_subtype = InputSubclass::MouseButton; + key.source_subtype = InputSubclass::PointerButton; key.data = static_cast(button_number.value()); - } - else - { - return std::nullopt; + return key; } - return key; + for (u32 i = 0; i < s_pointer_axis_names.size(); i++) + { + if (StringUtil::StartsWith(sub_binding, s_pointer_axis_names[i])) + { + key.source_subtype = InputSubclass::PointerAxis; + key.data = i; + + const std::string_view dir_part(sub_binding.substr(std::strlen(s_pointer_axis_names[i]))); + if (dir_part == "+") + key.negative = false; + else if (dir_part == "-") + key.negative = true; + else + return std::nullopt; + + return key; + } + } + + for (u32 i = 0; i < s_pointer_button_names.size(); i++) + { + if (sub_binding == s_pointer_button_names[i]) + { + key.source_subtype = InputSubclass::PointerButton; + key.data = i; + return key; + } + } + + return std::nullopt; } // ------------------------------------------------------------------------ @@ -484,17 +537,16 @@ void InputManager::AddPadBindings(SettingsInterface& si, u32 pad_index, const ch if (!bindings.empty()) { // we use axes for all pad bindings to simplify things, and because they are pressure sensitive - AddBindings(bindings, InputAxisEventHandler{[pad_index, bind_index, bind_names](float value) { - PAD::SetControllerState(pad_index, bind_index, value); - }}); + AddBindings(bindings, InputAxisEventHandler{[pad_index, bind_index, bind_names]( + float value) { PAD::SetControllerState(pad_index, bind_index, value); }}); } } } for (u32 macro_button_index = 0; macro_button_index < PAD::NUM_MACRO_BUTTONS_PER_CONTROLLER; macro_button_index++) { - const std::vector bindings(si.GetStringList(section.c_str(), - StringUtil::StdStringFromFormat("Macro%u", macro_button_index + 1).c_str())); + const std::vector bindings( + si.GetStringList(section.c_str(), StringUtil::StdStringFromFormat("Macro%u", macro_button_index + 1).c_str())); if (!bindings.empty()) { AddBindings(bindings, InputButtonEventHandler{[pad_index, macro_button_index](bool state) { @@ -547,14 +599,30 @@ bool InputManager::HasAnyBindingsForKey(InputBindingKey key) return (s_binding_map.find(key.MaskDirection()) != s_binding_map.end()); } +bool InputManager::HasAnyBindingsForSource(InputBindingKey key) +{ + std::unique_lock lock(s_binding_map_write_lock); + for (const auto& it : s_binding_map) + { + const InputBindingKey& okey = it.first; + if (okey.source_type == key.source_type && okey.source_index == key.source_index && + okey.source_subtype == key.source_subtype) + { + return true; + } + } + + return false; +} + bool InputManager::IsAxisHandler(const InputEventHandler& handler) { return std::holds_alternative(handler); } -bool InputManager::InvokeEvents(InputBindingKey key, float value) +bool InputManager::InvokeEvents(InputBindingKey key, float value, GenericInputBinding generic_key) { - if (DoEventHook(key, value)) + if (PreprocessEvent(key, value, generic_key)) return true; // find all the bindings associated with this key @@ -628,8 +696,7 @@ bool InputManager::InvokeEvents(InputBindingKey key, float value) binding->current_mask = new_mask; // invert if we're negative, since the handler expects 0..1 - const float value_to_pass = (negative ? ((value < 0.0f) ? -value : 0.0f) : (value > 0.0f) ? value : - 0.0f); + const float value_to_pass = (negative ? ((value < 0.0f) ? -value : 0.0f) : (value > 0.0f) ? value : 0.0f); // axes are fired regardless of a state change, unless they're zero // (but going from not-zero to zero will still fire, because of the full state) @@ -654,6 +721,89 @@ bool InputManager::InvokeEvents(InputBindingKey key, float value) return true; } +bool InputManager::PreprocessEvent(InputBindingKey key, float value, GenericInputBinding generic_key) +{ + if (DoEventHook(key, value)) + return true; + + // does imgui want the event? + if (key.source_type == InputSourceType::Keyboard) + { + if (ImGuiManager::ProcessHostKeyEvent(key, value)) + return true; + } + else if (key.source_type == InputSourceType::Pointer && key.source_subtype == InputSubclass::PointerButton) + { + if (ImGuiManager::ProcessPointerButtonEvent(key, value)) + return true; + } + else if (generic_key != GenericInputBinding::Unknown) + { + if (ImGuiManager::ProcessGenericInputEvent(generic_key, value)) + return true; + } + + return false; +} + +void InputManager::GenerateRelativeMouseEvents() +{ + for (u32 device = 0; device < MAX_POINTER_DEVICES; device++) + { + for (u32 axis = 0; axis < static_cast(static_cast(InputPointerAxis::Count)); axis++) + { + PointerAxisState& state = s_pointer_state[device][axis]; + const float delta = static_cast(state.delta.exchange(0, std::memory_order_acquire)) / 65536.0f; + const float unclamped_value = delta * s_pointer_axis_scale[axis]; + + const InputBindingKey key(MakePointerAxisKey(device, static_cast(axis))); + if (axis >= static_cast(InputPointerAxis::WheelX) && ImGuiManager::ProcessPointerAxisEvent(key, unclamped_value)) + continue; + + const float value = std::clamp(unclamped_value, -1.0f, 1.0f); + if (value != state.last_value) + { + state.last_value = value; + InvokeEvents(key, value, GenericInputBinding::Unknown); + } + } + } +} + +void InputManager::UpdatePointerAbsolutePosition(u32 index, float x, float y) +{ + const float dx = x - std::exchange(s_host_pointer_positions[index][static_cast(InputPointerAxis::X)], x); + const float dy = y - std::exchange(s_host_pointer_positions[index][static_cast(InputPointerAxis::Y)], y); + + if (dx != 0.0f) + UpdatePointerRelativeDelta(index, InputPointerAxis::X, dx); + if (dy != 0.0f) + UpdatePointerRelativeDelta(index, InputPointerAxis::Y, dy); + + ImGuiManager::UpdateMousePosition(x, y); +} + +void InputManager::UpdatePointerRelativeDelta(u32 index, InputPointerAxis axis, float d, bool raw_input) +{ + s_pointer_state[index][static_cast(axis)].delta.fetch_add(static_cast(d * 65536.0f), std::memory_order_release); +} + +bool InputManager::HasPointerAxisBinds() +{ + std::unique_lock lock(s_binding_map_write_lock); + for (const auto& it : s_binding_map) + { + const InputBindingKey& key = it.first; + if (key.source_type == InputSourceType::Pointer && key.source_subtype == InputSubclass::PointerAxis && + key.data >= static_cast(InputPointerAxis::X) && key.data <= static_cast(InputPointerAxis::Y)) + { + return true; + } + } + + return false; +} + // ------------------------------------------------------------------------ // Vibration // ------------------------------------------------------------------------ @@ -684,7 +834,8 @@ void InputManager::SetPadVibrationIntensity(u32 pad_index, float large_or_single { // both motors are bound to the same source, do an optimal update large_motor.last_update_time = Common::Timer::GetCurrentValue(); - large_motor.source->UpdateMotorState(large_motor.binding, small_motor.binding, large_or_single_motor_intensity, small_motor_intensity); + large_motor.source->UpdateMotorState( + large_motor.binding, small_motor.binding, large_or_single_motor_intensity, small_motor_intensity); } else { @@ -801,7 +952,11 @@ bool InputManager::DoEventHook(InputBindingKey key, float value) return false; const InputInterceptHook::CallbackResult action = m_event_intercept_callback(key, value); - return (action == InputInterceptHook::CallbackResult::StopProcessingEvent); + if (action >= InputInterceptHook::CallbackResult::RemoveHookAndStopProcessingEvent) + m_event_intercept_callback = {}; + + return (action == InputInterceptHook::CallbackResult::RemoveHookAndStopProcessingEvent || + action == InputInterceptHook::CallbackResult::StopProcessingEvent); } // ------------------------------------------------------------------------ @@ -825,6 +980,17 @@ void InputManager::ReloadBindings(SettingsInterface& si, SettingsInterface& bind // falling back to the base configuration. for (u32 pad = 0; pad < PAD::NUM_CONTROLLER_PORTS; pad++) AddPadBindings(binding_si, pad, PAD::GetDefaultPadType(pad)); + + for (u32 axis = 0; axis < static_cast(InputPointerAxis::Count); axis++) + { + // From lilypad: 1 mouse pixel = 1/8th way down. + const float default_scale = (axis <= static_cast(InputPointerAxis::Y)) ? 8.0f : 1.0f; + const float invert = + si.GetBoolValue("Pad", fmt::format("Pointer{}Invert", s_pointer_axis_names[axis]).c_str(), false) ? -1.0f : 1.0f; + s_pointer_axis_scale[axis] = + invert / + std::max(si.GetFloatValue("Pad", fmt::format("Pointer{}Scale", s_pointer_axis_names[axis]).c_str(), default_scale), 1.0f); + } } // ------------------------------------------------------------------------ @@ -851,6 +1017,8 @@ void InputManager::PollSources() s_input_sources[i]->PollEvents(); } + GenerateRelativeMouseEvents(); + if (VMManager::GetState() == VMState::Running && !s_pad_vibration_array.empty()) UpdateContinuedVibration(); } @@ -953,7 +1121,8 @@ GenericInputBindingMapping InputManager::GetGenericBindingMapping(const std::str } template -static void UpdateInputSourceState(SettingsInterface& si, std::unique_lock& settings_lock, InputSourceType type, bool default_state) +static void UpdateInputSourceState( + SettingsInterface& si, std::unique_lock& settings_lock, InputSourceType type, bool default_state) { const bool enabled = si.GetBoolValue("InputSources", InputManager::InputSourceToString(type), default_state); if (enabled) diff --git a/pcsx2/Frontend/InputManager.h b/pcsx2/Frontend/InputManager.h index ce4e000297..bf4aa5933e 100644 --- a/pcsx2/Frontend/InputManager.h +++ b/pcsx2/Frontend/InputManager.h @@ -28,7 +28,7 @@ enum class InputSourceType : u32 { Keyboard, - Mouse, + Pointer, #ifdef _WIN32 //DInput, XInput, @@ -44,9 +44,8 @@ enum class InputSubclass : u32 { None = 0, - MouseButton = 0, - MousePointer = 1, - MouseWheel = 2, + PointerButton = 0, + PointerAxis = 1, ControllerButton = 0, ControllerAxis = 1, @@ -102,7 +101,9 @@ struct InputInterceptHook enum class CallbackResult { StopProcessingEvent, - ContinueProcessingEvent + ContinueProcessingEvent, + RemoveHookAndStopProcessingEvent, + RemoveHookAndContinueProcessingEvent, }; using Callback = std::function; @@ -114,19 +115,24 @@ struct HotkeyInfo const char* name; const char* category; const char* display_name; - void(*handler)(bool pressed); + void (*handler)(bool pressed); }; #define DECLARE_HOTKEY_LIST(name) extern const HotkeyInfo name[] #define BEGIN_HOTKEY_LIST(name) const HotkeyInfo name[] = { #define DEFINE_HOTKEY(name, category, display_name, handler) {(name), (category), (display_name), (handler)}, -#define END_HOTKEY_LIST() {nullptr, nullptr, nullptr, nullptr} }; +#define END_HOTKEY_LIST() \ + { \ + nullptr, nullptr, nullptr, nullptr \ + } \ + } \ + ; DECLARE_HOTKEY_LIST(g_vm_manager_hotkeys); DECLARE_HOTKEY_LIST(g_gs_hotkeys); DECLARE_HOTKEY_LIST(g_host_hotkeys); /// Generic input bindings. These roughly match a DualShock 4 or XBox One controller. -/// They are used for automatic binding to PS2 controller types. +/// They are used for automatic binding to PS2 controller types, and for big picture mode navigation. enum class GenericInputBinding : u8 { Unknown, @@ -169,13 +175,26 @@ enum class GenericInputBinding : u8 }; using GenericInputBindingMapping = std::vector>; +/// Host mouse relative axes are X, Y, wheel horizontal, wheel vertical. +enum class InputPointerAxis : u8 +{ + X, + Y, + WheelX, + WheelY, + Count +}; + /// External input source class. class InputSource; namespace InputManager { /// Minimum interval between vibration updates when the effect is continuous. - static constexpr double VIBRATION_UPDATE_INTERVAL_SECONDS = 0.5; // 500ms + static constexpr double VIBRATION_UPDATE_INTERVAL_SECONDS = 0.5; // 500ms + + /// Maximum number of host mouse devices. + static constexpr u32 MAX_POINTER_DEVICES = 1; /// Returns a pointer to the external input source class, if present. InputSource* GetInputSourceInterface(InputSourceType type); @@ -196,10 +215,11 @@ namespace InputManager InputBindingKey MakeHostKeyboardKey(s32 key_code); /// Creates a key for a host-specific button. - InputBindingKey MakeHostMouseButtonKey(s32 button_index); + InputBindingKey MakePointerButtonKey(u32 index, u32 button_index); - /// Creates a key for a host-specific mouse wheel axis (0 = vertical, 1 = horizontal). - InputBindingKey MakeHostMouseWheelKey(s32 axis_index); + /// Creates a key for a host-specific mouse relative event + /// (axis 0 = horizontal, 1 = vertical, 2 = wheel horizontal, 3 = wheel vertical). + InputBindingKey MakePointerAxisKey(u32 index, InputPointerAxis axis); /// Parses an input binding key string. std::optional ParseInputBindingKey(const std::string_view& binding); @@ -235,12 +255,16 @@ namespace InputManager void PollSources(); /// Returns true if any bindings exist for the specified key. - /// This is the only function which can be safely called on another thread. + /// Can be safely called on another thread. bool HasAnyBindingsForKey(InputBindingKey key); + /// Returns true if any bindings exist for the specified source + index. + /// Can be safely called on another thread. + bool HasAnyBindingsForSource(InputBindingKey key); + /// Updates internal state for any binds for this key, and fires callbacks as needed. /// Returns true if anything was bound to this key, otherwise false. - bool InvokeEvents(InputBindingKey key, float value); + bool InvokeEvents(InputBindingKey key, float value, GenericInputBinding generic_key = GenericInputBinding::Unknown); /// Sets a hook which can be used to intercept events before they're processed by the normal bindings. /// This is typically used when binding new controls to detect what gets pressed. @@ -259,6 +283,15 @@ namespace InputManager /// Zeros all vibration intensities. Call when pausing. /// The pad vibration state will internally remain, so that when emulation is unpaused, the effect resumes. void PauseVibration(); + + /// Updates absolute pointer position. Can call from UI thread, use when the host only reports absolute coordinates. + void UpdatePointerAbsolutePosition(u32 index, float x, float y); + + /// Updates relative pointer position. Can call from the UI thread, use when host supports relative coordinate reporting. + void UpdatePointerRelativeDelta(u32 index, InputPointerAxis axis, float d, bool raw_input = false); + + /// Returns true if any bindings are present which require relative mouse movement. + bool HasPointerAxisBinds(); } // namespace InputManager namespace Host @@ -268,4 +301,4 @@ namespace Host /// Called when an input device is disconnected. void OnInputDeviceDisconnected(const std::string_view& identifier); -} +} // namespace Host diff --git a/pcsx2/Frontend/SDLInputSource.cpp b/pcsx2/Frontend/SDLInputSource.cpp index 27d61ebaa6..9c74576a4b 100644 --- a/pcsx2/Frontend/SDLInputSource.cpp +++ b/pcsx2/Frontend/SDLInputSource.cpp @@ -498,7 +498,10 @@ bool SDLInputSource::HandleControllerButtonEvent(const SDL_ControllerButtonEvent return false; const InputBindingKey key(MakeGenericControllerButtonKey(InputSourceType::SDL, it->player_id, ev->button)); - return InputManager::InvokeEvents(key, (ev->state == SDL_PRESSED) ? 1.0f : 0.0f); + const GenericInputBinding generic_key = (ev->button < std::size(s_sdl_generic_binding_button_mapping)) ? + s_sdl_generic_binding_button_mapping[ev->button] : + GenericInputBinding::Unknown; + return InputManager::InvokeEvents(key, (ev->state == SDL_PRESSED) ? 1.0f : 0.0f, generic_key); } std::vector SDLInputSource::EnumerateMotors() diff --git a/pcsx2/Frontend/XInputSource.cpp b/pcsx2/Frontend/XInputSource.cpp index d60b465192..96d2a3ea1b 100644 --- a/pcsx2/Frontend/XInputSource.cpp +++ b/pcsx2/Frontend/XInputSource.cpp @@ -29,7 +29,7 @@ const char* XInputSource::s_axis_names[XInputSource::NUM_AXES] = { "LeftTrigger", // AXIS_TRIGGERLEFT "RightTrigger", // AXIS_TRIGGERRIGHT }; -static const GenericInputBinding s_sdl_generic_binding_axis_mapping[][2] = { +static const GenericInputBinding s_xinput_generic_binding_axis_mapping[][2] = { {GenericInputBinding::LeftStickLeft, GenericInputBinding::LeftStickRight}, // AXIS_LEFTX {GenericInputBinding::LeftStickUp, GenericInputBinding::LeftStickDown}, // AXIS_LEFTY {GenericInputBinding::RightStickLeft, GenericInputBinding::RightStickRight}, // AXIS_RIGHTX @@ -72,7 +72,7 @@ const u16 XInputSource::s_button_masks[XInputSource::NUM_BUTTONS] = { XINPUT_GAMEPAD_Y, 0x400, // XINPUT_GAMEPAD_GUIDE }; -static const GenericInputBinding s_sdl_generic_binding_button_mapping[] = { +static const GenericInputBinding s_xinput_generic_binding_button_mapping[] = { GenericInputBinding::DPadUp, // XINPUT_GAMEPAD_DPAD_UP GenericInputBinding::DPadDown, // XINPUT_GAMEPAD_DPAD_DOWN GenericInputBinding::DPadLeft, // XINPUT_GAMEPAD_DPAD_LEFT @@ -347,19 +347,19 @@ bool XInputSource::GetGenericBindingMapping(const std::string_view& device, Gene // assume all buttons are present. const s32 pid = player_id.value(); - for (u32 i = 0; i < std::size(s_sdl_generic_binding_axis_mapping); i++) + for (u32 i = 0; i < std::size(s_xinput_generic_binding_axis_mapping); i++) { - const GenericInputBinding negative = s_sdl_generic_binding_axis_mapping[i][0]; - const GenericInputBinding positive = s_sdl_generic_binding_axis_mapping[i][1]; + const GenericInputBinding negative = s_xinput_generic_binding_axis_mapping[i][0]; + const GenericInputBinding positive = s_xinput_generic_binding_axis_mapping[i][1]; if (negative != GenericInputBinding::Unknown) mapping->emplace_back(negative, StringUtil::StdStringFromFormat("XInput-%d/-%s", pid, s_axis_names[i])); if (positive != GenericInputBinding::Unknown) mapping->emplace_back(positive, StringUtil::StdStringFromFormat("XInput-%d/+%s", pid, s_axis_names[i])); } - for (u32 i = 0; i < std::size(s_sdl_generic_binding_button_mapping); i++) + for (u32 i = 0; i < std::size(s_xinput_generic_binding_button_mapping); i++) { - const GenericInputBinding binding = s_sdl_generic_binding_button_mapping[i]; + const GenericInputBinding binding = s_xinput_generic_binding_button_mapping[i]; if (binding != GenericInputBinding::Unknown) mapping->emplace_back(binding, StringUtil::StdStringFromFormat("XInput-%d/%s", pid, s_button_names[i])); } @@ -435,9 +435,12 @@ void XInputSource::CheckForStateChanges(u32 index, const XINPUT_STATE& new_state const u16 button_mask = s_button_masks[button]; if ((old_button_bits & button_mask) != (new_button_bits & button_mask)) { + const GenericInputBinding generic_key = (button < std::size(s_xinput_generic_binding_button_mapping)) ? + s_xinput_generic_binding_button_mapping[button] : GenericInputBinding::Unknown; + const float value = ((new_button_bits & button_mask) != 0) ? 1.0f : 0.0f; InputManager::InvokeEvents( MakeGenericControllerButtonKey(InputSourceType::XInput, index, button), - (new_button_bits & button_mask) != 0); + value, generic_key); } } } diff --git a/pcsx2/PAD/Host/PAD.cpp b/pcsx2/PAD/Host/PAD.cpp index b7a7f2daa4..f87eed43d7 100644 --- a/pcsx2/PAD/Host/PAD.cpp +++ b/pcsx2/PAD/Host/PAD.cpp @@ -237,8 +237,13 @@ void PAD::SetDefaultConfig(SettingsInterface& si) si.SetBoolValue("InputSources", "SDL", true); si.SetBoolValue("InputSources", "SDLControllerEnhancedMode", false); si.SetBoolValue("InputSources", "XInput", false); + si.SetBoolValue("InputSources", "RawInput", false); si.SetBoolValue("Pad", "MultitapPort1", false); si.SetBoolValue("Pad", "MultitapPort2", false); + si.SetFloatValue("Pad", "PointerXScale", 8.0f); + si.SetFloatValue("Pad", "PointerYScale", 8.0f); + si.SetBoolValue("Pad", "PointerXInvert", false); + si.SetBoolValue("Pad", "PointerYInvert", false); // PCSX2 Controller Settings - Controller 1 / Controller 2 / ... // Use the automapper to set this up. diff --git a/pcsx2/pcsx2core.vcxproj b/pcsx2/pcsx2core.vcxproj index 8b28f463f2..1823645e24 100644 --- a/pcsx2/pcsx2core.vcxproj +++ b/pcsx2/pcsx2core.vcxproj @@ -809,4 +809,4 @@ - + \ No newline at end of file