mirror of
https://github.com/dolphin-emu/dolphin.git
synced 2025-07-23 14:19:46 -06:00
Merge pull request #7792 from jordan-woyak/auto-calibration
DolphinQt/ControllerEmu: Add stick calibration "wizard".
This commit is contained in:
@ -6,7 +6,11 @@
|
||||
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <numeric>
|
||||
|
||||
#include <QAction>
|
||||
#include <QDateTime>
|
||||
#include <QMessageBox>
|
||||
#include <QPainter>
|
||||
#include <QTimer>
|
||||
|
||||
@ -48,9 +52,9 @@ MappingIndicator::MappingIndicator(ControllerEmu::ControlGroup* group) : m_group
|
||||
{
|
||||
setMinimumHeight(128);
|
||||
|
||||
m_timer = new QTimer(this);
|
||||
connect(m_timer, &QTimer::timeout, this, [this] { repaint(); });
|
||||
m_timer->start(1000 / 30);
|
||||
const auto timer = new QTimer(this);
|
||||
connect(timer, &QTimer::timeout, this, [this] { repaint(); });
|
||||
timer->start(1000 / 30);
|
||||
}
|
||||
|
||||
namespace
|
||||
@ -75,6 +79,49 @@ QPolygonF GetPolygonFromRadiusGetter(F&& radius_getter, double scale)
|
||||
|
||||
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;
|
||||
|
||||
const double sum = std::accumulate(data.begin(), data.end(), 0.0);
|
||||
const double mean = sum / data.size();
|
||||
|
||||
if (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;
|
||||
|
||||
// Population standard deviation.
|
||||
const double square_sum = std::inner_product(data.begin(), data.end(), data.begin(), 0.0);
|
||||
const double standard_deviation = std::sqrt(square_sum / data.size() - mean * mean);
|
||||
|
||||
return standard_deviation < REASONABLE_DEVIATION;
|
||||
}
|
||||
|
||||
// Used to test for a miscalibrated stick so the user can be informed.
|
||||
bool IsPointOutsideCalibration(Common::DVec2 point, ControllerEmu::ReshapableInput& input)
|
||||
{
|
||||
const double current_radius = point.Length();
|
||||
const double input_radius =
|
||||
input.GetInputRadiusAtAngle(std::atan2(point.y, point.x) + MathUtil::TAU);
|
||||
|
||||
constexpr double ALLOWED_ERROR = 1.3;
|
||||
|
||||
return current_radius > input_radius * ALLOWED_ERROR;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void MappingIndicator::DrawCursor(ControllerEmu::Cursor& cursor)
|
||||
@ -89,6 +136,8 @@ void MappingIndicator::DrawCursor(ControllerEmu::Cursor& cursor)
|
||||
const auto adj_coord = cursor.GetState(true);
|
||||
Settings::Instance().SetControllerStateNeeded(false);
|
||||
|
||||
UpdateCalibrationWidget({raw_coord.x, raw_coord.y});
|
||||
|
||||
// Bounding box size:
|
||||
const double scale = height() / 2.5;
|
||||
|
||||
@ -107,6 +156,12 @@ void MappingIndicator::DrawCursor(ControllerEmu::Cursor& cursor)
|
||||
p.setRenderHint(QPainter::Antialiasing, true);
|
||||
p.setRenderHint(QPainter::SmoothPixmapTransform, true);
|
||||
|
||||
if (IsCalibrating())
|
||||
{
|
||||
DrawCalibration(p, {raw_coord.x, raw_coord.y});
|
||||
return;
|
||||
}
|
||||
|
||||
// Deadzone for Z (forward/backward):
|
||||
const double deadzone = cursor.numeric_settings[cursor.SETTING_DEADZONE]->GetValue();
|
||||
if (deadzone > 0.0)
|
||||
@ -198,6 +253,8 @@ void MappingIndicator::DrawReshapableInput(ControllerEmu::ReshapableInput& stick
|
||||
const auto adj_coord = stick.GetReshapableState(true);
|
||||
Settings::Instance().SetControllerStateNeeded(false);
|
||||
|
||||
UpdateCalibrationWidget(raw_coord);
|
||||
|
||||
// Bounding box size:
|
||||
const double scale = height() / 2.5;
|
||||
|
||||
@ -216,6 +273,12 @@ void MappingIndicator::DrawReshapableInput(ControllerEmu::ReshapableInput& stick
|
||||
p.setRenderHint(QPainter::Antialiasing, true);
|
||||
p.setRenderHint(QPainter::SmoothPixmapTransform, true);
|
||||
|
||||
if (IsCalibrating())
|
||||
{
|
||||
DrawCalibration(p, raw_coord);
|
||||
return;
|
||||
}
|
||||
|
||||
// Input gate. (i.e. the octagon shape)
|
||||
p.setPen(gate_pen_color);
|
||||
p.setBrush(gate_brush_color);
|
||||
@ -363,3 +426,149 @@ void MappingIndicator::paintEvent(QPaintEvent*)
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void MappingIndicator::DrawCalibration(QPainter& p, Common::DVec2 point)
|
||||
{
|
||||
// TODO: Ugly magic number used in a few places in this file.
|
||||
const double scale = height() / 2.5;
|
||||
|
||||
// Input shape.
|
||||
p.setPen(INPUT_SHAPE_PEN);
|
||||
p.setBrush(Qt::NoBrush);
|
||||
p.drawPolygon(GetPolygonFromRadiusGetter(
|
||||
[this](double angle) { return m_calibration_widget->GetCalibrationRadiusAtAngle(angle); },
|
||||
scale));
|
||||
|
||||
// Stick position.
|
||||
p.setPen(Qt::NoPen);
|
||||
p.setBrush(ADJ_INPUT_COLOR);
|
||||
p.drawEllipse(QPointF{point.x, point.y} * scale, INPUT_DOT_RADIUS, INPUT_DOT_RADIUS);
|
||||
}
|
||||
|
||||
void MappingIndicator::UpdateCalibrationWidget(Common::DVec2 point)
|
||||
{
|
||||
if (m_calibration_widget)
|
||||
m_calibration_widget->Update(point);
|
||||
}
|
||||
|
||||
bool MappingIndicator::IsCalibrating() const
|
||||
{
|
||||
return m_calibration_widget && m_calibration_widget->IsCalibrating();
|
||||
}
|
||||
|
||||
void MappingIndicator::SetCalibrationWidget(CalibrationWidget* widget)
|
||||
{
|
||||
m_calibration_widget = widget;
|
||||
}
|
||||
|
||||
CalibrationWidget::CalibrationWidget(ControllerEmu::ReshapableInput& input,
|
||||
MappingIndicator& indicator)
|
||||
: m_input(input), m_indicator(indicator), m_completion_action{}
|
||||
{
|
||||
m_indicator.SetCalibrationWidget(this);
|
||||
|
||||
// Make it more apparent that this is a menu with more options.
|
||||
setPopupMode(ToolButtonPopupMode::MenuButtonPopup);
|
||||
|
||||
SetupActions();
|
||||
|
||||
setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Fixed);
|
||||
|
||||
m_informative_timer = new QTimer(this);
|
||||
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::max_element(m_calibration_data.begin(), m_calibration_data.end()) > 0.5)
|
||||
return;
|
||||
|
||||
QMessageBox msg(QMessageBox::Information, tr("Calibration"),
|
||||
tr("For best results please slowly move your input to all possible regions."),
|
||||
QMessageBox::Ok, this);
|
||||
msg.setWindowModality(Qt::WindowModal);
|
||||
msg.exec();
|
||||
});
|
||||
m_informative_timer->setSingleShot(true);
|
||||
}
|
||||
|
||||
void CalibrationWidget::SetupActions()
|
||||
{
|
||||
const auto calibrate_action = new QAction(tr("Calibrate"), this);
|
||||
const auto reset_action = new QAction(tr("Reset"), this);
|
||||
|
||||
connect(calibrate_action, &QAction::triggered, [this]() { StartCalibration(); });
|
||||
connect(reset_action, &QAction::triggered, [this]() { m_input.SetCalibrationToDefault(); });
|
||||
|
||||
for (auto* action : actions())
|
||||
removeAction(action);
|
||||
|
||||
addAction(calibrate_action);
|
||||
addAction(reset_action);
|
||||
setDefaultAction(calibrate_action);
|
||||
|
||||
m_completion_action = new QAction(tr("Finish Calibration"), this);
|
||||
connect(m_completion_action, &QAction::triggered, [this]() {
|
||||
m_input.SetCalibrationData(std::move(m_calibration_data));
|
||||
m_informative_timer->stop();
|
||||
SetupActions();
|
||||
});
|
||||
}
|
||||
|
||||
void CalibrationWidget::StartCalibration()
|
||||
{
|
||||
m_calibration_data.assign(m_input.CALIBRATION_SAMPLE_COUNT, 0.0);
|
||||
|
||||
// Cancel calibration.
|
||||
const auto cancel_action = new QAction(tr("Cancel Calibration"), this);
|
||||
connect(cancel_action, &QAction::triggered, [this]() {
|
||||
m_calibration_data.clear();
|
||||
m_informative_timer->stop();
|
||||
SetupActions();
|
||||
});
|
||||
|
||||
for (auto* action : actions())
|
||||
removeAction(action);
|
||||
|
||||
addAction(cancel_action);
|
||||
addAction(m_completion_action);
|
||||
setDefaultAction(cancel_action);
|
||||
|
||||
// If the user doesn't seem to know what they are doing after a bit inform them.
|
||||
m_informative_timer->start(2000);
|
||||
}
|
||||
|
||||
void CalibrationWidget::Update(Common::DVec2 point)
|
||||
{
|
||||
QFont f = parentWidget()->font();
|
||||
QPalette p = parentWidget()->palette();
|
||||
|
||||
if (IsCalibrating())
|
||||
{
|
||||
m_input.UpdateCalibrationData(m_calibration_data, point);
|
||||
|
||||
if (IsCalibrationDataSensible(m_calibration_data))
|
||||
{
|
||||
setDefaultAction(m_completion_action);
|
||||
}
|
||||
}
|
||||
else if (IsPointOutsideCalibration(point, m_input))
|
||||
{
|
||||
// Flashing bold and red on miscalibration.
|
||||
if (QDateTime::currentDateTime().toMSecsSinceEpoch() % 500 < 350)
|
||||
{
|
||||
f.setBold(true);
|
||||
p.setColor(QPalette::ButtonText, Qt::red);
|
||||
}
|
||||
}
|
||||
|
||||
setFont(f);
|
||||
setPalette(p);
|
||||
}
|
||||
|
||||
bool CalibrationWidget::IsCalibrating() const
|
||||
{
|
||||
return !m_calibration_data.empty();
|
||||
}
|
||||
|
||||
double CalibrationWidget::GetCalibrationRadiusAtAngle(double angle) const
|
||||
{
|
||||
return m_input.GetCalibrationDataRadiusAtAngle(m_calibration_data, angle);
|
||||
}
|
||||
|
@ -4,33 +4,65 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QToolButton>
|
||||
#include <QWidget>
|
||||
|
||||
#include "InputCommon/ControllerEmu/StickGate.h"
|
||||
|
||||
namespace ControllerEmu
|
||||
{
|
||||
class Control;
|
||||
class ControlGroup;
|
||||
class Cursor;
|
||||
class NumericSetting;
|
||||
class ReshapableInput;
|
||||
} // namespace ControllerEmu
|
||||
|
||||
class QPainter;
|
||||
class QPaintEvent;
|
||||
class QTimer;
|
||||
|
||||
class CalibrationWidget;
|
||||
|
||||
class MappingIndicator : public QWidget
|
||||
{
|
||||
public:
|
||||
explicit MappingIndicator(ControllerEmu::ControlGroup* group);
|
||||
|
||||
void SetCalibrationWidget(CalibrationWidget* widget);
|
||||
|
||||
private:
|
||||
void DrawCursor(ControllerEmu::Cursor& cursor);
|
||||
void DrawReshapableInput(ControllerEmu::ReshapableInput& stick);
|
||||
void DrawMixedTriggers();
|
||||
void DrawCalibration(QPainter& p, Common::DVec2 point);
|
||||
|
||||
void paintEvent(QPaintEvent*) override;
|
||||
|
||||
ControllerEmu::ControlGroup* m_group;
|
||||
bool IsCalibrating() const;
|
||||
void UpdateCalibrationWidget(Common::DVec2 point);
|
||||
|
||||
QTimer* m_timer;
|
||||
ControllerEmu::ControlGroup* const m_group;
|
||||
CalibrationWidget* m_calibration_widget{};
|
||||
};
|
||||
|
||||
class CalibrationWidget : public QToolButton
|
||||
{
|
||||
public:
|
||||
CalibrationWidget(ControllerEmu::ReshapableInput& input, MappingIndicator& indicator);
|
||||
|
||||
void Update(Common::DVec2 point);
|
||||
|
||||
double GetCalibrationRadiusAtAngle(double angle) const;
|
||||
|
||||
bool IsCalibrating() const;
|
||||
|
||||
private:
|
||||
void StartCalibration();
|
||||
void SetupActions();
|
||||
|
||||
ControllerEmu::ReshapableInput& m_input;
|
||||
MappingIndicator& m_indicator;
|
||||
QAction* m_completion_action;
|
||||
ControllerEmu::ReshapableInput::CalibrationData m_calibration_data;
|
||||
QTimer* m_informative_timer;
|
||||
};
|
||||
|
@ -21,6 +21,7 @@
|
||||
#include "InputCommon/ControllerEmu/ControlGroup/ControlGroup.h"
|
||||
#include "InputCommon/ControllerEmu/Setting/BooleanSetting.h"
|
||||
#include "InputCommon/ControllerEmu/Setting/NumericSetting.h"
|
||||
#include "InputCommon/ControllerEmu/StickGate.h"
|
||||
|
||||
MappingWidget::MappingWidget(MappingWindow* window) : m_parent(window)
|
||||
{
|
||||
@ -73,10 +74,14 @@ QGroupBox* MappingWidget::CreateGroupBox(const QString& name, ControllerEmu::Con
|
||||
|
||||
group_box->setLayout(form_layout);
|
||||
|
||||
bool need_indicator = group->type == ControllerEmu::GroupType::Cursor ||
|
||||
group->type == ControllerEmu::GroupType::Stick ||
|
||||
group->type == ControllerEmu::GroupType::Tilt ||
|
||||
group->type == ControllerEmu::GroupType::MixedTriggers;
|
||||
const bool need_indicator = group->type == ControllerEmu::GroupType::Cursor ||
|
||||
group->type == ControllerEmu::GroupType::Stick ||
|
||||
group->type == ControllerEmu::GroupType::Tilt ||
|
||||
group->type == ControllerEmu::GroupType::MixedTriggers;
|
||||
|
||||
const bool need_calibration = group->type == ControllerEmu::GroupType::Cursor ||
|
||||
group->type == ControllerEmu::GroupType::Stick ||
|
||||
group->type == ControllerEmu::GroupType::Tilt;
|
||||
|
||||
for (auto& control : group->controls)
|
||||
{
|
||||
@ -135,7 +140,19 @@ QGroupBox* MappingWidget::CreateGroupBox(const QString& name, ControllerEmu::Con
|
||||
}
|
||||
|
||||
if (need_indicator)
|
||||
form_layout->addRow(new MappingIndicator(group));
|
||||
{
|
||||
auto const indicator = new MappingIndicator(group);
|
||||
|
||||
if (need_calibration)
|
||||
{
|
||||
const auto calibrate =
|
||||
new CalibrationWidget(*static_cast<ControllerEmu::ReshapableInput*>(group), *indicator);
|
||||
|
||||
form_layout->addRow(calibrate);
|
||||
}
|
||||
|
||||
form_layout->addRow(indicator);
|
||||
}
|
||||
|
||||
return group_box;
|
||||
}
|
||||
|
@ -148,6 +148,8 @@ void MappingWindow::ConnectWidgets()
|
||||
connect(m_profiles_save, &QPushButton::clicked, this, &MappingWindow::OnSaveProfilePressed);
|
||||
connect(m_profiles_load, &QPushButton::clicked, this, &MappingWindow::OnLoadProfilePressed);
|
||||
connect(m_profiles_delete, &QPushButton::clicked, this, &MappingWindow::OnDeleteProfilePressed);
|
||||
// We currently use the "Close" button as an "Accept" button so we must save on reject.
|
||||
connect(this, &QDialog::rejected, [this] { emit Save(); });
|
||||
}
|
||||
|
||||
void MappingWindow::OnDeleteProfilePressed()
|
||||
|
Reference in New Issue
Block a user