@@ -0,0 +1,287 @@
// Copyright 2018 Dolphin Emulator Project
// Licensed under GPLv2+
// Refer to the license.txt file included.

#include "DolphinQt2/Config/Mapping/MappingIndicator.h"

#include <array>
#include <cmath>

#include <QPainter>
#include <QTimer>

#include <iostream>

#include "InputCommon/ControlReference/ControlReference.h"
#include "InputCommon/ControllerEmu/Control/Control.h"
#include "InputCommon/ControllerEmu/ControlGroup/ControlGroup.h"
#include "InputCommon/ControllerEmu/Setting/NumericSetting.h"
#include "InputCommon/ControllerInterface/Device.h"

#include "DolphinQt2/Settings.h"

MappingIndicator::MappingIndicator(ControllerEmu::ControlGroup* group) : m_group(group)
{
setMinimumHeight(128);

switch (m_group->type)
{
case ControllerEmu::GroupType::Cursor:
BindCursorControls(false);
break;
case ControllerEmu::GroupType::Stick:
BindStickControls();
break;
case ControllerEmu::GroupType::Tilt:
BindCursorControls(true);
break;
case ControllerEmu::GroupType::MixedTriggers:
BindMixedTriggersControls();
break;
default:
break;
}

m_timer = new QTimer(this);
connect(m_timer, &QTimer::timeout, this, [this] { repaint(); });
m_timer->start(1000 / 30);
}

void MappingIndicator::BindCursorControls(bool tilt)
{
m_cursor_up = m_group->controls[0]->control_ref.get();
m_cursor_down = m_group->controls[1]->control_ref.get();
m_cursor_left = m_group->controls[2]->control_ref.get();
m_cursor_right = m_group->controls[3]->control_ref.get();

if (!tilt)
{
m_cursor_forward = m_group->controls[4]->control_ref.get();
m_cursor_backward = m_group->controls[5]->control_ref.get();

m_cursor_center = m_group->numeric_settings[0].get();
m_cursor_width = m_group->numeric_settings[1].get();
m_cursor_height = m_group->numeric_settings[2].get();
m_cursor_deadzone = m_group->numeric_settings[3].get();
}
else
{
m_cursor_deadzone = m_group->numeric_settings[0].get();
}
}

void MappingIndicator::BindStickControls()
{
m_stick_up = m_group->controls[0]->control_ref.get();
m_stick_down = m_group->controls[1]->control_ref.get();
m_stick_left = m_group->controls[2]->control_ref.get();
m_stick_right = m_group->controls[3]->control_ref.get();
m_stick_modifier = m_group->controls[4]->control_ref.get();

m_stick_radius = m_group->numeric_settings[0].get();
m_stick_deadzone = m_group->numeric_settings[1].get();
}

void MappingIndicator::BindMixedTriggersControls()
{
m_mixed_triggers_l_button = m_group->controls[0]->control_ref.get();
m_mixed_triggers_r_button = m_group->controls[1]->control_ref.get();
m_mixed_triggers_l_analog = m_group->controls[2]->control_ref.get();
m_mixed_triggers_r_analog = m_group->controls[3]->control_ref.get();

m_mixed_triggers_threshold = m_group->numeric_settings[0].get();
}

static ControlState PollControlState(ControlReference* ref)
{
Settings::Instance().SetControllerStateNeeded(true);

auto state = ref->State();

Settings::Instance().SetControllerStateNeeded(false);

if (state != 0)
return state;
else
return 0;
}

