Merge pull request #7829 from jordan-woyak/detect-input-improve

ControllerInterface/DolphinQt: Improve input detection.
This commit is contained in:
JMC47 2019-03-13 19:00:24 -04:00 committed by GitHub
commit 011ecd92e8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 234 additions and 244 deletions

View File

@ -136,8 +136,8 @@ void IOWindow::ConnectWidgets()
connect(m_or_button, &QPushButton::clicked, [this] { AppendSelectedOption(" | "); });
connect(m_not_button, &QPushButton::clicked, [this] { AppendSelectedOption("!"); });
connect(m_type == IOWindow::Type::Input ? m_detect_button : m_test_button, &QPushButton::clicked,
this, &IOWindow::OnDetectButtonPressed);
connect(m_detect_button, &QPushButton::clicked, this, &IOWindow::OnDetectButtonPressed);
connect(m_test_button, &QPushButton::clicked, this, &IOWindow::OnTestButtonPressed);
connect(m_button_box, &QDialogButtonBox::clicked, this, &IOWindow::OnDialogButtonPressed);
connect(m_devices_combo, &QComboBox::currentTextChanged, this, &IOWindow::OnDeviceChanged);
@ -180,35 +180,22 @@ void IOWindow::OnDialogButtonPressed(QAbstractButton* button)
void IOWindow::OnDetectButtonPressed()
{
installEventFilter(BlockUserInputFilter::Instance());
grabKeyboard();
grabMouse();
const auto expression =
MappingCommon::DetectExpression(m_detect_button, g_controller_interface, {m_devq.ToString()},
m_devq, MappingCommon::Quote::Off);
std::thread([this] {
auto* btn = m_type == IOWindow::Type::Input ? m_detect_button : m_test_button;
const auto old_label = btn->text();
if (expression.isEmpty())
return;
btn->setText(QStringLiteral("..."));
const auto list = m_option_list->findItems(expression, Qt::MatchFixedString);
const auto expr = MappingCommon::DetectExpression(
m_reference, g_controller_interface.FindDevice(m_devq).get(), m_devq,
MappingCommon::Quote::Off);
if (!list.empty())
m_option_list->setCurrentItem(list[0]);
}
btn->setText(old_label);
if (!expr.isEmpty())
{
const auto list = m_option_list->findItems(expr, Qt::MatchFixedString);
if (!list.empty())
m_option_list->setCurrentItem(list[0]);
}
releaseMouse();
releaseKeyboard();
removeEventFilter(BlockUserInputFilter::Instance());
})
.detach();
void IOWindow::OnTestButtonPressed()
{
MappingCommon::TestOutput(m_test_button, static_cast<OutputReference*>(m_reference));
}
void IOWindow::OnRangeChanged(int value)

View File

@ -48,6 +48,7 @@ private:
void OnDialogButtonPressed(QAbstractButton* button);
void OnDeviceChanged(const QString& device);
void OnDetectButtonPressed();
void OnTestButtonPressed();
void OnRangeChanged(int range);
void AppendSelectedOption(const std::string& prefix);

View File

