Qt: Implement mouse->controller binding

Also implements forwarding mouse events to imgui.
This commit is contained in:
Connor McLaughlin 2022-05-15 18:15:27 +10:00 committed by refractionpcsx2
parent 7b3847cc5c
commit 470365644f
23 changed files with 990 additions and 219 deletions

View File

@ -33,7 +33,9 @@
#include <QtGui/QWindowStateChangeEvent>
#include <cmath>
#if !defined(_WIN32) && !defined(APPLE)
#if defined(_WIN32)
#include "common/RedtapeWindows.h"
#elif !defined(APPLE)
#include <qpa/qplatformnativeinterface.h>
#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<WindowInfo> 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<HWND>(winId()), &rc))
ClipCursor(&rc);
}
else if (m_relative_mouse_enabled)
{
RECT rc;
if (GetWindowRect(reinterpret_cast<HWND>(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<float>(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<int>(static_cast<qreal>(mouse_pos.x()) * dpr);
const int scaled_y = static_cast<int>(static_cast<qreal>(mouse_pos.y()) * dpr);
windowMouseMoveEvent(scaled_x, scaled_y);
const float scaled_x = static_cast<float>(static_cast<qreal>(mouse_pos.x()) * dpr);
const float scaled_y = static_cast<float>(static_cast<qreal>(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<float>(mouse_pos.x() - m_relative_mouse_center_pos.x());
dy = static_cast<float>(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<float>(mouse_pos.x - m_relative_mouse_center_pos.x());
dy = static_cast<float>(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<u32>(static_cast<const QMouseEvent*>(event)->button())))
emit windowMouseButtonEvent(static_cast<int>(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<float>(pressed));
});
}
// don't toggle fullscreen when we're bound.. that wouldn't end well.
if (event->type() == QEvent::MouseButtonDblClick &&
static_cast<const QMouseEvent*>(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<QWheelEvent*>(event);
emit windowMouseWheelEvent(wheel_event->angleDelta());
// wheel delta is 120 as in winapi
const QPoint delta_angle(static_cast<QWheelEvent*>(event)->angleDelta());
constexpr float DELTA = 120.0f;
const float dx = std::clamp(static_cast<float>(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<float>(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;
}

View File

@ -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<WindowInfo> 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<int> m_keys_pressed_with_modifiers;
u32 m_last_window_width = 0;

View File

@ -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);

View File

@ -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;

View File

@ -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);

View File

@ -93,6 +93,7 @@ public Q_SLOTS:
void runOnUIThread(const std::function<void()>& 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();

View File

@ -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();
}

View File

@ -7,7 +7,7 @@
<x>0</x>
<y>0</y>
<width>900</width>
<height>500</height>
<height>675</height>
</rect>
</property>
<property name="windowTitle">
@ -42,20 +42,20 @@
</property>
</widget>
</item>
<item row="2" column="0" colspan="2">
<widget class="QCheckBox" name="enableSDLEnhancedMode">
<property name="text">
<string>DualShock 4 / DualSense Enhanced Mode</string>
</property>
</widget>
</item>
<item row="1" column="0" colspan="2">
<item row="1" column="0">
<widget class="QCheckBox" name="enableSDLSource">
<property name="text">
<string>Enable SDL Input Source</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QCheckBox" name="enableSDLEnhancedMode">
<property name="text">
<string>DualShock 4 / DualSense Enhanced Mode</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
@ -86,6 +86,141 @@
</widget>
</item>
<item row="2" column="0">
<widget class="QGroupBox" name="groupBox_5">
<property name="title">
<string>Mouse/Pointer Source</string>
</property>
<layout class="QGridLayout" name="gridLayout_6">
<item row="2" column="1">
<widget class="QSlider" name="pointerYScale">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>150</width>
<height>0</height>
</size>
</property>
<property name="minimum">
<number>1</number>
</property>
<property name="maximum">
<number>30</number>
</property>
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
</widget>
</item>
<item row="0" column="0" colspan="4">
<widget class="QLabel" name="label_7">
<property name="text">
<string>Using raw input improves precision when you bind controller sticks to the mouse pointer. Also enables multiple mice to be used.</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item row="2" column="3">
<widget class="QCheckBox" name="pointerYInvert">
<property name="text">
<string>Invert</string>
</property>
</widget>
</item>
<item row="1" column="2">
<widget class="QLabel" name="pointerXScaleLabel">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>20</width>
<height>0</height>
</size>
</property>
<property name="text">
<string>10</string>
</property>
</widget>
</item>
<item row="1" column="3">
<widget class="QCheckBox" name="pointerXInvert">
<property name="text">
<string>Invert</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_6">
<property name="text">
<string>Vertical Sensitivity:</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QSlider" name="pointerXScale">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>150</width>
<height>0</height>
</size>
</property>
<property name="minimum">
<number>1</number>
</property>
<property name="maximum">
<number>30</number>
</property>
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
</widget>
</item>
<item row="2" column="2">
<widget class="QLabel" name="pointerYScaleLabel">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>20</width>
<height>0</height>
</size>
</property>
<property name="text">
<string>10</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_5">
<property name="text">
<string>Horizontal Sensitivity:</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item row="3" column="0">
<widget class="QGroupBox" name="groupBox_4">
<property name="title">
<string>Controller Multitap</string>
@ -101,14 +236,14 @@
</property>
</widget>
</item>
<item row="1" column="0" colspan="2">
<item row="1" column="0">
<widget class="QCheckBox" name="multitapPort1">
<property name="text">
<string>Multitap on Console Port 1</string>
</property>
</widget>
</item>
<item row="2" column="0" colspan="2">
<item row="1" column="1">
<widget class="QCheckBox" name="multitapPort2">
<property name="text">
<string>Multitap on Console Port 2</string>
@ -118,7 +253,7 @@
</layout>
</widget>
</item>
<item row="3" column="0">
<item row="4" column="0">
<widget class="QGroupBox" name="profileSettings">
<property name="title">
<string>Profile Settings</string>
@ -144,7 +279,7 @@
</layout>
</widget>
</item>
<item row="4" column="0">
<item row="5" column="0">
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
@ -157,7 +292,7 @@
</property>
</spacer>
</item>
<item row="0" column="1" rowspan="5">
<item row="0" column="1" rowspan="6">
<widget class="QGroupBox" name="groupBox_3">
<property name="title">
<string>Detected Devices</string>

View File

@ -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);

View File

@ -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<u32>(static_cast<const QMouseEvent*>(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<QMouseEvent*>(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);
}

View File

@ -68,4 +68,5 @@ protected:
QTimer* m_input_listen_timer = nullptr;
u32 m_input_listen_remaining_seconds = 0;
QPoint m_input_listen_start_position{};
};

View File

@ -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<u32>(static_cast<const QMouseEvent*>(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<QMouseEvent*>(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<InputBindingKey>().swap(m_new_bindings);
unhookInputManager();
setMouseTracking(false);
releaseMouse();
releaseKeyboard();
removeEventFilter(this);

View File

@ -73,6 +73,7 @@ protected:
std::vector<InputBindingKey> 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

View File

@ -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);
}

View File

@ -88,7 +88,7 @@
<item row="1" column="1">
<widget class="QCheckBox" name="hideMouseCursor">
<property name="text">
<string>Hide Mouse Cursor</string>
<string>Hide Cursor In Fullscreen</string>
</property>
</widget>
</item>

View File

@ -19,15 +19,18 @@
#include <cmath>
#include <deque>
#include <mutex>
#include <unordered_map>
#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<u8> s_standard_font_data;
static std::vector<u8> s_fixed_font_data;
static std::vector<u8> 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<u32, ImGuiKey> 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<float>(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<float>(display->GetWindowWidth());
ImGui::GetIO().DisplaySize.y = static_cast<float>(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<float>(display->GetWindowWidth());
io.DisplaySize.y = static_cast<float>(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<u32> 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<u8>().swap(s_standard_font_data);
std::vector<u8>().swap(s_fixed_font_data);
std::vector<u8>().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<int>(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<int>(s_fixed_font_data.size()), size, &cfg, nullptr);
return ImGui::GetIO().Fonts->AddFontFromMemoryTTF(
s_fixed_font_data.data(), static_cast<int>(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<int>(s_icon_font_data.size()),
size * 0.75f, &cfg, range_fa) != nullptr);
return (ImGui::GetIO().Fonts->AddFontFromMemoryTTF(
s_icon_font_data.data(), static_cast<int>(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<OSDMessage>::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<float>::max(), -1.0f, (text), nullptr, nullptr); \
dl->AddText( \
font, font->FontSize, \
text_size = font->CalcTextSizeA(font->FontSize, std::numeric_limits<float>::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<u32>(InputPointerAxis::WheelX))
return false;
// still update state anyway
const bool horizontal = (key.data == static_cast<u32>(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<u32>(key) >= std::size(key_map) || key_map[static_cast<u32>(key)] == ImGuiKey_None)
return false;
GetMTGS().RunOnGSThread(
[key = key_map[static_cast<u32>(key)], value]() { ImGui::GetIO().AddKeyAnalogEvent(key, (value > 0.0f), value); });
return true;
}
#endif // PCSX2_CORE

View File

@ -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

View File

@ -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 <array>
#include <memory>
#include <mutex>
@ -36,7 +40,7 @@ enum : u32
{
MAX_KEYS_PER_BINDING = 4,
MAX_MOTORS_PER_PAD = 2,
FIRST_EXTERNAL_INPUT_SOURCE = static_cast<u32>(InputSourceType::Mouse) + 1u,
FIRST_EXTERNAL_INPUT_SOURCE = static_cast<u32>(InputSourceType::Pointer) + 1u,
LAST_EXTERNAL_INPUT_SOURCE = static_cast<u32>(InputSourceType::Count),
};
@ -90,10 +94,8 @@ struct PadVibrationBinding
// ------------------------------------------------------------------------
namespace InputManager
{
static std::optional<InputBindingKey> ParseHostKeyboardKey(
const std::string_view& source, const std::string_view& sub_binding);
static std::optional<InputBindingKey> ParseHostMouseKey(
const std::string_view& source, const std::string_view& sub_binding);
static std::optional<InputBindingKey> ParseHostKeyboardKey(const std::string_view& source, const std::string_view& sub_binding);
static std::optional<InputBindingKey> ParsePointerKey(const std::string_view& source, const std::string_view& sub_binding);
static std::vector<std::string_view> 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<std::unique_ptr<InputSource>, static_cast<u32>(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<const char*, static_cast<u8>(InputPointerAxis::Count)> s_pointer_axis_names = {
{"X", "Y", "WheelX", "WheelY"}};
static constexpr const std::array<const char*, 3> s_pointer_button_names = {{"LeftButton", "RightButton", "MiddleButton"}};
struct PointerAxisState
{
std::atomic<s32> delta;
float last_value;
};
static std::array<std::array<float, static_cast<u8>(InputPointerAxis::Count)>, InputManager::MAX_POINTER_DEVICES> s_host_pointer_positions;
static std::array<std::array<PointerAxisState, static_cast<u8>(InputPointerAxis::Count)>, InputManager::MAX_POINTER_DEVICES>
s_pointer_state;
static std::array<float, static_cast<u8>(InputPointerAxis::Count)> s_pointer_axis_scale;
// ------------------------------------------------------------------------
// Binding Parsing
// ------------------------------------------------------------------------
@ -166,8 +188,7 @@ std::vector<std::string_view> 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<InputBindingKey> 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<std::string> 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<u32>(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<u32>(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<u32>(axis_index);
key.data = static_cast<u32>(axis);
key.source_index = index;
key.source_type = InputSourceType::Pointer;
key.source_subtype = InputSubclass::PointerAxis;
return key;
}
@ -389,8 +416,7 @@ std::optional<InputSourceType> InputManager::ParseInputSourceString(const std::s
return std::nullopt;
}
std::optional<InputBindingKey> InputManager::ParseHostKeyboardKey(
const std::string_view& source, const std::string_view& sub_binding)
std::optional<InputBindingKey> 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<InputBindingKey> InputManager::ParseHostKeyboardKey(
return key;
}
std::optional<InputBindingKey> InputManager::ParseHostMouseKey(
const std::string_view& source, const std::string_view& sub_binding)
std::optional<InputBindingKey> InputManager::ParsePointerKey(const std::string_view& source, const std::string_view& sub_binding)
{
if (source != "Mouse")
const std::optional<s32> pointer_index = StringUtil::FromChars<s32>(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<u32>(pointer_index.value());
if (StringUtil::StartsWith(sub_binding, "Button"))
{
@ -420,15 +447,41 @@ std::optional<InputBindingKey> 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<u32>(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<std::string> bindings(si.GetStringList(section.c_str(),
StringUtil::StdStringFromFormat("Macro%u", macro_button_index + 1).c_str()));
const std::vector<std::string> 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<InputAxisEventHandler>(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<u32>(static_cast<u8>(InputPointerAxis::Count)); axis++)
{
PointerAxisState& state = s_pointer_state[device][axis];
const float delta = static_cast<float>(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<InputPointerAxis>(axis)));
if (axis >= static_cast<u32>(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<u8>(InputPointerAxis::X)], x);
const float dy = y - std::exchange(s_host_pointer_positions[index][static_cast<u8>(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<u8>(axis)].delta.fetch_add(static_cast<s32>(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<u32>(InputPointerAxis::X) && key.data <= static_cast<u32>(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<u32>(InputPointerAxis::Count); axis++)
{
// From lilypad: 1 mouse pixel = 1/8th way down.
const float default_scale = (axis <= static_cast<u32>(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 <typename T>
static void UpdateInputSourceState(SettingsInterface& si, std::unique_lock<std::mutex>& settings_lock, InputSourceType type, bool default_state)
static void UpdateInputSourceState(
SettingsInterface& si, std::unique_lock<std::mutex>& settings_lock, InputSourceType type, bool default_state)
{
const bool enabled = si.GetBoolValue("InputSources", InputManager::InputSourceToString(type), default_state);
if (enabled)

View File

@ -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<CallbackResult(InputBindingKey key, float value)>;
@ -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<std::pair<GenericInputBinding, std::string>>;
/// 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<InputBindingKey> 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

View File

@ -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<InputBindingKey> SDLInputSource::EnumerateMotors()

View File

@ -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);
}
}
}

View File

@ -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.

View File

@ -809,4 +809,4 @@
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<ImportGroup Label="ExtensionTargets" />
</Project>
</Project>