void MappingIndicator::DrawCursor(bool tilt)
{
float centerx = width() / 2., centery = height() / 2.;

QPainter p(this);

float width = 64, height = 64;
float deadzone = m_cursor_deadzone->GetValue() * 48;

if (!tilt)
{
float depth = centery - PollControlState(m_cursor_forward) * this->height() / 2.5 +
PollControlState(m_cursor_backward) * this->height() / 2.5;

p.fillRect(0, depth, this->width(), 4, Qt::gray);

width *= m_cursor_width->GetValue();
height *= m_cursor_height->GetValue();
}

float curx = centerx - 4 - std::min(PollControlState(m_cursor_left), 0.5) * width +
std::min(PollControlState(m_cursor_right), 0.5) * width,
cury = centery - 4 - std::min(PollControlState(m_cursor_up), 0.5) * height +
std::min(PollControlState(m_cursor_down), 0.5) * height;

// Draw background
p.setBrush(Qt::white);
p.setPen(Qt::black);
p.drawRect(centerx - (width / 2), centery - (height / 2), width, height);

// Draw deadzone
p.setBrush(Qt::lightGray);
p.drawEllipse(centerx - (deadzone / 2), centery - (deadzone / 2), deadzone, deadzone);

// Draw cursor
p.fillRect(curx, cury, 8, 8, Qt::red);
}

void MappingIndicator::DrawStick()
{
float centerx = width() / 2., centery = height() / 2.;

bool c_stick = m_group->name == "C-Stick";
bool classic_controller = m_group->name == "Left Stick" || m_group->name == "Right Stick";

float ratio = 1;

if (c_stick)
ratio = 1.;
else if (classic_controller)
ratio = 0.9f;

// Polled values
float mod = PollControlState(m_stick_modifier) ? 0.5 : 1;
float radius = m_stick_radius->GetValue();
float curx = -PollControlState(m_stick_left) + PollControlState(m_stick_right),
cury = -PollControlState(m_stick_up) + PollControlState(m_stick_down);
// The maximum deadzone value covers 50% of the stick area
float deadzone = m_stick_deadzone->GetValue() / 2.;

// Size parameters
float max_size = (height() / 2.5) / ratio;
float stick_size = (height() / 3.) / ratio;

// Emulated cursor position
float virt_curx, virt_cury;

if (abs(curx) < deadzone && abs(cury) < deadzone)
{
virt_curx = virt_cury = 0;
}
else
{
virt_curx = curx * mod;
virt_cury = cury * mod;
}

// Coordinates for an octagon
std::array<QPointF, 8> radius_octagon = {
QPointF(centerx, centery + stick_size), // Bottom
QPointF(centerx + stick_size / sqrt(2), centery + stick_size / sqrt(2)), // Bottom Right
QPointF(centerx + stick_size, centery), // Right
QPointF(centerx + stick_size / sqrt(2), centery - stick_size / sqrt(2)), // Top Right
QPointF(centerx, centery - stick_size), // Top
QPointF(centerx - stick_size / sqrt(2), centery - stick_size / sqrt(2)), // Top Left
QPointF(centerx - stick_size, centery), // Left
QPointF(centerx - stick_size / sqrt(2), centery + stick_size / sqrt(2)) // Bottom Left
};

QPainter p(this);

// Draw maximum values
p.setBrush(Qt::white);
p.setPen(Qt::black);
p.drawRect(centerx - max_size, centery - max_size, max_size * 2, max_size * 2);

// Draw radius
p.setBrush(c_stick ? Qt::yellow : Qt::darkGray);
p.drawPolygon(radius_octagon.data(), static_cast<int>(radius_octagon.size()));

// Draw deadzone
p.setBrush(c_stick ? Qt::darkYellow : Qt::lightGray);
p.drawEllipse(centerx - deadzone * stick_size, centery - deadzone * stick_size,
deadzone * stick_size * 2, deadzone * stick_size * 2);

// Draw stick
p.setBrush(Qt::black);
p.drawEllipse(centerx - 4 + curx * max_size, centery - 4 + cury * max_size, 8, 8);

// Draw virtual stick
p.setBrush(Qt::red);
p.drawEllipse(centerx - 4 + virt_curx * max_size * radius,
centery - 4 + virt_cury * max_size * radius, 8, 8);
}