@ -2,8 +2,7 @@
// Licensed under GPLv2+
// Refer to the license.txt file included.
#include <future>
#include <utility>
#include "DolphinQt/Config/Mapping/MappingButton.h"
#include <QApplication>
#include <QFontMetrics>
@ -12,8 +11,6 @@
#include <QString>
#include <QTimer>
#include "DolphinQt/Config/Mapping/MappingButton.h"
#include "Common/Thread.h"
#include "Core/Core.h"
@ -103,96 +100,33 @@ void MappingButton::Detect()
if (m_parent->GetDevice() == nullptr || !m_reference->IsInput())
return;
installEventFilter(BlockUserInputFilter::Instance());
grabKeyboard();
const auto default_device_qualifier = m_parent->GetController()->GetDefaultDevice();
// Make sure that we don't block event handling
QueueOnObject(this, [this] {
setText(QStringLiteral("..."));
QString expression;
// The button text won't be updated if we don't process events here
QApplication::processEvents();
if (m_parent->GetParent()->IsMappingAllDevices())
{
expression = MappingCommon::DetectExpression(this, g_controller_interface,
g_controller_interface.GetAllDeviceStrings(),
default_device_qualifier);
}
else
{
expression = MappingCommon::DetectExpression(this, g_controller_interface,
{default_device_qualifier.ToString()},
default_device_qualifier);
}
// Avoid that the button press itself is registered as an event
Common::SleepCurrentThread(100);
if (expression.isEmpty())
return;
std::vector<std::future<std::pair<QString, QString>>> futures;
QString expr;
m_reference->SetExpression(expression.toStdString());
m_parent->SaveSettings();
Update();
m_parent->GetController()->UpdateReferences(g_controller_interface);
if (m_parent->GetParent()->IsMappingAllDevices())
{
for (const std::string& device_str : g_controller_interface.GetAllDeviceStrings())
{
ciface::Core::DeviceQualifier devq;
devq.FromString(device_str);
auto dev = g_controller_interface.FindDevice(devq);
auto future = std::async(std::launch::async, [this, devq, dev, device_str] {
return std::make_pair(
QString::fromStdString(device_str),
MappingCommon::DetectExpression(m_reference, dev.get(),
m_parent->GetController()->GetDefaultDevice()));
});
futures.push_back(std::move(future));
}
bool done = false;
while (!done)
{
for (auto& future : futures)
{
const auto status = future.wait_for(std::chrono::milliseconds(10));
if (status == std::future_status::ready)
{
const auto pair = future.get();
done = true;
if (pair.second.isEmpty())
break;
expr = QStringLiteral("`%1:%2`")
.arg(pair.first)
.arg(pair.second.startsWith(QLatin1Char('`')) ? pair.second.mid(1) :
pair.second);
break;
}
}
}
}
else
{
const auto dev = m_parent->GetDevice();
expr = MappingCommon::DetectExpression(m_reference, dev.get(),
m_parent->GetController()->GetDefaultDevice());
}
releaseKeyboard();
removeEventFilter(BlockUserInputFilter::Instance());
if (!expr.isEmpty())
{
m_reference->SetExpression(expr.toStdString());
m_parent->SaveSettings();
Update();
m_parent->GetController()->UpdateReferences(g_controller_interface);
if (m_parent->IsIterativeInput())
m_parent->NextButton(this);
}
else
{
OnButtonTimeout();
}
});
}
void MappingButton::OnButtonTimeout()
{
setText(ToDisplayString(QString::fromStdString(m_reference->GetExpression())));
if (m_parent->IsIterativeInput())
m_parent->NextButton(this);
}
void MappingButton::Clear()

View File

@ -30,7 +30,6 @@ signals:
private:
void mouseReleaseEvent(QMouseEvent* event) override;
void OnButtonTimeout();
void Connect();
MappingWidget* m_parent;

View File

