With this change, “Reload Input” will discover newly attached devices on macOS, which previously did not work. Also add correct detection of disconnected devices, which previously was not handled well, especially with the new ‘keep trying to open devices’ behaviour of FGJoystickInput. Should fix: https://sourceforge.net/p/flightgear/codetickets/2259/
462 lines
14 KiB
C++
462 lines
14 KiB
C++
// FGJoystickInput.cxx -- handle user input from joystick devices
|
|
//
|
|
// Written by Torsten Dreyer, started August 2009
|
|
// Based on work from David Megginson, started May 2001.
|
|
//
|
|
// Copyright (C) 2009 Torsten Dreyer, Torsten (at) t3r _dot_ de
|
|
// Copyright (C) 2001 David Megginson, david@megginson.com
|
|
//
|
|
// This program is free software; you can redistribute it and/or
|
|
// modify it under the terms of the GNU General Public License as
|
|
// published by the Free Software Foundation; either version 2 of the
|
|
// License, or (at your option) any later version.
|
|
//
|
|
// This program is distributed in the hope that it will be useful, but
|
|
// WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
// General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU General Public License
|
|
// along with this program; if not, write to the Free Software
|
|
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
|
//
|
|
// $Id$
|
|
|
|
#include "config.h"
|
|
|
|
#include "FGJoystickInput.hxx"
|
|
|
|
#if defined(SG_WINDOWS)
|
|
# include <windows.h>
|
|
#endif
|
|
|
|
#include <cmath>
|
|
|
|
#include <simgear/props/props_io.hxx>
|
|
#include "FGDeviceConfigurationMap.hxx"
|
|
#include <Main/fg_props.hxx>
|
|
#include <Scripting/NasalSys.hxx>
|
|
|
|
using simgear::PropertyList;
|
|
|
|
FGJoystickInput::axis::axis ()
|
|
: last_value(9999999),
|
|
tolerance(0.002),
|
|
low_threshold(-0.9),
|
|
high_threshold(0.9),
|
|
interval_sec(0),
|
|
delay_sec(0),
|
|
release_delay_sec(0),
|
|
last_dt(0)
|
|
{
|
|
}
|
|
|
|
FGJoystickInput::axis::~axis ()
|
|
{
|
|
}
|
|
|
|
FGJoystickInput::joystick::joystick ()
|
|
: jsnum(0),
|
|
naxes(0),
|
|
nbuttons(0),
|
|
axes(0),
|
|
buttons(0),
|
|
predefined(true)
|
|
{
|
|
}
|
|
|
|
void FGJoystickInput::joystick::clearAxesAndButtons()
|
|
{
|
|
delete[] axes;
|
|
delete[] buttons;
|
|
axes = NULL;
|
|
buttons = NULL;
|
|
naxes = 0;
|
|
nbuttons = 0;
|
|
}
|
|
|
|
FGJoystickInput::joystick::~joystick ()
|
|
{
|
|
jsnum = 0;
|
|
clearAxesAndButtons();
|
|
}
|
|
|
|
|
|
FGJoystickInput::FGJoystickInput()
|
|
{
|
|
}
|
|
|
|
FGJoystickInput::~FGJoystickInput()
|
|
{
|
|
_remove(true);
|
|
}
|
|
|
|
void FGJoystickInput::_remove(bool all)
|
|
{
|
|
SGPropertyNode * js_nodes = fgGetNode("/input/joysticks", true);
|
|
|
|
for (int i = 0; i < MAX_JOYSTICKS; i++)
|
|
{
|
|
joystick* joy = &joysticks[i];
|
|
// do not remove predefined joysticks info on reinit
|
|
if (all || (!joy->predefined))
|
|
js_nodes->removeChild("js", i);
|
|
|
|
joy->plibJS.reset();
|
|
joy->clearAxesAndButtons();
|
|
}
|
|
}
|
|
|
|
void FGJoystickInput::init()
|
|
{
|
|
jsInit();
|
|
SG_LOG(SG_INPUT, SG_DEBUG, "Initializing joystick bindings");
|
|
SGPropertyNode_ptr js_nodes = fgGetNode("/input/joysticks", true);
|
|
status_node = fgGetNode("/devices/status/joysticks", true);
|
|
|
|
FGDeviceConfigurationMap configMap("Input/Joysticks",js_nodes, "js-named");
|
|
|
|
for (int i = 0; i < MAX_JOYSTICKS; i++) {
|
|
jsJoystick * js = new jsJoystick(i);
|
|
joysticks[i].plibJS.reset(js);
|
|
joysticks[i].initializing = true;
|
|
|
|
if (js->notWorking()) {
|
|
SG_LOG(SG_INPUT, SG_DEBUG, "Joystick " << i << " not found");
|
|
continue;
|
|
}
|
|
|
|
const char * name = js->getName();
|
|
SGPropertyNode_ptr js_node = js_nodes->getChild("js", i);
|
|
|
|
if (js_node) {
|
|
SG_LOG(SG_INPUT, SG_INFO, "Using existing bindings for joystick " << i);
|
|
|
|
} else {
|
|
joysticks[i].predefined = false;
|
|
SG_LOG(SG_INPUT, SG_INFO, "Looking for bindings for joystick \"" << name << '"');
|
|
SGPropertyNode_ptr named;
|
|
|
|
// allow distinguishing duplicated devices by the name
|
|
string indexedName = computeDeviceIndexName(name, i);
|
|
if (configMap.hasConfiguration(indexedName)) {
|
|
named = configMap.configurationForDeviceName(indexedName);
|
|
std::string source = named->getStringValue("source", "user defined");
|
|
SG_LOG(SG_INPUT, SG_INFO, "... found joystick: " << source);
|
|
} else if (configMap.hasConfiguration(name)) {
|
|
named = configMap.configurationForDeviceName(name);
|
|
std::string source = named->getStringValue("source", "user defined");
|
|
SG_LOG(SG_INPUT, SG_INFO, "... found joystick: " << source);
|
|
} else if ((named = configMap.configurationForDeviceName("default"))) {
|
|
std::string source = named->getStringValue("source", "user defined");
|
|
SG_LOG(SG_INPUT, SG_INFO, "No config found for joystick \"" << name
|
|
<< "\"\nUsing default: \"" << source << '"');
|
|
|
|
} else {
|
|
SG_LOG(SG_INPUT, SG_WARN, "No joystick configuration file with <name>" << name << "</name> entry found!");
|
|
}
|
|
|
|
js_node = js_nodes->getChild("js", i, true);
|
|
copyProperties(named, js_node);
|
|
js_node->setStringValue("id", name);
|
|
}
|
|
}
|
|
}
|
|
|
|
std::string FGJoystickInput::computeDeviceIndexName(const std::string& name,
|
|
int lastIndex) const
|
|
{
|
|
int index = 0;
|
|
for (int i = 0; i < lastIndex; i++) {
|
|
if (joysticks[i].plibJS && (joysticks[i].plibJS->getName() == name)) {
|
|
++index;
|
|
}
|
|
}
|
|
|
|
std::ostringstream os;
|
|
os << name << "_" << index;
|
|
return os.str();
|
|
}
|
|
|
|
void FGJoystickInput::reinit()
|
|
{
|
|
SG_LOG(SG_INPUT, SG_DEBUG, "Re-Initializing joystick bindings");
|
|
_remove(false);
|
|
|
|
#if defined(SG_MAC)
|
|
jsShutdown();
|
|
#endif
|
|
|
|
FGJoystickInput::init();
|
|
FGJoystickInput::postinit();
|
|
}
|
|
|
|
void FGJoystickInput::postinit()
|
|
{
|
|
FGNasalSys *nasalsys = (FGNasalSys *)globals->get_subsystem("nasal");
|
|
SGPropertyNode_ptr js_nodes = fgGetNode("/input/joysticks");
|
|
|
|
for (int i = 0; i < MAX_JOYSTICKS; i++) {
|
|
SGPropertyNode_ptr js_node = js_nodes->getChild("js", i);
|
|
jsJoystick *js = joysticks[i].plibJS.get();
|
|
if (!js_node || js->notWorking())
|
|
continue;
|
|
|
|
#ifdef WIN32
|
|
JOYCAPS jsCaps ;
|
|
joyGetDevCaps( i, &jsCaps, sizeof(jsCaps) );
|
|
unsigned int nbuttons = jsCaps.wNumButtons;
|
|
if (nbuttons > MAX_JOYSTICK_BUTTONS) nbuttons = MAX_JOYSTICK_BUTTONS;
|
|
#else
|
|
unsigned int nbuttons = MAX_JOYSTICK_BUTTONS;
|
|
#endif
|
|
|
|
int naxes = js->getNumAxes();
|
|
if (naxes > MAX_JOYSTICK_AXES) naxes = MAX_JOYSTICK_AXES;
|
|
joysticks[i].naxes = naxes;
|
|
joysticks[i].nbuttons = nbuttons;
|
|
|
|
SG_LOG(SG_INPUT, SG_DEBUG, "Initializing joystick " << i);
|
|
|
|
// Set up range arrays
|
|
float minRange[MAX_JOYSTICK_AXES];
|
|
float maxRange[MAX_JOYSTICK_AXES];
|
|
float center[MAX_JOYSTICK_AXES];
|
|
|
|
// Initialize with default values
|
|
js->getMinRange(minRange);
|
|
js->getMaxRange(maxRange);
|
|
js->getCenter(center);
|
|
|
|
// Allocate axes and buttons
|
|
joysticks[i].axes = new axis[naxes];
|
|
joysticks[i].buttons = new FGButton[nbuttons];
|
|
|
|
//
|
|
// Initialize nasal groups.
|
|
//
|
|
std::ostringstream str;
|
|
str << "__js" << i;
|
|
std::string module = str.str();
|
|
nasalsys->createModule(module.c_str(), module.c_str(), "", 0);
|
|
|
|
PropertyList nasal = js_node->getChildren("nasal");
|
|
unsigned int j;
|
|
for (j = 0; j < nasal.size(); j++) {
|
|
nasal[j]->setStringValue("module", module.c_str());
|
|
nasalsys->handleCommand(nasal[j],nullptr);
|
|
}
|
|
|
|
//
|
|
// Initialize the axes.
|
|
//
|
|
PropertyList axes = js_node->getChildren("axis");
|
|
size_t nb_axes = axes.size();
|
|
for (j = 0; j < nb_axes; j++ ) {
|
|
const SGPropertyNode * axis_node = axes[j];
|
|
const SGPropertyNode * num_node = axis_node->getChild("number");
|
|
int n_axis = axis_node->getIndex();
|
|
if (num_node != 0) {
|
|
n_axis = num_node->getIntValue(TGT_PLATFORM, -1);
|
|
|
|
#ifdef SG_MAC
|
|
// Mac falls back to Unix by default - avoids specifying many
|
|
// duplicate <mac> entries in joystick config files
|
|
if (n_axis < 0) {
|
|
n_axis = num_node->getIntValue("unix", -1);
|
|
}
|
|
#endif
|
|
|
|
// Silently ignore platforms that are not specified within the
|
|
// <number></number> section
|
|
if (n_axis < 0) {
|
|
continue;
|
|
}
|
|
}
|
|
|
|
if (n_axis >= naxes) {
|
|
SG_LOG(SG_INPUT, SG_DEBUG, "Dropping bindings for axis " << n_axis);
|
|
continue;
|
|
}
|
|
axis &a = joysticks[i].axes[n_axis];
|
|
|
|
js->setDeadBand(n_axis, axis_node->getDoubleValue("dead-band", 0.0));
|
|
|
|
a.tolerance = axis_node->getDoubleValue("tolerance", 0.002);
|
|
minRange[n_axis] = axis_node->getDoubleValue("min-range", minRange[n_axis]);
|
|
maxRange[n_axis] = axis_node->getDoubleValue("max-range", maxRange[n_axis]);
|
|
center[n_axis] = axis_node->getDoubleValue("center", center[n_axis]);
|
|
|
|
read_bindings(axis_node, a.bindings, KEYMOD_NONE, module );
|
|
|
|
// Initialize the virtual axis buttons.
|
|
a.low.init(axis_node->getChild("low"), "low", module );
|
|
a.low_threshold = axis_node->getDoubleValue("low-threshold", -0.9);
|
|
|
|
a.high.init(axis_node->getChild("high"), "high", module );
|
|
a.high_threshold = axis_node->getDoubleValue("high-threshold", 0.9);
|
|
a.interval_sec = axis_node->getDoubleValue("interval-sec",0.0);
|
|
a.delay_sec = axis_node->getDoubleValue("delay-sec",0.0);
|
|
a.release_delay_sec = axis_node->getDoubleValue("release-delay-sec",0.0);
|
|
a.last_dt = 0.0;
|
|
}
|
|
|
|
//
|
|
// Initialize the buttons.
|
|
//
|
|
PropertyList buttons = js_node->getChildren("button");
|
|
for (auto button_node : buttons ) {
|
|
size_t n_but = button_node->getIndex();
|
|
|
|
const SGPropertyNode * num_node = button_node->getChild("number");
|
|
if (NULL != num_node)
|
|
n_but = num_node->getIntValue(TGT_PLATFORM,n_but);
|
|
|
|
if (n_but >= nbuttons) {
|
|
SG_LOG(SG_INPUT, SG_DEBUG, "Dropping bindings for button " << n_but);
|
|
continue;
|
|
}
|
|
|
|
std::ostringstream buf;
|
|
buf << (unsigned)n_but;
|
|
|
|
SG_LOG(SG_INPUT, SG_DEBUG, "Initializing button " << buf.str());
|
|
FGButton &b = joysticks[i].buttons[n_but];
|
|
b.init(button_node, buf.str(), module );
|
|
// get interval-sec property
|
|
b.interval_sec = button_node->getDoubleValue("interval-sec",0.0);
|
|
b.delay_sec = button_node->getDoubleValue("delay-sec",0.0);
|
|
b.release_delay_sec = button_node->getDoubleValue("release-delay-sec",0.0);
|
|
b.last_dt = 0.0;
|
|
}
|
|
|
|
js->setMinRange(minRange);
|
|
js->setMaxRange(maxRange);
|
|
js->setCenter(center);
|
|
}
|
|
}
|
|
|
|
void FGJoystickInput::updateJoystick(int index, FGJoystickInput::joystick* joy, double dt)
|
|
{
|
|
float axis_values[MAX_JOYSTICK_AXES];
|
|
int modifiers = fgGetKeyModifiers();
|
|
int buttons;
|
|
bool pressed, last_state;
|
|
bool axes_initialized;
|
|
float delay;
|
|
|
|
jsJoystick * js = joy->plibJS.get();
|
|
if (js == 0 || js->notWorking()) {
|
|
joysticks[index].initializing = true;
|
|
if (js) {
|
|
joysticks[index].init_dt += dt;
|
|
if (joysticks[index].init_dt >= 1.0) {
|
|
joysticks[index].plibJS.reset( new jsJoystick(index) );
|
|
joysticks[index].init_dt = 0.0;
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
js->read(&buttons, axis_values);
|
|
if (js->notWorking()) { // If js is disconnected
|
|
joysticks[index].initializing = true;
|
|
return;
|
|
}
|
|
|
|
// Joystick axes can get initialized to extreme values, at least on Linux.
|
|
// Wait until one of the axes get a different value before continuing.
|
|
// https://sourceforge.net/p/flightgear/codetickets/2185/
|
|
axes_initialized = true;
|
|
if (joysticks[index].initializing) {
|
|
|
|
if (!joysticks[index].initialized) {
|
|
js->read(NULL, joysticks[index].values);
|
|
joysticks[index].initialized = true;
|
|
}
|
|
|
|
int j;
|
|
for (j = 0; j < joy->naxes; j++) {
|
|
if (axis_values[j] != joysticks[index].values[j]) break;
|
|
}
|
|
if (j < joy->naxes) {
|
|
joysticks[index].initializing = false;
|
|
} else {
|
|
axes_initialized = false;
|
|
}
|
|
}
|
|
|
|
// Update device status
|
|
SGPropertyNode_ptr status = status_node->getChild("joystick", index, true);
|
|
if (axes_initialized) {
|
|
for (int j = 0; j < MAX_JOYSTICK_AXES; j++) {
|
|
status->getChild("axis", j, true)->setFloatValue(axis_values[j]);
|
|
}
|
|
}
|
|
|
|
for (int j = 0; j < MAX_JOYSTICK_BUTTONS; j++) {
|
|
status->getChild("button", j, true)->setBoolValue((buttons & (1u << j)) > 0 );
|
|
}
|
|
|
|
// Fire bindings for the axes.
|
|
if (axes_initialized) {
|
|
for (int j = 0; j < joy->naxes; j++) {
|
|
axis &a = joy->axes[j];
|
|
|
|
// Do nothing if the axis position
|
|
// is unchanged; only a change in
|
|
// position fires the bindings.
|
|
// But only if there are bindings
|
|
if (fabs(axis_values[j] - a.last_value) > a.tolerance
|
|
&& a.bindings[KEYMOD_NONE].size() > 0 ) {
|
|
a.last_value = axis_values[j];
|
|
for (unsigned int k = 0; k < a.bindings[KEYMOD_NONE].size(); k++)
|
|
a.bindings[KEYMOD_NONE][k]->fire(axis_values[j]);
|
|
}
|
|
|
|
// do we have to emulate axis buttons?
|
|
last_state = joy->axes[j].low.last_state || joy->axes[j].high.last_state;
|
|
pressed = axis_values[j] < a.low_threshold || axis_values[j] > a.high_threshold;
|
|
delay = (pressed ? last_state ? a.interval_sec : a.delay_sec : a.release_delay_sec );
|
|
if(pressed || last_state) a.last_dt += dt;
|
|
else a.last_dt = 0;
|
|
if(a.last_dt >= delay) {
|
|
if (a.low.bindings[modifiers].size())
|
|
joy->axes[j].low.update( modifiers, axis_values[j] < a.low_threshold );
|
|
|
|
if (a.high.bindings[modifiers].size())
|
|
joy->axes[j].high.update( modifiers, axis_values[j] > a.high_threshold );
|
|
|
|
a.last_dt -= delay;
|
|
}
|
|
} // of axes iteration
|
|
} // axes_initialized
|
|
|
|
// Fire bindings for the buttons.
|
|
for (int j = 0; j < joy->nbuttons; j++) {
|
|
FGButton &b = joy->buttons[j];
|
|
pressed = (buttons & (1u << j)) > 0;
|
|
last_state = joy->buttons[j].last_state;
|
|
delay = (pressed ? last_state ? b.interval_sec : b.delay_sec : b.release_delay_sec );
|
|
if(pressed || last_state) b.last_dt += dt;
|
|
else b.last_dt = 0;
|
|
if(b.last_dt >= delay) {
|
|
joy->buttons[j].update( modifiers, pressed );
|
|
b.last_dt -= delay;
|
|
}
|
|
} // of butotns iterations
|
|
|
|
}
|
|
|
|
void FGJoystickInput::update( double dt )
|
|
{
|
|
for (int i = 0; i < MAX_JOYSTICKS; i++) {
|
|
updateJoystick(i, &joysticks[i], dt);
|
|
}
|
|
}
|
|
|
|
|
|
// Register the subsystem.
|
|
SGSubsystemMgr::Registrant<FGJoystickInput> registrantFGJoystickInput(
|
|
SGSubsystemMgr::GENERAL,
|
|
{{"nasal", SGSubsystemMgr::Dependency::HARD}});
|