void MappingIndicator::DrawMixedTriggers()
{
QPainter p(this);

// Polled values
double r_analog = PollControlState(m_mixed_triggers_r_analog);
double r_button = PollControlState(m_mixed_triggers_r_button);
double l_analog = PollControlState(m_mixed_triggers_l_analog);
double l_button = PollControlState(m_mixed_triggers_l_button);
double threshold = m_mixed_triggers_threshold->GetValue();

double r_bar_percent = r_analog;
double l_bar_percent = l_analog;

if (r_button && (r_button != r_analog) || (r_button == r_analog) && (r_analog > threshold))
r_bar_percent = 1;
else
r_bar_percent *= 0.8;

if (l_button && (l_button != l_analog) || (l_button == l_analog) && (l_analog > threshold))
l_bar_percent = 1;
else
l_bar_percent *= 0.8;

p.fillRect(0, 0, width(), 64, Qt::black);

p.fillRect(0, 0, l_bar_percent * width(), 32, Qt::red);
p.fillRect(0, 32, r_bar_percent * width(), 32, Qt::red);

p.setPen(Qt::white);
p.drawLine(width() * 0.8, 0, width() * 0.8, 63);
p.drawLine(0, 32, width(), 32);

p.setPen(Qt::green);
p.drawLine(width() * 0.8 * threshold, 0, width() * 0.8 * threshold, 63);

p.setBrush(Qt::black);
p.setPen(Qt::white);
p.drawText(width() * 0.225, 16, tr("L-Analog"));
p.drawText(width() * 0.8 + 16, 16, tr("L"));
p.drawText(width() * 0.225, 48, tr("R-Analog"));
p.drawText(width() * 0.8 + 16, 48, tr("R"));
}

void MappingIndicator::paintEvent(QPaintEvent*)
{
switch (m_group->type)
{
case ControllerEmu::GroupType::Cursor:
DrawCursor(false);
break;
case ControllerEmu::GroupType::Tilt:
DrawCursor(true);
break;
case ControllerEmu::GroupType::Stick:
DrawStick();
break;
case ControllerEmu::GroupType::MixedTriggers:
DrawMixedTriggers();
break;
default:
break;
}
}
@@ -0,0 +1,68 @@
// Copyright 2018 Dolphin Emulator Project
// Licensed under GPLv2+
// Refer to the license.txt file included.

#include <QWidget>

namespace ControllerEmu
{
class Control;
class ControlGroup;
class NumericSetting;
}

class QPaintEvent;
class QTimer;

class ControlReference;

class MappingIndicator : public QWidget
{
public:
explicit MappingIndicator(ControllerEmu::ControlGroup* group);

private:
void BindCursorControls(bool tilt);
void BindStickControls();
void BindMixedTriggersControls();

void DrawCursor(bool tilt);
void DrawStick();
void DrawMixedTriggers();

void paintEvent(QPaintEvent*) override;
ControllerEmu::ControlGroup* m_group;

// Stick settings
ControlReference* m_stick_up;
ControlReference* m_stick_down;
ControlReference* m_stick_left;
ControlReference* m_stick_right;
ControlReference* m_stick_modifier;

ControllerEmu::NumericSetting* m_stick_radius;
ControllerEmu::NumericSetting* m_stick_deadzone;

// Cursor settings
ControlReference* m_cursor_up;
ControlReference* m_cursor_down;
ControlReference* m_cursor_left;
ControlReference* m_cursor_right;
ControlReference* m_cursor_forward;
ControlReference* m_cursor_backward;

ControllerEmu::NumericSetting* m_cursor_center;
ControllerEmu::NumericSetting* m_cursor_width;
ControllerEmu::NumericSetting* m_cursor_height;
ControllerEmu::NumericSetting* m_cursor_deadzone;

// Triggers settings
ControlReference* m_mixed_triggers_r_analog;
ControlReference* m_mixed_triggers_r_button;
ControlReference* m_mixed_triggers_l_analog;
ControlReference* m_mixed_triggers_l_button;

ControllerEmu::NumericSetting* m_mixed_triggers_threshold;

QTimer* m_timer;
};
@@ -11,6 +11,7 @@
#include "DolphinQt2/Config/Mapping/IOWindow.h"
#include "DolphinQt2/Config/Mapping/MappingBool.h"
#include "DolphinQt2/Config/Mapping/MappingButton.h"
#include "DolphinQt2/Config/Mapping/MappingIndicator.h"
#include "DolphinQt2/Config/Mapping/MappingNumeric.h"
#include "DolphinQt2/Config/Mapping/MappingWindow.h"
#include "InputCommon/ControlReference/ControlReference.h"
@@ -47,9 +48,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;