@ -4,16 +4,23 @@
#include "DolphinQt/Config/Mapping/MappingCommon.h"
#include <tuple>
#include <QApplication>
#include <QPushButton>
#include <QRegExp>
#include <QString>
#include "DolphinQt/QtUtils/BlockUserInputFilter.h"
#include "InputCommon/ControlReference/ControlReference.h"
#include "InputCommon/ControllerInterface/Device.h"
#include "Common/Thread.h"
namespace MappingCommon
{
constexpr int INPUT_DETECT_TIME = 3000;
constexpr int OUTPUT_DETECT_TIME = 2000;
constexpr int OUTPUT_TEST_TIME = 2000;
QString GetExpressionForControl(const QString& control_name,
const ciface::Core::DeviceQualifier& control_device,
@ -41,18 +48,56 @@ QString GetExpressionForControl(const QString& control_name,
return expr;
}
QString DetectExpression(ControlReference* reference, ciface::Core::Device* device,
QString DetectExpression(QPushButton* button, ciface::Core::DeviceContainer& device_container,
const std::vector<std::string>& device_strings,
const ciface::Core::DeviceQualifier& default_device, Quote quote)
{
const int ms = reference->IsInput() ? INPUT_DETECT_TIME : OUTPUT_DETECT_TIME;
button->installEventFilter(BlockUserInputFilter::Instance());
button->grabKeyboard();
button->grabMouse();
ciface::Core::Device::Control* const ctrl = reference->Detect(ms, device);
const auto old_text = button->text();
button->setText(QStringLiteral("..."));
if (ctrl)
{
return MappingCommon::GetExpressionForControl(QString::fromStdString(ctrl->GetName()),
default_device, default_device, quote);
}
return QStringLiteral("");
// The button text won't be updated if we don't process events here
QApplication::processEvents();
// Avoid that the button press itself is registered as an event
Common::SleepCurrentThread(100);
std::shared_ptr<ciface::Core::Device> device;
ciface::Core::Device::Input* input;
std::tie(device, input) = device_container.DetectInput(INPUT_DETECT_TIME, device_strings);
button->releaseMouse();
button->releaseKeyboard();
button->removeEventFilter(BlockUserInputFilter::Instance());
button->setText(old_text);
if (!input)
return {};
ciface::Core::DeviceQualifier device_qualifier;
device_qualifier.FromDevice(device.get());
return MappingCommon::GetExpressionForControl(QString::fromStdString(input->GetName()),
device_qualifier, default_device, quote);
}
void TestOutput(QPushButton* button, OutputReference* reference)
{
const auto old_text = button->text();
button->setText(QStringLiteral("..."));
// The button text won't be updated if we don't process events here
QApplication::processEvents();
reference->State(1.0);
Common::SleepCurrentThread(OUTPUT_TEST_TIME);
reference->State(0.0);
button->setText(old_text);
}
} // namespace MappingCommon

View File

@ -4,17 +4,21 @@
#pragma once
#include <string>
#include <vector>
class QString;
class ControlReference;
class OutputReference;
class QPushButton;
namespace ciface
{
namespace Core
{
class Device;
class DeviceContainer;
class DeviceQualifier;
}
}
} // namespace Core
} // namespace ciface
namespace MappingCommon
{
@ -28,7 +32,12 @@ QString GetExpressionForControl(const QString& control_name,
const ciface::Core::DeviceQualifier& control_device,
const ciface::Core::DeviceQualifier& default_device,
Quote quote = Quote::On);
QString DetectExpression(ControlReference* reference, ciface::Core::Device* device,
QString DetectExpression(QPushButton* button, ciface::Core::DeviceContainer& device_container,
const std::vector<std::string>& device_strings,
const ciface::Core::DeviceQualifier& default_device,
Quote quote = Quote::On);
void TestOutput(QPushButton* button, OutputReference* reference);
} // namespace MappingCommon

View File

@ -2,19 +2,16 @@
// Licensed under GPLv2+
// Refer to the license.txt file included.
#include "Common/Thread.h"
#include "InputCommon/ControlReference/ControlReference.h"
// For InputGateOn()
// This is a bad layering violation, but it's the cleanest
// place I could find to put it.
#include "Core/ConfigManager.h"
#include "Core/Host.h"
#include "InputCommon/ControlReference/ControlReference.h"
using namespace ciface::ExpressionParser;
constexpr ControlState INPUT_DETECT_THRESHOLD = 0.55;
bool ControlReference::InputGateOn()
{
return SConfig::GetInstance().m_BackgroundInput || Host_RendererHasFocus() ||
@ -109,86 +106,3 @@ ControlState OutputReference::State(const ControlState state)
m_parsed_expression->SetValue(state * range);
return 0.0;
}
//
// InputReference :: Detect
//
// Wait for input on all binded devices
// supports not detecting inputs that were held down at the time of Detect start,
// which is useful for those crazy flightsticks that have certain buttons that are always held down
// or some crazy axes or something
// upon input, return pointer to detected Control
// else return nullptr
//
ciface::Core::Device::Control* InputReference::Detect(const unsigned int ms,
ciface::Core::Device* const device)
{
unsigned int time = 0;
std::vector<bool> states(device->Inputs().size());
if (device->Inputs().empty())
return nullptr;
// get starting state of all inputs,
// so we can ignore those that were activated at time of Detect start
std::vector<ciface::Core::Device::Input*>::const_iterator i = device->Inputs().begin(),
e = device->Inputs().end();
for (std::vector<bool>::iterator state = states.begin(); i != e; ++i)
*state++ = ((*i)->GetState() > (1 - INPUT_DETECT_THRESHOLD));
while (time < ms)
{
device->UpdateInput();
i = device->Inputs().begin();
for (std::vector<bool>::iterator state = states.begin(); i != e; ++i, ++state)
{
// detected an input
if ((*i)->IsDetectable() && (*i)->GetState() > INPUT_DETECT_THRESHOLD)
{
// input was released at some point during Detect call
// return the detected input
if (false == *state)
return *i;
}
else if ((*i)->GetState() < (1 - INPUT_DETECT_THRESHOLD))
{
*state = false;
}
}
Common::SleepCurrentThread(10);
time += 10;
}
// no input was detected
return nullptr;
}
//
// OutputReference :: Detect
//
// Totally different from the inputReference detect / I have them combined so it was simpler to make
// the GUI.
// The GUI doesn't know the difference between an input and an output / it's odd but I was lazy and
// it was easy
//
// set all binded outputs to <range> power for x milliseconds return false
//
ciface::Core::Device::Control* OutputReference::Detect(const unsigned int ms,
ciface::Core::Device* const device)
{
// ignore device
// don't hang if we don't even have any controls mapped
if (BoundCount() > 0)
{
State(1);
unsigned int slept = 0;
// this loop is to make stuff like flashing keyboard LEDs work
while (ms > (slept += 10))
Common::SleepCurrentThread(10);
State(0);
}
return nullptr;
}

View File

@ -26,8 +26,6 @@ public:
virtual ~ControlReference();
virtual ControlState State(const ControlState state = 0) = 0;
virtual ciface::Core::Device::Control* Detect(const unsigned int ms,
ciface::Core::Device* const device) = 0;
virtual bool IsInput() const = 0;
int BoundCount() const;
@ -57,8 +55,6 @@ public:
InputReference();
bool IsInput() const override;
ControlState State(const ControlState state) override;
ciface::Core::Device::Control* Detect(const unsigned int ms,
ciface::Core::Device* const device) override;
};
//
@ -72,6 +68,4 @@ public:
OutputReference();
bool IsInput() const override;
ControlState State(const ControlState state) override;
ciface::Core::Device::Control* Detect(const unsigned int ms,
ciface::Core::Device* const device) override;
};

