mirror of
https://github.com/dolphin-emu/dolphin.git
synced 2025-06-27 17:39:34 -06:00
Merge pull request #13703 from jordan-woyak/map-and-calibrate
DolphinQt/InputCommon: Make the "Calibrate" button also map inputs.
This commit is contained in:
@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
#include "DolphinQt/Config/Mapping/IOWindow.h"
|
#include "DolphinQt/Config/Mapping/IOWindow.h"
|
||||||
|
|
||||||
|
#include <array>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
|
|
||||||
#include <QBrush>
|
#include <QBrush>
|
||||||
@ -532,7 +533,7 @@ void IOWindow::ConnectWidgets()
|
|||||||
m_detect_button->setText(tr("[ Press Now ]"));
|
m_detect_button->setText(tr("[ Press Now ]"));
|
||||||
m_input_detector = std::make_unique<ciface::Core::InputDetector>();
|
m_input_detector = std::make_unique<ciface::Core::InputDetector>();
|
||||||
const auto lock = m_controller->GetStateLock();
|
const auto lock = m_controller->GetStateLock();
|
||||||
m_input_detector->Start(g_controller_interface, {m_devq.ToString()});
|
m_input_detector->Start(g_controller_interface, std::array{m_devq.ToString()});
|
||||||
QtUtils::InstallKeyboardBlocker(m_detect_button, this, &IOWindow::DetectInputComplete);
|
QtUtils::InstallKeyboardBlocker(m_detect_button, this, &IOWindow::DetectInputComplete);
|
||||||
});
|
});
|
||||||
connect(this, &IOWindow::DetectInputComplete,
|
connect(this, &IOWindow::DetectInputComplete,
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
#include "DolphinQt/Config/Mapping/MappingCommon.h"
|
#include "DolphinQt/Config/Mapping/MappingCommon.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
#include <deque>
|
#include <deque>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
|
||||||
@ -26,11 +27,6 @@ constexpr auto INPUT_DETECT_MAXIMUM_TIME = std::chrono::seconds(5);
|
|||||||
// Ignore the mouse-click when queuing more buttons with "alternate mappings" enabled.
|
// Ignore the mouse-click when queuing more buttons with "alternate mappings" enabled.
|
||||||
constexpr auto INPUT_DETECT_ENDING_IGNORE_TIME = std::chrono::milliseconds(50);
|
constexpr auto INPUT_DETECT_ENDING_IGNORE_TIME = std::chrono::milliseconds(50);
|
||||||
|
|
||||||
bool ContainsAnalogInput(const ciface::Core::InputDetector::Results& results)
|
|
||||||
{
|
|
||||||
return std::ranges::any_of(results, [](auto& detection) { return detection.smoothness > 1; });
|
|
||||||
}
|
|
||||||
|
|
||||||
class MappingProcessor : public QObject
|
class MappingProcessor : public QObject
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
@ -102,7 +98,7 @@ public:
|
|||||||
// Skip "Modifier" mappings when using analog inputs.
|
// Skip "Modifier" mappings when using analog inputs.
|
||||||
auto* next_button = m_clicked_mapping_buttons.front();
|
auto* next_button = m_clicked_mapping_buttons.front();
|
||||||
if (next_button->GetControlType() == MappingButton::ControlType::ModifierInput &&
|
if (next_button->GetControlType() == MappingButton::ControlType::ModifierInput &&
|
||||||
ContainsAnalogInput(results))
|
std::ranges::any_of(results, &ciface::Core::InputDetector::Detection::IsAnalogPress))
|
||||||
{
|
{
|
||||||
// Clear "Modifier" mapping and queue the next button.
|
// Clear "Modifier" mapping and queue the next button.
|
||||||
SetButtonExpression(next_button, "");
|
SetButtonExpression(next_button, "");
|
||||||
|
@ -5,14 +5,15 @@
|
|||||||
|
|
||||||
#include <array>
|
#include <array>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
|
#include <numbers>
|
||||||
|
|
||||||
#include <fmt/format.h>
|
#include <fmt/format.h>
|
||||||
|
|
||||||
#include <QAction>
|
#include <QAction>
|
||||||
#include <QDateTime>
|
#include <QDateTime>
|
||||||
|
#include <QEasingCurve>
|
||||||
#include <QPainter>
|
#include <QPainter>
|
||||||
#include <QPainterPath>
|
#include <QPainterPath>
|
||||||
#include <QTimer>
|
|
||||||
|
|
||||||
#include "Common/MathUtil.h"
|
#include "Common/MathUtil.h"
|
||||||
|
|
||||||
@ -23,9 +24,10 @@
|
|||||||
#include "InputCommon/ControllerEmu/ControlGroup/Force.h"
|
#include "InputCommon/ControllerEmu/ControlGroup/Force.h"
|
||||||
#include "InputCommon/ControllerEmu/ControlGroup/MixedTriggers.h"
|
#include "InputCommon/ControllerEmu/ControlGroup/MixedTriggers.h"
|
||||||
#include "InputCommon/ControllerInterface/CoreDevice.h"
|
#include "InputCommon/ControllerInterface/CoreDevice.h"
|
||||||
|
#include "InputCommon/ControllerInterface/MappingCommon.h"
|
||||||
|
|
||||||
#include "DolphinQt/Config/Mapping/MappingWidget.h"
|
#include "DolphinQt/Config/Mapping/MappingWidget.h"
|
||||||
#include "DolphinQt/QtUtils/ModalMessageBox.h"
|
#include "DolphinQt/Config/Mapping/MappingWindow.h"
|
||||||
#include "DolphinQt/Settings.h"
|
#include "DolphinQt/Settings.h"
|
||||||
|
|
||||||
namespace
|
namespace
|
||||||
@ -238,34 +240,6 @@ QPolygonF GetPolygonSegmentFromRadiusGetter(F&& radius_getter, double direction,
|
|||||||
return shape;
|
return shape;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Used to check if the user seems to have attempted proper calibration.
|
|
||||||
bool IsCalibrationDataSensible(const ControllerEmu::ReshapableInput::CalibrationData& data)
|
|
||||||
{
|
|
||||||
// Test that the average input radius is not below a threshold.
|
|
||||||
// This will make sure the user has actually moved their stick from neutral.
|
|
||||||
|
|
||||||
// Even the GC controller's small range would pass this test.
|
|
||||||
constexpr double REASONABLE_AVERAGE_RADIUS = 0.6;
|
|
||||||
|
|
||||||
MathUtil::RunningVariance<ControlState> stats;
|
|
||||||
|
|
||||||
for (auto& x : data)
|
|
||||||
stats.Push(x);
|
|
||||||
|
|
||||||
if (stats.Mean() < REASONABLE_AVERAGE_RADIUS)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test that the standard deviation is below a threshold.
|
|
||||||
// This will make sure the user has not just filled in one side of their input.
|
|
||||||
|
|
||||||
// Approx. deviation of a square input gate, anything much more than that would be unusual.
|
|
||||||
constexpr double REASONABLE_DEVIATION = 0.14;
|
|
||||||
|
|
||||||
return stats.StandardDeviation() < REASONABLE_DEVIATION;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Used to test for a miscalibrated stick so the user can be informed.
|
// Used to test for a miscalibrated stick so the user can be informed.
|
||||||
bool IsPointOutsideCalibration(Common::DVec2 point, ControllerEmu::ReshapableInput& input)
|
bool IsPointOutsideCalibration(Common::DVec2 point, ControllerEmu::ReshapableInput& input)
|
||||||
{
|
{
|
||||||
@ -313,6 +287,23 @@ void GenerateFibonacciSphere(int point_count, F&& callback)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Draws an analog stick pushed to the right by the provided amount.
|
||||||
|
void DrawPushedStick(QPainter& p, ReshapableInputIndicator& indicator, double value)
|
||||||
|
{
|
||||||
|
auto stick_color = indicator.GetGateBrushColor();
|
||||||
|
indicator.AdjustGateColor(&stick_color);
|
||||||
|
const auto stick_pen_color = stick_color.darker(125);
|
||||||
|
p.setPen(QPen{stick_pen_color, 0});
|
||||||
|
p.setBrush(stick_color);
|
||||||
|
constexpr float circle_radius = 0.65f;
|
||||||
|
p.drawEllipse(QPointF{value * 0.35f, 0.f}, circle_radius, circle_radius);
|
||||||
|
|
||||||
|
p.setPen(QPen{indicator.GetRawInputColor(), 0});
|
||||||
|
p.setBrush(Qt::NoBrush);
|
||||||
|
constexpr float alt_circle_radius = 0.45f;
|
||||||
|
p.drawEllipse(QPointF{value * 0.45f, 0.f}, alt_circle_radius, alt_circle_radius);
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
void MappingIndicator::paintEvent(QPaintEvent*)
|
void MappingIndicator::paintEvent(QPaintEvent*)
|
||||||
@ -328,11 +319,16 @@ void MappingIndicator::paintEvent(QPaintEvent*)
|
|||||||
Draw();
|
Draw();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QColor CursorIndicator::GetGateBrushColor() const
|
||||||
|
{
|
||||||
|
return CURSOR_TV_COLOR;
|
||||||
|
}
|
||||||
|
|
||||||
void CursorIndicator::Draw()
|
void CursorIndicator::Draw()
|
||||||
{
|
{
|
||||||
const auto adj_coord = m_cursor_group.GetState(true);
|
const auto adj_coord = m_cursor_group.GetState(true);
|
||||||
|
|
||||||
DrawReshapableInput(m_cursor_group, CURSOR_TV_COLOR,
|
DrawReshapableInput(m_cursor_group,
|
||||||
adj_coord.IsVisible() ?
|
adj_coord.IsVisible() ?
|
||||||
std::make_optional(Common::DVec2(adj_coord.x, adj_coord.y)) :
|
std::make_optional(Common::DVec2(adj_coord.x, adj_coord.y)) :
|
||||||
std::nullopt);
|
std::nullopt);
|
||||||
@ -362,7 +358,7 @@ void SquareIndicator::TransformPainter(QPainter& p)
|
|||||||
}
|
}
|
||||||
|
|
||||||
void ReshapableInputIndicator::DrawReshapableInput(
|
void ReshapableInputIndicator::DrawReshapableInput(
|
||||||
ControllerEmu::ReshapableInput& stick, QColor gate_brush_color,
|
ControllerEmu::ReshapableInput& stick,
|
||||||
std::optional<ControllerEmu::ReshapableInput::ReshapeData> adj_coord)
|
std::optional<ControllerEmu::ReshapableInput::ReshapeData> adj_coord)
|
||||||
{
|
{
|
||||||
QPainter p(this);
|
QPainter p(this);
|
||||||
@ -378,13 +374,14 @@ void ReshapableInputIndicator::DrawReshapableInput(
|
|||||||
|
|
||||||
if (IsCalibrating())
|
if (IsCalibrating())
|
||||||
{
|
{
|
||||||
DrawCalibration(p, raw_coord);
|
m_calibration_widget->Draw(p, raw_coord);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
DrawUnderGate(p);
|
DrawUnderGate(p);
|
||||||
|
|
||||||
QColor gate_pen_color = gate_brush_color.darker(125);
|
auto gate_brush_color = GetGateBrushColor();
|
||||||
|
auto gate_pen_color = gate_brush_color.darker(125);
|
||||||
|
|
||||||
AdjustGateColor(&gate_brush_color);
|
AdjustGateColor(&gate_brush_color);
|
||||||
AdjustGateColor(&gate_pen_color);
|
AdjustGateColor(&gate_pen_color);
|
||||||
@ -435,24 +432,33 @@ void ReshapableInputIndicator::DrawReshapableInput(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void AnalogStickIndicator::Draw()
|
QColor AnalogStickIndicator::GetGateBrushColor() const
|
||||||
{
|
{
|
||||||
// Some hacks for pretty colors:
|
// Some hacks for pretty colors:
|
||||||
const bool is_c_stick = m_group.name == "C-Stick";
|
const bool is_c_stick = m_group.name == "C-Stick";
|
||||||
|
|
||||||
const auto gate_brush_color = is_c_stick ? C_STICK_GATE_COLOR : STICK_GATE_COLOR;
|
return is_c_stick ? C_STICK_GATE_COLOR : STICK_GATE_COLOR;
|
||||||
|
}
|
||||||
|
|
||||||
|
void AnalogStickIndicator::Draw()
|
||||||
|
{
|
||||||
const auto adj_coord = m_group.GetReshapableState(true);
|
const auto adj_coord = m_group.GetReshapableState(true);
|
||||||
|
|
||||||
DrawReshapableInput(m_group, gate_brush_color,
|
DrawReshapableInput(m_group,
|
||||||
(adj_coord.x || adj_coord.y) ? std::make_optional(adj_coord) : std::nullopt);
|
(adj_coord.x || adj_coord.y) ? std::make_optional(adj_coord) : std::nullopt);
|
||||||
}
|
}
|
||||||
|
|
||||||
void TiltIndicator::Update(float elapsed_seconds)
|
void TiltIndicator::Update(float elapsed_seconds)
|
||||||
{
|
{
|
||||||
|
ReshapableInputIndicator::Update(elapsed_seconds);
|
||||||
WiimoteEmu::EmulateTilt(&m_motion_state, &m_group, elapsed_seconds);
|
WiimoteEmu::EmulateTilt(&m_motion_state, &m_group, elapsed_seconds);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QColor TiltIndicator::GetGateBrushColor() const
|
||||||
|
{
|
||||||
|
return TILT_GATE_COLOR;
|
||||||
|
}
|
||||||
|
|
||||||
void TiltIndicator::Draw()
|
void TiltIndicator::Draw()
|
||||||
{
|
{
|
||||||
auto adj_coord = Common::DVec2{-m_motion_state.angle.y, m_motion_state.angle.x} / MathUtil::PI;
|
auto adj_coord = Common::DVec2{-m_motion_state.angle.y, m_motion_state.angle.x} / MathUtil::PI;
|
||||||
@ -468,7 +474,7 @@ void TiltIndicator::Draw()
|
|||||||
adj_coord.x = std::fmod(adj_coord.x + norm_360_deg + norm_180_deg, norm_360_deg) - norm_180_deg;
|
adj_coord.x = std::fmod(adj_coord.x + norm_360_deg + norm_180_deg, norm_360_deg) - norm_180_deg;
|
||||||
adj_coord.y = std::fmod(adj_coord.y + norm_360_deg + norm_180_deg, norm_360_deg) - norm_180_deg;
|
adj_coord.y = std::fmod(adj_coord.y + norm_360_deg + norm_180_deg, norm_360_deg) - norm_180_deg;
|
||||||
|
|
||||||
DrawReshapableInput(m_group, TILT_GATE_COLOR,
|
DrawReshapableInput(m_group,
|
||||||
(adj_coord.x || adj_coord.y) ? std::make_optional(adj_coord) : std::nullopt);
|
(adj_coord.x || adj_coord.y) ? std::make_optional(adj_coord) : std::nullopt);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -604,12 +610,18 @@ void SwingIndicator::DrawUnderGate(QPainter& p)
|
|||||||
|
|
||||||
void SwingIndicator::Update(float elapsed_seconds)
|
void SwingIndicator::Update(float elapsed_seconds)
|
||||||
{
|
{
|
||||||
|
ReshapableInputIndicator::Update(elapsed_seconds);
|
||||||
WiimoteEmu::EmulateSwing(&m_motion_state, &m_swing_group, elapsed_seconds);
|
WiimoteEmu::EmulateSwing(&m_motion_state, &m_swing_group, elapsed_seconds);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QColor SwingIndicator::GetGateBrushColor() const
|
||||||
|
{
|
||||||
|
return SWING_GATE_COLOR;
|
||||||
|
}
|
||||||
|
|
||||||
void SwingIndicator::Draw()
|
void SwingIndicator::Draw()
|
||||||
{
|
{
|
||||||
DrawReshapableInput(m_swing_group, SWING_GATE_COLOR,
|
DrawReshapableInput(m_swing_group,
|
||||||
Common::DVec2{-m_motion_state.position.x, m_motion_state.position.z});
|
Common::DVec2{-m_motion_state.position.x, m_motion_state.position.z});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -915,42 +927,103 @@ void IRPassthroughMappingIndicator::Draw()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void ReshapableInputIndicator::DrawCalibration(QPainter& p, Common::DVec2 point)
|
void CalibrationWidget::Draw(QPainter& p, Common::DVec2 point)
|
||||||
{
|
{
|
||||||
const auto center = m_calibration_widget->GetCenter();
|
DrawInProgressMapping(p);
|
||||||
|
DrawInProgressCalibration(p, point);
|
||||||
|
}
|
||||||
|
|
||||||
|
double CalibrationWidget::GetAnimationElapsedSeconds() const
|
||||||
|
{
|
||||||
|
return DT_s{Clock::now() - m_animation_start_time}.count();
|
||||||
|
}
|
||||||
|
|
||||||
|
void CalibrationWidget::RestartAnimation()
|
||||||
|
{
|
||||||
|
m_animation_start_time = Clock::now();
|
||||||
|
}
|
||||||
|
|
||||||
|
void CalibrationWidget::DrawInProgressMapping(QPainter& p)
|
||||||
|
{
|
||||||
|
if (!IsMapping())
|
||||||
|
return;
|
||||||
|
|
||||||
|
p.rotate(qRadiansToDegrees(m_mapper->GetCurrentAngle()));
|
||||||
|
|
||||||
|
const auto ping_pong = 1 - std::abs(1 - (2 * std::fmod(GetAnimationElapsedSeconds(), 1)));
|
||||||
|
|
||||||
|
// Stick.
|
||||||
|
DrawPushedStick(p, m_indicator,
|
||||||
|
QEasingCurve(QEasingCurve::OutBounce).valueForProgress(ping_pong));
|
||||||
|
|
||||||
|
// Arrow.
|
||||||
|
p.save();
|
||||||
|
const auto triangle_x =
|
||||||
|
(QEasingCurve(QEasingCurve::InOutQuart).valueForProgress(ping_pong) * 0.3) + 0.1;
|
||||||
|
p.translate(triangle_x, 0.0);
|
||||||
|
|
||||||
|
// An equilateral triangle.
|
||||||
|
constexpr auto triangle_h = 0.2f;
|
||||||
|
constexpr auto triangle_w_2 = triangle_h / std::numbers::sqrt3_v<float>;
|
||||||
|
|
||||||
|
p.setPen(Qt::NoPen);
|
||||||
|
p.setBrush(m_indicator.GetRawInputColor());
|
||||||
|
p.drawPolygon(QPolygonF{{triangle_h, 0.f}, {0.f, -triangle_w_2}, {0.f, +triangle_w_2}});
|
||||||
|
|
||||||
|
p.restore();
|
||||||
|
}
|
||||||
|
|
||||||
|
void CalibrationWidget::DrawInProgressCalibration(QPainter& p, Common::DVec2 point)
|
||||||
|
{
|
||||||
|
if (!IsCalibrating())
|
||||||
|
return;
|
||||||
|
|
||||||
|
const auto elapsed_seconds = GetAnimationElapsedSeconds();
|
||||||
|
|
||||||
|
// Clockwise spinning stick starting from center.
|
||||||
|
p.save();
|
||||||
|
p.rotate(elapsed_seconds * -360.0);
|
||||||
|
DrawPushedStick(
|
||||||
|
p, m_indicator,
|
||||||
|
-QEasingCurve(QEasingCurve::OutCirc).valueForProgress(std::min(elapsed_seconds * 2, 1.0)));
|
||||||
|
p.restore();
|
||||||
|
|
||||||
|
const auto center = m_calibrator->GetCenter();
|
||||||
p.save();
|
p.save();
|
||||||
p.translate(center.x, center.y);
|
p.translate(center.x, center.y);
|
||||||
|
|
||||||
// Input shape.
|
// Input shape.
|
||||||
p.setPen(GetInputShapePen());
|
p.setPen(m_indicator.GetInputShapePen());
|
||||||
p.setBrush(Qt::NoBrush);
|
p.setBrush(Qt::NoBrush);
|
||||||
p.drawPolygon(GetPolygonFromRadiusGetter(
|
p.drawPolygon(GetPolygonFromRadiusGetter(
|
||||||
[this](double angle) { return m_calibration_widget->GetCalibrationRadiusAtAngle(angle); }));
|
[this](double angle) { return m_calibrator->GetCalibrationRadiusAtAngle(angle); }));
|
||||||
|
|
||||||
// Center.
|
// Calibrated center.
|
||||||
if (center.x || center.y)
|
if (center.x || center.y)
|
||||||
{
|
{
|
||||||
p.setPen(GetInputDotPen(GetCenterColor()));
|
p.setPen(GetInputDotPen(m_indicator.GetCenterColor()));
|
||||||
p.drawPoint(QPointF{});
|
p.drawPoint(QPointF{});
|
||||||
}
|
}
|
||||||
|
|
||||||
p.restore();
|
p.restore();
|
||||||
|
|
||||||
// Stick position.
|
// Show the red dot only if the input is at least halfway pressed.
|
||||||
p.setPen(GetInputDotPen(GetAdjustedInputColor()));
|
// The cool spinning stick is otherwise uglified by the red dot always being shown.
|
||||||
p.drawPoint(QPointF{point.x, point.y});
|
if (Common::DVec2{point.x, point.y}.LengthSquared() > (0.5 * 0.5))
|
||||||
|
{
|
||||||
|
p.setPen(GetInputDotPen(m_indicator.GetAdjustedInputColor()));
|
||||||
|
p.drawPoint(QPointF{point.x, point.y});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void ReshapableInputIndicator::UpdateCalibrationWidget(Common::DVec2 point)
|
void ReshapableInputIndicator::UpdateCalibrationWidget(Common::DVec2 point)
|
||||||
{
|
{
|
||||||
if (m_calibration_widget)
|
if (m_calibration_widget != nullptr)
|
||||||
m_calibration_widget->Update(point);
|
m_calibration_widget->Update(point);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ReshapableInputIndicator::IsCalibrating() const
|
bool ReshapableInputIndicator::IsCalibrating() const
|
||||||
{
|
{
|
||||||
return m_calibration_widget && m_calibration_widget->IsCalibrating();
|
return m_calibration_widget != nullptr && m_calibration_widget->IsActive();
|
||||||
}
|
}
|
||||||
|
|
||||||
void ReshapableInputIndicator::SetCalibrationWidget(CalibrationWidget* widget)
|
void ReshapableInputIndicator::SetCalibrationWidget(CalibrationWidget* widget)
|
||||||
@ -958,110 +1031,172 @@ void ReshapableInputIndicator::SetCalibrationWidget(CalibrationWidget* widget)
|
|||||||
m_calibration_widget = widget;
|
m_calibration_widget = widget;
|
||||||
}
|
}
|
||||||
|
|
||||||
CalibrationWidget::CalibrationWidget(ControllerEmu::ReshapableInput& input,
|
CalibrationWidget::~CalibrationWidget() = default;
|
||||||
|
|
||||||
|
CalibrationWidget::CalibrationWidget(MappingWidget& mapping_widget,
|
||||||
|
ControllerEmu::ReshapableInput& input,
|
||||||
ReshapableInputIndicator& indicator)
|
ReshapableInputIndicator& indicator)
|
||||||
: m_input(input), m_indicator(indicator), m_completion_action{}
|
: m_mapping_widget(mapping_widget), m_input(input), m_indicator(indicator)
|
||||||
{
|
{
|
||||||
|
connect(mapping_widget.GetParent(), &MappingWindow::CancelMapping, this,
|
||||||
|
&CalibrationWidget::ResetActions);
|
||||||
|
connect(mapping_widget.GetParent(), &MappingWindow::ConfigChanged, this,
|
||||||
|
&CalibrationWidget::ResetActions);
|
||||||
|
|
||||||
m_indicator.SetCalibrationWidget(this);
|
m_indicator.SetCalibrationWidget(this);
|
||||||
|
|
||||||
// Make it more apparent that this is a menu with more options.
|
// Make it more apparent that this is a menu with more options.
|
||||||
setPopupMode(ToolButtonPopupMode::MenuButtonPopup);
|
setPopupMode(ToolButtonPopupMode::MenuButtonPopup);
|
||||||
|
|
||||||
SetupActions();
|
|
||||||
|
|
||||||
setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Fixed);
|
setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Fixed);
|
||||||
|
|
||||||
m_informative_timer = new QTimer(this);
|
ResetActions();
|
||||||
connect(m_informative_timer, &QTimer::timeout, this, [this] {
|
|
||||||
// If the user has started moving we'll assume they know what they are doing.
|
|
||||||
if (*std::ranges::max_element(m_calibration_data) > 0.5)
|
|
||||||
return;
|
|
||||||
|
|
||||||
ModalMessageBox::information(
|
|
||||||
this, tr("Calibration"),
|
|
||||||
tr("For best results please slowly move your input to all possible regions."));
|
|
||||||
});
|
|
||||||
m_informative_timer->setSingleShot(true);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void CalibrationWidget::SetupActions()
|
void CalibrationWidget::DeleteAllActions()
|
||||||
{
|
{
|
||||||
const auto calibrate_action = new QAction(tr("Calibrate"), this);
|
|
||||||
const auto center_action = new QAction(tr("Center and Calibrate"), this);
|
|
||||||
const auto reset_action = new QAction(tr("Reset"), this);
|
|
||||||
|
|
||||||
connect(calibrate_action, &QAction::triggered, [this] {
|
|
||||||
StartCalibration();
|
|
||||||
m_new_center = Common::DVec2{};
|
|
||||||
});
|
|
||||||
connect(center_action, &QAction::triggered, [this] {
|
|
||||||
StartCalibration();
|
|
||||||
m_new_center = std::nullopt;
|
|
||||||
});
|
|
||||||
connect(reset_action, &QAction::triggered, [this] {
|
|
||||||
m_input.SetCalibrationToDefault();
|
|
||||||
m_input.SetCenter({0, 0});
|
|
||||||
});
|
|
||||||
|
|
||||||
for (auto* action : actions())
|
for (auto* action : actions())
|
||||||
removeAction(action);
|
delete action;
|
||||||
|
}
|
||||||
|
|
||||||
|
void CalibrationWidget::ResetActions()
|
||||||
|
{
|
||||||
|
m_calibrator.reset();
|
||||||
|
m_mapper.reset();
|
||||||
|
|
||||||
|
// i18n: A button to start the process of game controller analog stick mapping and calibration.
|
||||||
|
auto* const map_and_calibrate_action = new QAction(tr("Map and Calibrate"), this);
|
||||||
|
|
||||||
|
// i18n: A button to start the process of game controller analog stick calibration.
|
||||||
|
auto* const calibrate_action = new QAction(tr("Calibrate"), this);
|
||||||
|
|
||||||
|
// i18n: A button to calibrate the center and extremities of a game controller analog stick.
|
||||||
|
auto* const center_action = new QAction(tr("Center and Calibrate"), this);
|
||||||
|
|
||||||
|
// i18n: A button to reset game controller analog stick calibration.
|
||||||
|
auto* const reset_action = new QAction(tr("Reset Calibration"), this);
|
||||||
|
|
||||||
|
connect(map_and_calibrate_action, &QAction::triggered, this,
|
||||||
|
&CalibrationWidget::StartMappingAndCalibration);
|
||||||
|
connect(calibrate_action, &QAction::triggered, this, [this]() { StartCalibration(); });
|
||||||
|
connect(center_action, &QAction::triggered, this, [this]() { StartCalibration(std::nullopt); });
|
||||||
|
connect(reset_action, &QAction::triggered, this, [this]() {
|
||||||
|
const auto lock = m_mapping_widget.GetController()->GetStateLock();
|
||||||
|
m_input.SetCalibrationToDefault();
|
||||||
|
m_input.SetCenter({});
|
||||||
|
});
|
||||||
|
|
||||||
|
DeleteAllActions();
|
||||||
|
|
||||||
|
addAction(map_and_calibrate_action);
|
||||||
addAction(calibrate_action);
|
addAction(calibrate_action);
|
||||||
addAction(center_action);
|
addAction(center_action);
|
||||||
addAction(reset_action);
|
addAction(reset_action);
|
||||||
setDefaultAction(calibrate_action);
|
|
||||||
|
|
||||||
m_completion_action = new QAction(tr("Finish Calibration"), this);
|
setDefaultAction(map_and_calibrate_action);
|
||||||
connect(m_completion_action, &QAction::triggered, [this] {
|
|
||||||
m_input.SetCenter(GetCenter());
|
|
||||||
m_input.SetCalibrationData(std::move(m_calibration_data));
|
|
||||||
m_informative_timer->stop();
|
|
||||||
SetupActions();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void CalibrationWidget::StartCalibration()
|
void CalibrationWidget::StartMappingAndCalibration()
|
||||||
{
|
{
|
||||||
m_prev_point = {};
|
RestartAnimation();
|
||||||
m_calibration_data.assign(m_input.CALIBRATION_SAMPLE_COUNT, 0.0);
|
|
||||||
|
|
||||||
// Cancel calibration.
|
// i18n: A button to stop a game controller button mapping process.
|
||||||
const auto cancel_action = new QAction(tr("Cancel Calibration"), this);
|
auto* const cancel_action = new QAction(tr("Cancel Mapping"), this);
|
||||||
connect(cancel_action, &QAction::triggered, [this] {
|
connect(cancel_action, &QAction::triggered, this, &CalibrationWidget::ResetActions);
|
||||||
m_calibration_data.clear();
|
|
||||||
m_informative_timer->stop();
|
|
||||||
SetupActions();
|
|
||||||
});
|
|
||||||
|
|
||||||
for (auto* action : actions())
|
DeleteAllActions();
|
||||||
removeAction(action);
|
|
||||||
|
|
||||||
addAction(cancel_action);
|
addAction(cancel_action);
|
||||||
addAction(m_completion_action);
|
|
||||||
setDefaultAction(cancel_action);
|
setDefaultAction(cancel_action);
|
||||||
|
|
||||||
// If the user doesn't seem to know what they are doing after a bit inform them.
|
auto* const window = m_mapping_widget.GetParent();
|
||||||
m_informative_timer->start(2000);
|
const auto& default_device = window->GetController()->GetDefaultDevice();
|
||||||
|
|
||||||
|
std::vector device_strings{default_device.ToString()};
|
||||||
|
if (window->IsCreateOtherDeviceMappingsEnabled())
|
||||||
|
device_strings = g_controller_interface.GetAllDeviceStrings();
|
||||||
|
|
||||||
|
const auto lock = window->GetController()->GetStateLock();
|
||||||
|
m_mapper = std::make_unique<ciface::MappingCommon::ReshapableInputMapper>(g_controller_interface,
|
||||||
|
device_strings);
|
||||||
|
}
|
||||||
|
|
||||||
|
void CalibrationWidget::StartCalibration(std::optional<Common::DVec2> center)
|
||||||
|
{
|
||||||
|
RestartAnimation();
|
||||||
|
m_calibrator = std::make_unique<ciface::MappingCommon::CalibrationBuilder>(center);
|
||||||
|
|
||||||
|
// i18n: A button to abort a game controller calibration process.
|
||||||
|
auto* const cancel_action = new QAction(tr("Cancel Calibration"), this);
|
||||||
|
connect(cancel_action, &QAction::triggered, this, &CalibrationWidget::ResetActions);
|
||||||
|
|
||||||
|
// i18n: A button to finalize a game controller calibration process.
|
||||||
|
auto* const finish_action = new QAction(tr("Finish Calibration"), this);
|
||||||
|
connect(finish_action, &QAction::triggered, this, [this]() {
|
||||||
|
const auto lock = m_mapping_widget.GetController()->GetStateLock();
|
||||||
|
m_calibrator->ApplyResults(&m_input);
|
||||||
|
ResetActions();
|
||||||
|
});
|
||||||
|
connect(this, &CalibrationWidget::CalibrationIsSensible, finish_action,
|
||||||
|
[this, finish_action]() { setDefaultAction(finish_action); });
|
||||||
|
|
||||||
|
DeleteAllActions();
|
||||||
|
|
||||||
|
addAction(finish_action);
|
||||||
|
addAction(cancel_action);
|
||||||
|
setDefaultAction(cancel_action);
|
||||||
}
|
}
|
||||||
|
|
||||||
void CalibrationWidget::Update(Common::DVec2 point)
|
void CalibrationWidget::Update(Common::DVec2 point)
|
||||||
{
|
{
|
||||||
|
// FYI: The "StateLock" is always held when this is called.
|
||||||
|
|
||||||
QFont f = parentWidget()->font();
|
QFont f = parentWidget()->font();
|
||||||
QPalette p = parentWidget()->palette();
|
QPalette p = parentWidget()->palette();
|
||||||
|
|
||||||
// Use current point if center is being calibrated.
|
if (IsMapping())
|
||||||
if (!m_new_center.has_value())
|
|
||||||
m_new_center = point;
|
|
||||||
|
|
||||||
if (IsCalibrating())
|
|
||||||
{
|
{
|
||||||
const auto new_point = point - *m_new_center;
|
if (m_mapper->Update())
|
||||||
m_input.UpdateCalibrationData(m_calibration_data, m_prev_point, new_point);
|
|
||||||
m_prev_point = new_point;
|
|
||||||
|
|
||||||
if (IsCalibrationDataSensible(m_calibration_data))
|
|
||||||
{
|
{
|
||||||
setDefaultAction(m_completion_action);
|
// Restart the animation for the next direction when progress is made.
|
||||||
|
RestartAnimation();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_mapper->IsComplete())
|
||||||
|
{
|
||||||
|
const bool needs_calibration = m_mapper->IsCalibrationNeeded();
|
||||||
|
|
||||||
|
if (m_mapper->ApplyResults(m_mapping_widget.GetController(), &m_input))
|
||||||
|
{
|
||||||
|
emit m_mapping_widget.ConfigChanged();
|
||||||
|
|
||||||
|
if (needs_calibration)
|
||||||
|
{
|
||||||
|
StartCalibration();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Load square calibration for digital inputs.
|
||||||
|
m_input.SetCalibrationFromGate(ControllerEmu::SquareStickGate{1});
|
||||||
|
m_input.SetCenter({});
|
||||||
|
|
||||||
|
ResetActions();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ResetActions();
|
||||||
|
}
|
||||||
|
|
||||||
|
m_mapper.reset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (IsCalibrating())
|
||||||
|
{
|
||||||
|
m_calibrator->Update(point);
|
||||||
|
if (m_calibrator->IsCalibrationDataSensible())
|
||||||
|
{
|
||||||
|
emit CalibrationIsSensible();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (IsPointOutsideCalibration(point, m_input))
|
else if (IsPointOutsideCalibration(point, m_input))
|
||||||
@ -1075,17 +1210,17 @@ void CalibrationWidget::Update(Common::DVec2 point)
|
|||||||
setPalette(p);
|
setPalette(p);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool CalibrationWidget::IsActive() const
|
||||||
|
{
|
||||||
|
return IsMapping() || IsCalibrating();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool CalibrationWidget::IsMapping() const
|
||||||
|
{
|
||||||
|
return m_mapper != nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
bool CalibrationWidget::IsCalibrating() const
|
bool CalibrationWidget::IsCalibrating() const
|
||||||
{
|
{
|
||||||
return !m_calibration_data.empty();
|
return m_calibrator != nullptr;
|
||||||
}
|
|
||||||
|
|
||||||
double CalibrationWidget::GetCalibrationRadiusAtAngle(double angle) const
|
|
||||||
{
|
|
||||||
return m_input.GetCalibrationDataRadiusAtAngle(m_calibration_data, angle);
|
|
||||||
}
|
|
||||||
|
|
||||||
Common::DVec2 CalibrationWidget::GetCenter() const
|
|
||||||
{
|
|
||||||
return m_new_center.value_or(Common::DVec2{});
|
|
||||||
}
|
}
|
||||||
|
@ -26,6 +26,13 @@ class QPaintEvent;
|
|||||||
class QTimer;
|
class QTimer;
|
||||||
|
|
||||||
class CalibrationWidget;
|
class CalibrationWidget;
|
||||||
|
class MappingWidget;
|
||||||
|
|
||||||
|
namespace ciface::MappingCommon
|
||||||
|
{
|
||||||
|
class ReshapableInputMapper;
|
||||||
|
class CalibrationBuilder;
|
||||||
|
} // namespace ciface::MappingCommon
|
||||||
|
|
||||||
class MappingIndicator : public QWidget
|
class MappingIndicator : public QWidget
|
||||||
{
|
{
|
||||||
@ -79,15 +86,16 @@ class ReshapableInputIndicator : public SquareIndicator
|
|||||||
public:
|
public:
|
||||||
void SetCalibrationWidget(CalibrationWidget* widget);
|
void SetCalibrationWidget(CalibrationWidget* widget);
|
||||||
|
|
||||||
|
virtual QColor GetGateBrushColor() const = 0;
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
void DrawReshapableInput(ControllerEmu::ReshapableInput& group, QColor gate_color,
|
void DrawReshapableInput(ControllerEmu::ReshapableInput& group,
|
||||||
std::optional<ControllerEmu::ReshapableInput::ReshapeData> adj_coord);
|
std::optional<ControllerEmu::ReshapableInput::ReshapeData> adj_coord);
|
||||||
|
|
||||||
virtual void DrawUnderGate(QPainter&) {}
|
virtual void DrawUnderGate(QPainter&) {}
|
||||||
|
|
||||||
bool IsCalibrating() const;
|
bool IsCalibrating() const;
|
||||||
|
|
||||||
void DrawCalibration(QPainter& p, Common::DVec2 point);
|
|
||||||
void UpdateCalibrationWidget(Common::DVec2 point);
|
void UpdateCalibrationWidget(Common::DVec2 point);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
@ -99,6 +107,8 @@ class AnalogStickIndicator : public ReshapableInputIndicator
|
|||||||
public:
|
public:
|
||||||
explicit AnalogStickIndicator(ControllerEmu::ReshapableInput& stick) : m_group(stick) {}
|
explicit AnalogStickIndicator(ControllerEmu::ReshapableInput& stick) : m_group(stick) {}
|
||||||
|
|
||||||
|
QColor GetGateBrushColor() const final;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void Draw() override;
|
void Draw() override;
|
||||||
|
|
||||||
@ -110,6 +120,8 @@ class TiltIndicator : public ReshapableInputIndicator
|
|||||||
public:
|
public:
|
||||||
explicit TiltIndicator(ControllerEmu::Tilt& tilt) : m_group(tilt) {}
|
explicit TiltIndicator(ControllerEmu::Tilt& tilt) : m_group(tilt) {}
|
||||||
|
|
||||||
|
QColor GetGateBrushColor() const final;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void Draw() override;
|
void Draw() override;
|
||||||
void Update(float elapsed_seconds) override;
|
void Update(float elapsed_seconds) override;
|
||||||
@ -123,6 +135,8 @@ class CursorIndicator : public ReshapableInputIndicator
|
|||||||
public:
|
public:
|
||||||
explicit CursorIndicator(ControllerEmu::Cursor& cursor) : m_cursor_group(cursor) {}
|
explicit CursorIndicator(ControllerEmu::Cursor& cursor) : m_cursor_group(cursor) {}
|
||||||
|
|
||||||
|
QColor GetGateBrushColor() const final;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void Draw() override;
|
void Draw() override;
|
||||||
|
|
||||||
@ -145,6 +159,8 @@ class SwingIndicator : public ReshapableInputIndicator
|
|||||||
public:
|
public:
|
||||||
explicit SwingIndicator(ControllerEmu::Force& swing) : m_swing_group(swing) {}
|
explicit SwingIndicator(ControllerEmu::Force& swing) : m_swing_group(swing) {}
|
||||||
|
|
||||||
|
QColor GetGateBrushColor() const final;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void Draw() override;
|
void Draw() override;
|
||||||
void Update(float elapsed_seconds) override;
|
void Update(float elapsed_seconds) override;
|
||||||
@ -219,28 +235,46 @@ private:
|
|||||||
|
|
||||||
ControllerEmu::IRPassthrough& m_ir_group;
|
ControllerEmu::IRPassthrough& m_ir_group;
|
||||||
};
|
};
|
||||||
|
|
||||||
class CalibrationWidget : public QToolButton
|
class CalibrationWidget : public QToolButton
|
||||||
{
|
{
|
||||||
|
Q_OBJECT
|
||||||
public:
|
public:
|
||||||
CalibrationWidget(ControllerEmu::ReshapableInput& input, ReshapableInputIndicator& indicator);
|
CalibrationWidget(MappingWidget& mapping_widget, ControllerEmu::ReshapableInput& input,
|
||||||
|
ReshapableInputIndicator& indicator);
|
||||||
|
~CalibrationWidget() override;
|
||||||
|
|
||||||
void Update(Common::DVec2 point);
|
void Update(Common::DVec2 point);
|
||||||
|
|
||||||
double GetCalibrationRadiusAtAngle(double angle) const;
|
void Draw(QPainter& p, Common::DVec2 point);
|
||||||
|
|
||||||
Common::DVec2 GetCenter() const;
|
bool IsActive() const;
|
||||||
|
|
||||||
bool IsCalibrating() const;
|
signals:
|
||||||
|
void CalibrationIsSensible();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void StartCalibration();
|
void DrawInProgressMapping(QPainter& p);
|
||||||
void SetupActions();
|
void DrawInProgressCalibration(QPainter& p, Common::DVec2 point);
|
||||||
|
|
||||||
|
bool IsMapping() const;
|
||||||
|
bool IsCalibrating() const;
|
||||||
|
|
||||||
|
void StartMappingAndCalibration();
|
||||||
|
void StartCalibration(std::optional<Common::DVec2> center = Common::DVec2{});
|
||||||
|
|
||||||
|
void ResetActions();
|
||||||
|
void DeleteAllActions();
|
||||||
|
|
||||||
|
MappingWidget& m_mapping_widget;
|
||||||
ControllerEmu::ReshapableInput& m_input;
|
ControllerEmu::ReshapableInput& m_input;
|
||||||
ReshapableInputIndicator& m_indicator;
|
ReshapableInputIndicator& m_indicator;
|
||||||
QAction* m_completion_action;
|
|
||||||
ControllerEmu::ReshapableInput::CalibrationData m_calibration_data;
|
std::unique_ptr<ciface::MappingCommon::ReshapableInputMapper> m_mapper;
|
||||||
QTimer* m_informative_timer;
|
std::unique_ptr<ciface::MappingCommon::CalibrationBuilder> m_calibrator;
|
||||||
std::optional<Common::DVec2> m_new_center;
|
|
||||||
Common::DVec2 m_prev_point;
|
double GetAnimationElapsedSeconds() const;
|
||||||
|
void RestartAnimation();
|
||||||
|
|
||||||
|
Clock::time_point m_animation_start_time{};
|
||||||
};
|
};
|
||||||
|
@ -152,7 +152,7 @@ QGroupBox* MappingWidget::CreateGroupBox(const QString& name, ControllerEmu::Con
|
|||||||
if (need_calibration)
|
if (need_calibration)
|
||||||
{
|
{
|
||||||
const auto calibrate =
|
const auto calibrate =
|
||||||
new CalibrationWidget(*static_cast<ControllerEmu::ReshapableInput*>(group),
|
new CalibrationWidget(*this, *static_cast<ControllerEmu::ReshapableInput*>(group),
|
||||||
*static_cast<ReshapableInputIndicator*>(indicator));
|
*static_cast<ReshapableInputIndicator*>(indicator));
|
||||||
|
|
||||||
form_layout->addRow(calibrate);
|
form_layout->addRow(calibrate);
|
||||||
|
@ -246,7 +246,7 @@
|
|||||||
<ClInclude Include="Config\ConfigControls\ConfigControl.h" />
|
<ClInclude Include="Config\ConfigControls\ConfigControl.h" />
|
||||||
<ClInclude Include="Config\GameConfigEdit.h" />
|
<ClInclude Include="Config\GameConfigEdit.h" />
|
||||||
<ClInclude Include="Config\Mapping\MappingCommon.h" />
|
<ClInclude Include="Config\Mapping\MappingCommon.h" />
|
||||||
<ClInclude Include="Config\Mapping\MappingIndicator.h" />
|
<QtMoc Include="Config\Mapping\MappingIndicator.h" />
|
||||||
<ClInclude Include="Config\Mapping\MappingNumeric.h" />
|
<ClInclude Include="Config\Mapping\MappingNumeric.h" />
|
||||||
<ClInclude Include="Config\NewPatchDialog.h" />
|
<ClInclude Include="Config\NewPatchDialog.h" />
|
||||||
<QtMoc Include="Config\PatchesWidget.h" />
|
<QtMoc Include="Config\PatchesWidget.h" />
|
||||||
|
@ -391,7 +391,7 @@ InputDetector::InputDetector() : m_start_time{}, m_state{}
|
|||||||
}
|
}
|
||||||
|
|
||||||
void InputDetector::Start(const DeviceContainer& container,
|
void InputDetector::Start(const DeviceContainer& container,
|
||||||
const std::vector<std::string>& device_strings)
|
std::span<const std::string> device_strings)
|
||||||
|
|
||||||
{
|
{
|
||||||
m_start_time = Clock::now();
|
m_start_time = Clock::now();
|
||||||
|
@ -8,6 +8,7 @@
|
|||||||
#include <memory>
|
#include <memory>
|
||||||
#include <mutex>
|
#include <mutex>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
|
#include <span>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <string_view>
|
#include <string_view>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
@ -217,6 +218,8 @@ public:
|
|||||||
Clock::time_point press_time;
|
Clock::time_point press_time;
|
||||||
std::optional<Clock::time_point> release_time;
|
std::optional<Clock::time_point> release_time;
|
||||||
ControlState smoothness = 0;
|
ControlState smoothness = 0;
|
||||||
|
|
||||||
|
bool IsAnalogPress() const { return smoothness > 1.00001; }
|
||||||
};
|
};
|
||||||
|
|
||||||
Device::Input* FindInput(std::string_view name, const Device* def_dev) const;
|
Device::Input* FindInput(std::string_view name, const Device* def_dev) const;
|
||||||
@ -253,7 +256,7 @@ public:
|
|||||||
InputDetector();
|
InputDetector();
|
||||||
~InputDetector();
|
~InputDetector();
|
||||||
|
|
||||||
void Start(const DeviceContainer& container, const std::vector<std::string>& device_strings);
|
void Start(const DeviceContainer& container, std::span<const std::string> device_strings);
|
||||||
void Update(std::chrono::milliseconds initial_wait, std::chrono::milliseconds confirmation_wait,
|
void Update(std::chrono::milliseconds initial_wait, std::chrono::milliseconds confirmation_wait,
|
||||||
std::chrono::milliseconds maximum_wait);
|
std::chrono::milliseconds maximum_wait);
|
||||||
bool IsComplete() const;
|
bool IsComplete() const;
|
||||||
|
@ -5,14 +5,21 @@
|
|||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
|
#include <ranges>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include <fmt/format.h>
|
#include <fmt/format.h>
|
||||||
#include <fmt/ranges.h>
|
#include <fmt/ranges.h>
|
||||||
|
|
||||||
|
#include "Common/MathUtil.h"
|
||||||
#include "Common/StringUtil.h"
|
#include "Common/StringUtil.h"
|
||||||
|
|
||||||
|
#include "InputCommon/ControllerEmu/ControllerEmu.h"
|
||||||
|
#include "InputCommon/ControllerEmu/StickGate.h"
|
||||||
|
#include "InputCommon/ControllerInterface/ControllerInterface.h"
|
||||||
#include "InputCommon/ControllerInterface/CoreDevice.h"
|
#include "InputCommon/ControllerInterface/CoreDevice.h"
|
||||||
|
#include "InputCommon/InputConfig.h"
|
||||||
|
|
||||||
namespace ciface::MappingCommon
|
namespace ciface::MappingCommon
|
||||||
{
|
{
|
||||||
@ -139,7 +146,7 @@ void RemoveSpuriousTriggerCombinations(Core::InputDetector::Results* detections)
|
|||||||
const auto is_spurious = [&](const auto& detection) {
|
const auto is_spurious = [&](const auto& detection) {
|
||||||
return std::ranges::any_of(*detections, [&](const auto& d) {
|
return std::ranges::any_of(*detections, [&](const auto& d) {
|
||||||
// This is a spurious digital detection if a "smooth" (analog) detection is temporally near.
|
// This is a spurious digital detection if a "smooth" (analog) detection is temporally near.
|
||||||
return &d != &detection && d.smoothness > 1 && d.smoothness > detection.smoothness &&
|
return &d != &detection && d.IsAnalogPress() && !detection.IsAnalogPress() &&
|
||||||
abs(d.press_time - detection.press_time) < SPURIOUS_TRIGGER_COMBO_THRESHOLD;
|
abs(d.press_time - detection.press_time) < SPURIOUS_TRIGGER_COMBO_THRESHOLD;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@ -163,4 +170,127 @@ bool ContainsCompleteDetection(const Core::InputDetector::Results& results)
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ReshapableInputMapper::ReshapableInputMapper(const Core::DeviceContainer& container,
|
||||||
|
std::span<const std::string> device_strings)
|
||||||
|
{
|
||||||
|
m_input_detector.Start(container, device_strings);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ReshapableInputMapper::Update()
|
||||||
|
{
|
||||||
|
const auto prev_size = m_input_detector.GetResults().size();
|
||||||
|
|
||||||
|
constexpr auto wait_time = std::chrono::seconds{4};
|
||||||
|
m_input_detector.Update(wait_time, wait_time, wait_time * REQUIRED_INPUT_COUNT);
|
||||||
|
|
||||||
|
return m_input_detector.GetResults().size() != prev_size;
|
||||||
|
}
|
||||||
|
|
||||||
|
float ReshapableInputMapper::GetCurrentAngle() const
|
||||||
|
{
|
||||||
|
constexpr auto quarter_circle = float(MathUtil::TAU) * 0.25f;
|
||||||
|
return quarter_circle - (float(m_input_detector.GetResults().size()) * quarter_circle);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ReshapableInputMapper::IsComplete() const
|
||||||
|
{
|
||||||
|
return m_input_detector.GetResults().size() >= REQUIRED_INPUT_COUNT ||
|
||||||
|
m_input_detector.IsComplete();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ReshapableInputMapper::IsCalibrationNeeded() const
|
||||||
|
{
|
||||||
|
return std::ranges::any_of(m_input_detector.GetResults() | std::views::take(REQUIRED_INPUT_COUNT),
|
||||||
|
&ciface::Core::InputDetector::Detection::IsAnalogPress);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ReshapableInputMapper::ApplyResults(ControllerEmu::EmulatedController* controller,
|
||||||
|
ControllerEmu::ReshapableInput* stick)
|
||||||
|
{
|
||||||
|
auto const detections = m_input_detector.TakeResults();
|
||||||
|
|
||||||
|
if (detections.size() < REQUIRED_INPUT_COUNT)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// Transpose URDL to UDLR.
|
||||||
|
const std::array results{detections[0], detections[2], detections[3], detections[1]};
|
||||||
|
|
||||||
|
const auto default_device = controller->GetDefaultDevice();
|
||||||
|
|
||||||
|
for (std::size_t i = 0; i != results.size(); ++i)
|
||||||
|
{
|
||||||
|
ciface::Core::DeviceQualifier device_qualifier;
|
||||||
|
device_qualifier.FromDevice(results[i].device.get());
|
||||||
|
|
||||||
|
stick->controls[i]->control_ref->SetExpression(ciface::MappingCommon::GetExpressionForControl(
|
||||||
|
results[i].input->GetName(), device_qualifier, default_device,
|
||||||
|
ciface::MappingCommon::Quote::On));
|
||||||
|
|
||||||
|
controller->UpdateSingleControlReference(g_controller_interface,
|
||||||
|
stick->controls[i]->control_ref.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
controller->GetConfig()->GenerateControllerTextures();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
CalibrationBuilder::CalibrationBuilder(std::optional<Common::DVec2> center)
|
||||||
|
: m_calibration_data(ControllerEmu::ReshapableInput::CALIBRATION_SAMPLE_COUNT, 0.0),
|
||||||
|
m_center{center}
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
void CalibrationBuilder::Update(Common::DVec2 point)
|
||||||
|
{
|
||||||
|
if (!m_center.has_value())
|
||||||
|
m_center = point;
|
||||||
|
|
||||||
|
const auto new_point = point - *m_center;
|
||||||
|
ControllerEmu::ReshapableInput::UpdateCalibrationData(m_calibration_data, m_prev_point,
|
||||||
|
new_point);
|
||||||
|
m_prev_point = new_point;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool CalibrationBuilder::IsCalibrationDataSensible() const
|
||||||
|
{
|
||||||
|
// Even the GC controller's small range would pass this test.
|
||||||
|
constexpr double REASONABLE_AVERAGE_RADIUS = 0.6;
|
||||||
|
|
||||||
|
// Test that the average input radius is not below a threshold.
|
||||||
|
// This will make sure the user has actually moved their stick from neutral.
|
||||||
|
|
||||||
|
MathUtil::RunningVariance<ControlState> stats;
|
||||||
|
|
||||||
|
for (const auto x : m_calibration_data)
|
||||||
|
stats.Push(x);
|
||||||
|
|
||||||
|
if (stats.Mean() < REASONABLE_AVERAGE_RADIUS)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// Test that the standard deviation is below a threshold.
|
||||||
|
// This will make sure the user has not just filled in one side of their input.
|
||||||
|
|
||||||
|
// Approx. deviation of a square input gate, anything much more than that would be unusual.
|
||||||
|
constexpr double REASONABLE_DEVIATION = 0.14;
|
||||||
|
|
||||||
|
return stats.StandardDeviation() < REASONABLE_DEVIATION;
|
||||||
|
}
|
||||||
|
|
||||||
|
ControlState CalibrationBuilder::GetCalibrationRadiusAtAngle(double angle) const
|
||||||
|
{
|
||||||
|
return ControllerEmu::ReshapableInput::GetCalibrationDataRadiusAtAngle(m_calibration_data, angle);
|
||||||
|
}
|
||||||
|
|
||||||
|
void CalibrationBuilder::ApplyResults(ControllerEmu::ReshapableInput* stick)
|
||||||
|
{
|
||||||
|
stick->SetCenter(GetCenter());
|
||||||
|
stick->SetCalibrationData(std::move(m_calibration_data));
|
||||||
|
}
|
||||||
|
|
||||||
|
Common::DVec2 CalibrationBuilder::GetCenter() const
|
||||||
|
{
|
||||||
|
return m_center.value_or(Common::DVec2{});
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace ciface::MappingCommon
|
} // namespace ciface::MappingCommon
|
||||||
|
@ -5,8 +5,15 @@
|
|||||||
|
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
|
#include "Common/Matrix.h"
|
||||||
|
#include "InputCommon/ControllerEmu/StickGate.h"
|
||||||
#include "InputCommon/ControllerInterface/CoreDevice.h"
|
#include "InputCommon/ControllerInterface/CoreDevice.h"
|
||||||
|
|
||||||
|
namespace ControllerEmu
|
||||||
|
{
|
||||||
|
class EmulatedController;
|
||||||
|
} // namespace ControllerEmu
|
||||||
|
|
||||||
namespace ciface::MappingCommon
|
namespace ciface::MappingCommon
|
||||||
{
|
{
|
||||||
enum class Quote
|
enum class Quote
|
||||||
@ -27,4 +34,73 @@ void RemoveSpuriousTriggerCombinations(Core::InputDetector::Results*);
|
|||||||
void RemoveDetectionsAfterTimePoint(Core::InputDetector::Results*, Clock::time_point after);
|
void RemoveDetectionsAfterTimePoint(Core::InputDetector::Results*, Clock::time_point after);
|
||||||
bool ContainsCompleteDetection(const Core::InputDetector::Results&);
|
bool ContainsCompleteDetection(const Core::InputDetector::Results&);
|
||||||
|
|
||||||
|
// class for detecting four directional input mappings in sequence.
|
||||||
|
class ReshapableInputMapper
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
// Four cardinal directions.
|
||||||
|
static constexpr std::size_t REQUIRED_INPUT_COUNT = 4;
|
||||||
|
|
||||||
|
// Caller should hold the "StateLock".
|
||||||
|
ReshapableInputMapper(const Core::DeviceContainer& container,
|
||||||
|
std::span<const std::string> device_strings);
|
||||||
|
|
||||||
|
// Reads inputs and updates internal state.
|
||||||
|
// Returns true if an input was detected in this call.
|
||||||
|
// (useful for UI animation)
|
||||||
|
// Caller should hold the "StateLock".
|
||||||
|
bool Update();
|
||||||
|
|
||||||
|
// A counter-clockwise angle in radians for the currently desired input direction.
|
||||||
|
// Used for a graphical indicator in the UI.
|
||||||
|
// 0 == East
|
||||||
|
float GetCurrentAngle() const;
|
||||||
|
|
||||||
|
// True if all four directions have been detected or the timer expired.
|
||||||
|
bool IsComplete() const;
|
||||||
|
|
||||||
|
// Returns true if "analog" inputs were detected and calibration should be performed.
|
||||||
|
// Must use *before* ApplyResults.
|
||||||
|
bool IsCalibrationNeeded() const;
|
||||||
|
|
||||||
|
// Use when IsComplete returns true.
|
||||||
|
// Updates the mappings on the provided ReshapableInput.
|
||||||
|
// Caller should hold the "StateLock".
|
||||||
|
bool ApplyResults(ControllerEmu::EmulatedController*, ControllerEmu::ReshapableInput* stick);
|
||||||
|
|
||||||
|
private:
|
||||||
|
Core::InputDetector m_input_detector;
|
||||||
|
};
|
||||||
|
|
||||||
|
class CalibrationBuilder
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
// Provide nullopt if you want to calibrate the center on first Update.
|
||||||
|
explicit CalibrationBuilder(std::optional<Common::DVec2> center = Common::DVec2{});
|
||||||
|
|
||||||
|
// Updates the calibration data using the provided point and the previous point.
|
||||||
|
void Update(Common::DVec2 point);
|
||||||
|
|
||||||
|
// Returns true when the calibration data seems to be reasonably filled in.
|
||||||
|
// Used to update the UI to encourage the user to click the "Finish" button.
|
||||||
|
bool IsCalibrationDataSensible() const;
|
||||||
|
|
||||||
|
// Grabs the calibration value at the provided angle.
|
||||||
|
// Used to render the calibration in the UI while it's in progress.
|
||||||
|
ControlState GetCalibrationRadiusAtAngle(double angle) const;
|
||||||
|
|
||||||
|
// Sets the calibration data of the provided ReshapableInput.
|
||||||
|
// Caller should hold the "StateLock".
|
||||||
|
void ApplyResults(ControllerEmu::ReshapableInput* stick);
|
||||||
|
|
||||||
|
Common::DVec2 GetCenter() const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
ControllerEmu::ReshapableInput::CalibrationData m_calibration_data;
|
||||||
|
|
||||||
|
std::optional<Common::DVec2> m_center = std::nullopt;
|
||||||
|
|
||||||
|
Common::DVec2 m_prev_point{};
|
||||||
|
};
|
||||||
|
|
||||||
} // namespace ciface::MappingCommon
|
} // namespace ciface::MappingCommon
|
||||||
|
Reference in New Issue
Block a user