for (auto& control : group->controls)
{
auto* button = new MappingButton(this, control->control_ref.get());
auto* button = new MappingButton(this, control->control_ref.get(), !need_indicator);

button->setMinimumWidth(100);
button->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
@@ -87,6 +93,9 @@ QGroupBox* MappingWidget::CreateGroupBox(const QString& name, ControllerEmu::Con
m_bools.push_back(checkbox);
}

if (need_indicator)
form_layout->addRow(new MappingIndicator(group));

return group_box;
}

@@ -218,6 +218,7 @@
<ClCompile Include="Config\Mapping\MappingBool.cpp" />
<ClCompile Include="Config\Mapping\MappingButton.cpp" />
<ClCompile Include="Config\Mapping\MappingCommon.cpp" />
<ClCompile Include="Config\Mapping\MappingIndicator.cpp" />
<ClCompile Include="Config\Mapping\MappingNumeric.cpp" />
<ClCompile Include="Config\Mapping\MappingWidget.cpp" />
<ClCompile Include="Config\Mapping\MappingWindow.cpp" />
@@ -294,6 +295,7 @@
<ClInclude Include="Config\Mapping\HotkeyWii.h" />
<ClInclude Include="Config\Mapping\MappingBool.h" />
<ClInclude Include="Config\Mapping\MappingCommon.h" />
<ClInclude Include="Config\Mapping\MappingIndicator.h" />
<ClInclude Include="Config\Mapping\MappingNumeric.h" />
<ClInclude Include="Config\Mapping\WiimoteEmuExtension.h" />
<ClInclude Include="Config\Mapping\WiimoteEmuGeneral.h" />
@@ -10,6 +10,7 @@
#include "Common/Common.h"
#include "Core/ConfigManager.h"
#include "Core/Host.h"
#include "DolphinQt2/Settings.h"
#include "VideoCommon/RenderBase.h"

Host::Host() = default;
@@ -108,7 +109,7 @@ void Host_RequestRenderWindowSize(int w, int h)
}
bool Host_UINeedsControllerState()
{
return false;
return Settings::Instance().IsControllerStateNeeded();
}
void Host_NotifyMapLoaded()
{
@@ -269,3 +269,13 @@ bool Settings::IsBreakpointsVisible() const
{
return QSettings().value(QStringLiteral("debugger/showbreakpoints")).toBool();
}

bool Settings::IsControllerStateNeeded() const
{
return m_controller_state_needed;
}

void Settings::SetControllerStateNeeded(bool needed)
{
m_controller_state_needed = needed;
}
@@ -46,6 +46,8 @@ class Settings final : public QObject
void SetLogVisible(bool visible);
bool IsLogConfigVisible() const;
void SetLogConfigVisible(bool visible);
bool IsControllerStateNeeded() const;
void SetControllerStateNeeded(bool needed);

// GameList
QStringList GetPaths() const;
@@ -111,6 +113,7 @@ class Settings final : public QObject
void DebugModeToggled(bool enabled);

private:
bool m_controller_state_needed = false;
std::unique_ptr<NetPlayClient> m_client;
std::unique_ptr<NetPlayServer> m_server;
Settings();