View File

@ -217,7 +217,19 @@ public:
std::shared_ptr<Device> m_device;
explicit ControlExpression(ControlQualifier qualifier_) : qualifier(qualifier_) {}
ControlState GetValue() const override { return control ? control->ToInput()->GetState() : 0.0; }
ControlState GetValue() const override
{
if (!control)
return 0.0;
// Note: Inputs may return negative values in situations where opposing directions are
// activated. We clamp off the negative values here.
// FYI: Clamping values greater than 1.0 is purposely not done to support unbounded values in
// the future. (e.g. raw accelerometer/gyro data)
return std::max(0.0, control->ToInput()->GetState());
}
void SetValue(ControlState value) override
{
if (control)

View File

@ -259,7 +259,7 @@ std::string Joystick::Hat::GetName() const
ControlState Joystick::Axis::GetState() const
{
return std::max(0.0, ControlState(m_axis - m_base) / m_range);
return ControlState(m_axis - m_base) / m_range;
}
ControlState Joystick::Button::GetState() const

View File

@ -4,22 +4,22 @@
#include "InputCommon/ControllerInterface/Device.h"
#include <cmath>
#include <memory>
#include <sstream>
#include <string>
#include <tuple>
#include "Common/StringUtil.h"
#include "Common/Thread.h"
namespace ciface
{
namespace Core
{
//
// Device :: ~Device
//
// Destructor, delete all inputs/outputs on device destruction
//
// Compared to an input's current state (ideally 1.0) minus abs(initial_state) (ideally 0.0).
constexpr ControlState INPUT_DETECT_THRESHOLD = 0.55;
Device::~Device()
{
// delete inputs
@ -68,6 +68,11 @@ Device::Output* Device::FindOutput(const std::string& name) const
return nullptr;
}
ControlState Device::FullAnalogSurface::GetState() const
{
return (1 + std::max(0.0, m_high.GetState()) - std::max(0.0, m_low.GetState())) / 2;
}
//
// DeviceQualifier :: ToString
//
@ -214,5 +219,83 @@ bool DeviceContainer::HasConnectedDevice(const DeviceQualifier& qualifier) const
const auto device = FindDevice(qualifier);
return device != nullptr && device->IsValid();
}
// Wait for input on a particular device.
// Inputs are considered if they are first seen in a neutral state.
// This is useful for crazy flightsticks that have certain buttons that are always held down
// and also properly handles detection when using "FullAnalogSurface" inputs.
// Upon input, return the detected Device and Input, else return nullptrs
std::pair<std::shared_ptr<Device>, Device::Input*>
DeviceContainer::DetectInput(u32 wait_ms, std::vector<std::string> device_strings)
{
struct InputState
{
ciface::Core::Device::Input& input;
ControlState initial_state;
};
struct DeviceState
{
std::shared_ptr<Device> device;
std::vector<InputState> input_states;
};
// Acquire devices and initial input states.
std::vector<DeviceState> device_states;
for (auto& device_string : device_strings)
{
DeviceQualifier dq;
dq.FromString(device_string);
auto device = FindDevice(dq);
if (!device)
continue;
std::vector<InputState> input_states;
for (auto* input : device->Inputs())
{
// Don't detect things like absolute cursor position.
if (!input->IsDetectable())
continue;
// Undesirable axes will have negative values here when trying to map a
// "FullAnalogSurface".
input_states.push_back({*input, input->GetState()});
}
if (!input_states.empty())
device_states.emplace_back(DeviceState{std::move(device), std::move(input_states)});
}
if (device_states.empty())
return {};
u32 time = 0;
while (time < wait_ms)
{
Common::SleepCurrentThread(10);
time += 10;
for (auto& device_state : device_states)
{
device_state.device->UpdateInput();
for (auto& input_state : device_state.input_states)
{
// We want an input that was initially 0.0 and currently 1.0.
const auto detection_score =
(input_state.input.GetState() - std::abs(input_state.initial_state));
if (detection_score > INPUT_DETECT_THRESHOLD)
return {device_state.device, &input_state.input};
}
}
}
// No input was detected. :'(
return {};
}
}
} // namespace Core
} // namespace ciface

View File

@ -55,9 +55,22 @@ public:
class Input : public Control
{
public:
// things like absolute axes/ absolute mouse position will override this
// Things like absolute axes/ absolute mouse position should override this to prevent
// undesirable behavior in our mapping logic.
virtual bool IsDetectable() { return true; }
// Implementations should return a value from 0.0 to 1.0 across their normal range.
// One input should be provided for each "direction". (e.g. 2 for each axis)
// If possible, negative values may be returned in situations where an opposing input is
// activated. (e.g. When an underlying axis, X, is currently negative, "Axis X-", will return a
// positive value and "Axis X+" may return a negative value.)
// Doing so is solely to allow our input detection logic to better detect false positives.
// This is necessary when making use of "FullAnalogSurface" as multiple inputs will be seen
// increasing from 0.0 to 1.0 as a user tries to map just one. The negative values provide a
// view of the underlying axis. (Negative values are clamped off before they reach
// expression-parser or controller-emu)
virtual ControlState GetState() const = 0;
Input* ToInput() override { return this; }
};
@ -96,11 +109,7 @@ protected:
{
public:
FullAnalogSurface(Input* low, Input* high) : m_low(*low), m_high(*high) {}
ControlState GetState() const override
{
return (1 + m_high.GetState() - m_low.GetState()) / 2;
}
ControlState GetState() const override;
std::string GetName() const override { return m_low.GetName() + *m_high.GetName().rbegin(); }
private:
@ -163,6 +172,9 @@ public:
bool HasConnectedDevice(const DeviceQualifier& qualifier) const;
std::pair<std::shared_ptr<Device>, Device::Input*>
DetectInput(u32 wait_ms, std::vector<std::string> device_strings);
protected:
mutable std::mutex m_devices_mutex;
std::vector<std::shared_ptr<Device>> m_devices;

View File

@ -228,7 +228,7 @@ ControlState Device::Trigger::GetState() const
ControlState Device::Axis::GetState() const
{
return std::max(0.0, ControlState(m_axis) / m_range);
return ControlState(m_axis) / m_range;
}
void Device::Motor::SetState(ControlState state)
@ -236,5 +236,5 @@ void Device::Motor::SetState(ControlState state)
m_motor = (WORD)(state * m_range);
m_parent->UpdateMotors();
}
}
}
} // namespace XInput
} // namespace ciface

View File

@ -343,7 +343,7 @@ ControlState evdevDevice::Axis::GetState() const
int value = 0;
libevdev_fetch_event_value(m_dev, EV_ABS, m_code, &value);
return std::max(0.0, ControlState(value - m_base) / m_range);
return ControlState(value - m_base) / m_range;
}
evdevDevice::Effect::Effect(int fd) : m_fd(fd)