Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Fetching contributors…

Cannot retrieve contributors at this time

728 lines (627 sloc) 23.829 kB
#import <AppKit/AppKit.h>
#import <Carbon/Carbon.h>
#include <Gosu/Input.hpp>
#include <Gosu/TextInput.hpp>
#include <GosuImpl/MacUtility.hpp>
#include <Gosu/TR1.hpp>
#include <Gosu/Utility.hpp>
#include <IOKit/hidsystem/IOLLEvent.h>
#include <map>
#include <string>
#include <vector>
#include <mach/mach.h>
#include <mach/mach_error.h>
#include <Kernel/IOKit/hidsystem/IOHIDUsageTables.h>
#include <CoreFoundation/CoreFoundation.h>
#include <IOKit/hid/IOHIDLib.h>
#include <IOKit/hid/IOHIDKeys.h>
#include <IOKit/IOKitLib.h>
#include <IOKit/IOCFPlugIn.h>
#include <stdexcept>
#include <vector>
// USB Gamepad code, likely to be moved somewhere else later.
// This is Frankencode until the Input redesign happens.
namespace {
using namespace std;
using namespace Gosu;
using tr1::shared_ptr;
template<typename Negatable>
void checkTrue(Negatable cond, const char* message = "work")
{
if (!cond)
throw runtime_error(string("HID system failed to ") + message);
}
void checkIO(IOReturn val, const char* message = "work")
{
checkTrue(val == kIOReturnSuccess, message);
}
class IOScope
{
io_object_t ref;
IOScope(const IOScope&);
IOScope& operator=(const IOScope&);
public:
IOScope(io_object_t ref)
: ref(ref)
{
}
~IOScope()
{
IOObjectRelease(ref);
}
};
string getDictString(CFMutableDictionaryRef dict, CFStringRef key, const char* what)
{
char buf[256];
CFStringRef str =
(CFStringRef)CFDictionaryGetValue(dict, key);
checkTrue(str && CFStringGetCString(str, buf, sizeof buf, CFStringGetSystemEncoding()),
what);
return buf;
}
SInt32 getDictSInt32(CFMutableDictionaryRef dict, CFStringRef key, const char* what)
{
SInt32 value;
CFNumberRef number =
(CFNumberRef)CFDictionaryGetValue(dict, key);
checkTrue(number && CFNumberGetValue(number, kCFNumberSInt32Type, &value),
what);
return value;
}
struct Axis
{
IOHIDElementCookie cookie;
long min, max;
enum Role { mainX, mainY } role;
// Some devices report more axis than they have, with random constant values.
// An axis has to go into, and out of the neutral zone so it will be reported.
bool wasNeutralOnce;
Axis(CFMutableDictionaryRef dict, Role role)
: role(role), wasNeutralOnce(false)
{
cookie = (IOHIDElementCookie)getDictSInt32(dict, CFSTR(kIOHIDElementCookieKey),
"get an element cookie");
min = getDictSInt32(dict, CFSTR(kIOHIDElementMinKey),
"get a min value");
max = getDictSInt32(dict, CFSTR(kIOHIDElementMaxKey),
"get a max value");
}
};
struct Hat
{
IOHIDElementCookie cookie;
enum { fourWay, eightWay, unknown } kind;
long min;
Hat(CFMutableDictionaryRef dict)
{
cookie = (IOHIDElementCookie)getDictSInt32(dict, CFSTR(kIOHIDElementCookieKey),
"an element cookie");
min = getDictSInt32(dict, CFSTR(kIOHIDElementMinKey),
"a min value");
SInt32 max = getDictSInt32(dict, CFSTR(kIOHIDElementMaxKey),
"a max value");
if ((max - min) == 3)
kind = fourWay;
else if ((max - min) == 7)
kind = eightWay;
else
kind = unknown;
}
};
struct Button
{
IOHIDElementCookie cookie;
Button(CFMutableDictionaryRef dict)
{
cookie = (IOHIDElementCookie)getDictSInt32(dict, CFSTR(kIOHIDElementCookieKey),
"get an element cookie");
}
};
struct Device
{
shared_ptr<IOHIDDeviceInterface*> interface;
std::string name;
vector<Axis> axis;
vector<Hat> hats;
vector<Button> buttons;
};
class System
{
vector<Device> devices;
static void closeAndReleaseInterface(IOHIDDeviceInterface** ptr)
{
(*ptr)->close(ptr); // Won't hurt if open() wasn't called
(*ptr)->Release(ptr);
}
static void eraseDevice(void* target, IOReturn, void* refcon, void*)
{
System& self = *static_cast<System*>(target);
for (unsigned i = 0; i < self.devices.size(); ++i)
if (self.devices[i].interface.get() == refcon)
{
self.devices.erase(self.devices.begin() + i);
return;
}
assert(false);
}
bool isDeviceInteresting(CFMutableDictionaryRef properties)
{
// Get usage page/usage.
SInt32 page = getDictSInt32(properties, CFSTR(kIOHIDPrimaryUsagePageKey),
"a usage page");
SInt32 usage = getDictSInt32(properties, CFSTR(kIOHIDPrimaryUsageKey),
"a usage value");
// Device uninteresting?
return page == kHIDPage_GenericDesktop &&
(usage == kHIDUsage_GD_Joystick ||
usage == kHIDUsage_GD_GamePad ||
usage == kHIDUsage_GD_MultiAxisController);
}
shared_ptr<IOHIDDeviceInterface*> getDeviceInterface(io_registry_entry_t object)
{
IOCFPlugInInterface** intermediate = 0;
SInt32 theScore;
checkIO(IOCreatePlugInInterfaceForService(object, kIOHIDDeviceUserClientTypeID,
kIOCFPlugInInterfaceID, &intermediate, &theScore),
"get intermediate device interface");
IOHIDDeviceInterface** rawResult = 0;
HRESULT ret = (*intermediate)->QueryInterface(intermediate,
CFUUIDGetUUIDBytes(kIOHIDDeviceInterfaceID),
reinterpret_cast<void**>(&rawResult));
(*intermediate)->Release(intermediate);
if (ret != S_OK)
checkTrue(false, "get real device interface through intermediate");
// Yay - got it safe in here.
shared_ptr<IOHIDDeviceInterface*> result(rawResult, closeAndReleaseInterface);
checkIO((*result)->open(result.get(), 0));
checkIO((*result)->setRemovalCallback(result.get(), eraseDevice, result.get(), this));
return result;
}
static void addElement(const void* value, void* parameter)
{
CFMutableDictionaryRef dict = (CFMutableDictionaryRef)value;
Device& target = *static_cast<Device*>(parameter);
SInt32 elementType = getDictSInt32(dict, CFSTR(kIOHIDElementTypeKey),
("an element type on " + target.name).c_str());
SInt32 usagePage = getDictSInt32(dict, CFSTR(kIOHIDElementUsagePageKey),
("an element page on " + target.name).c_str());
SInt32 usage = getDictSInt32(dict, CFSTR(kIOHIDElementUsageKey),
("an element usage on " + target.name).c_str());
if ((elementType == kIOHIDElementTypeInput_Misc) ||
(elementType == kIOHIDElementTypeInput_Button) ||
(elementType == kIOHIDElementTypeInput_Axis))
{
switch (usagePage)
{
case kHIDPage_GenericDesktop:
switch (usage)
{
case kHIDUsage_GD_Y:
case kHIDUsage_GD_Ry:
target.axis.push_back(Axis(dict, Axis::mainY));
break;
case kHIDUsage_GD_X:
case kHIDUsage_GD_Rx:
case kHIDUsage_GD_Z:
case kHIDUsage_GD_Rz:
case kHIDUsage_GD_Slider:
case kHIDUsage_GD_Dial:
case kHIDUsage_GD_Wheel:
target.axis.push_back(Axis(dict, Axis::mainX));
break;
case kHIDUsage_GD_Hatswitch:
target.hats.push_back(Hat(dict));
break;
}
break;
case kHIDPage_Button:
target.buttons.push_back(Button(dict));
break;
}
}
else if (elementType == kIOHIDElementTypeCollection)
addElementCollection(target, dict);
}
static void addElementCollection(Device& target, CFMutableDictionaryRef properties)
{
CFArrayRef array =
(CFArrayRef)CFDictionaryGetValue(properties, CFSTR(kIOHIDElementKey));
checkTrue(array,
("get an element list for " + target.name).c_str());
CFRange range = { 0, CFArrayGetCount(array) };
CFArrayApplyFunction(array, range, addElement, &target);
}
void addDevice(io_object_t object)
{
// Get handle to device properties.
CFMutableDictionaryRef properties;
IORegistryEntryCreateCFProperties(object, &properties,
kCFAllocatorDefault, kNilOptions);
if (!properties)
return;
CFRef<> guard(properties);
if (!isDeviceInteresting(properties))
return;
Device newDevice;
newDevice.interface = getDeviceInterface(object);
try
{
newDevice.name = getDictString(properties, CFSTR(kIOHIDProductKey),
"get a product name");
}
catch (const runtime_error&)
{
newDevice.name = "unnamed device";
}
addElementCollection(newDevice, properties);
devices.push_back(newDevice);
}
public:
System()
{
mach_port_t masterPort;
checkIO(IOMasterPort(bootstrap_port, &masterPort),
"get master port");
CFMutableDictionaryRef hidDeviceKey =
IOServiceMatching(kIOHIDDeviceKey);
checkTrue(hidDeviceKey,
"build device list");
io_iterator_t iterator;
checkIO(IOServiceGetMatchingServices(masterPort, hidDeviceKey, &iterator),
"set up HID iterator");
IOScope guard(iterator);
while (io_registry_entry_t deviceObject = IOIteratorNext(iterator))
{
IOScope guard(deviceObject);
addDevice(deviceObject);
}
}
unsigned countDevices() const
{
return devices.size();
}
const Device& getDevice(unsigned i) const
{
return devices.at(i);
}
std::tr1::array<bool, gpNum> poll()
{
std::tr1::array<bool, gpNum> result = { false };
IOHIDEventStruct event;
for (int dev = 0; dev < devices.size(); ++dev)
{
// Axis
for (int ax = 0; ax < devices[dev].axis.size(); ++ax)
{
checkIO((*devices[dev].interface)->getElementValue(
devices[dev].interface.get(),
devices[dev].axis[ax].cookie, &event));
Axis& a = devices[dev].axis[ax];
if (event.value < (3 * a.min + 1 * a.max) / 4.0)
{
if (a.wasNeutralOnce)
result[(a.role == Axis::mainX ? gpLeft : gpUp) - gpRangeBegin] = true;
}
else if (event.value > (1 * a.min + 3 * a.max) / 4.0)
{
if (a.wasNeutralOnce)
result[(a.role == Axis::mainX ? gpRight : gpDown) - gpRangeBegin] = true;
}
else
a.wasNeutralOnce = true;
}
// Hats (merge into axis)
for (int hat = 0; hat < devices[dev].hats.size(); ++hat)
{
checkIO((*devices[dev].interface)->getElementValue(
devices[dev].interface.get(),
devices[dev].hats[hat].cookie, &event));
// In case device does not start at 0 as expected.
event.value -= devices[dev].hats[hat].min;
// Treat all hats as being 8-way.
if (devices[dev].hats[hat].kind == Hat::fourWay)
event.value *= 2;
switch (event.value)
{
// Must...resist...doing...crappy...fallthrough...magic...
case 0:
result[gpUp - gpRangeBegin] = true;
break;
case 1:
result[gpUp - gpRangeBegin] = true;
result[gpRight - gpRangeBegin] = true;
break;
case 2:
result[gpRight - gpRangeBegin] = true;
break;
case 3:
result[gpDown - gpRangeBegin] = true;
result[gpRight - gpRangeBegin] = true;
break;
case 4:
result[gpDown - gpRangeBegin] = true;
break;
case 5:
result[gpDown - gpRangeBegin] = true;
result[gpLeft - gpRangeBegin] = true;
break;
case 6:
result[gpLeft - gpRangeBegin] = true;
break;
case 7:
result[gpUp - gpRangeBegin] = true;
result[gpLeft - gpRangeBegin] = true;
break;
}
}
// Buttons
for (int btn = 0; btn < devices[dev].buttons.size() && btn < 16; ++btn)
{
checkIO((*devices[dev].interface)->getElementValue(
devices[dev].interface.get(),
devices[dev].buttons[btn].cookie, &event));
if (event.value >= 1)
result[gpButton0 + btn - gpRangeBegin] = true;
}
}
return result;
}
};
}
// Needed for char translation.
namespace Gosu
{
std::wstring macRomanToWstring(const std::string& s);
}
namespace {
const unsigned numScancodes = 128;
std::tr1::array<wchar_t, numScancodes> idChars = { 0 };
std::map<wchar_t, unsigned> charIds;
void initCharTranslation()
{
static bool initializedCharData = false;
if (initializedCharData)
return;
initializedCharData = true;
CFRef<TISInputSourceRef> is(TISCopyCurrentKeyboardLayoutInputSource());
CFRef<CFDataRef> UCHR(
static_cast<CFDataRef>(TISGetInputSourceProperty(is.obj(), kTISPropertyUnicodeKeyLayoutData)));
if (!UCHR.get())
return;
// Reverse for() to prefer lower IDs in charToId
for (int code = numScancodes - 1; code >= 0; --code)
{
UInt32 deadKeyState = 0;
UniChar value[8];
UniCharCount valueLength = 8;
if (code == kbLeftShift || code == kbRightShift ||
code == kbLeftAlt || code == kbRightAlt ||
code == kbLeftControl || code == kbRightControl ||
code == kbLeftMeta || code == kbRightMeta)
continue;
if (noErr != UCKeyTranslate(
reinterpret_cast<const UCKeyboardLayout*>(CFDataGetBytePtr(UCHR.obj())),
code, kUCKeyActionDown, 0, LMGetKbdType(),
kUCKeyTranslateNoDeadKeysMask, &deadKeyState,
valueLength, &valueLength, value))
continue;
// Ignore special characters except newline.
if (value[0] == 3)
value[0] = 13; // convert Enter to Return
if (value[0] < 32 && value[0] != 13)
continue;
idChars[code] = value[0];
charIds[value[0]] = code;
}
}
std::tr1::array<bool, Gosu::numButtons> buttonStates = { false };
}
struct Gosu::Input::Impl
{
Input& input;
NSWindow* window;
TextInput* textInput;
double mouseX, mouseY;
double mouseFactorX, mouseFactorY;
unsigned currentMods;
struct WaitingButton
{
Button btn;
bool down;
WaitingButton(unsigned btnId, bool down) : btn(btnId), down(down) {}
};
std::vector<WaitingButton> queue;
Impl(Input& input)
: input(input), textInput(0), mouseFactorX(1), mouseFactorY(1), currentMods(0)
{
}
void enqueue(unsigned btnId, bool down)
{
queue.push_back(WaitingButton(btnId, down));
}
void updateMods(unsigned newMods)
{
static const unsigned ids[8] = { kbLeftShift, kbLeftControl,
kbLeftAlt, kbLeftMeta,
kbRightShift, kbRightControl,
kbRightAlt, kbRightMeta };
static const unsigned bits[8] = { NX_DEVICELSHIFTKEYMASK, NX_DEVICELCTLKEYMASK,
NX_DEVICELALTKEYMASK, NX_DEVICELCMDKEYMASK,
NX_DEVICERSHIFTKEYMASK, NX_DEVICERCTLKEYMASK,
NX_DEVICERALTKEYMASK, NX_DEVICERCMDKEYMASK };
for (unsigned i = 0; i < 8; ++i)
if ((currentMods & bits[i]) != (newMods & bits[i]))
enqueue(ids[i], (newMods & bits[i]) != 0);
currentMods = newMods;
}
void refreshMousePosition()
{
NSPoint mousePos = [NSEvent mouseLocation];
if (window)
{
mousePos = [window convertScreenToBase: mousePos];
mousePos.y = [[window contentView] frame].size.height - mousePos.y;
}
else
mousePos.y = CGDisplayBounds(CGMainDisplayID()).size.height - mousePos.y;
mouseX = mousePos.x;
mouseY = mousePos.y;
}
};
Gosu::Input::Input(void* window)
: pimpl(new Impl(*this))
{
pimpl->window = static_cast<NSWindow*>(window);
initCharTranslation();
}
Gosu::Input::~Input()
{
}
bool Gosu::Input::feedNSEvent(void* event)
{
NSEvent* ev = (NSEvent*)event;
unsigned type = [ev type];
if (type == NSKeyDown && textInput() && textInput()->feedNSEvent(event))
return true;
if (type == NSKeyDown && [ev isARepeat])
return false;
// Process modifier keys.
unsigned mods = [ev modifierFlags];
if (mods != pimpl->currentMods)
pimpl->updateMods(mods);
// Handle mouse input.
switch (type)
{
case NSLeftMouseDown:
pimpl->enqueue(msLeft, true);
return true;
case NSLeftMouseUp:
pimpl->enqueue(msLeft, false);
return true;
case NSRightMouseDown:
pimpl->enqueue(msRight, true);
return true;
case NSRightMouseUp:
pimpl->enqueue(msRight, false);
return true;
case NSScrollWheel:
if ([ev deltaY] > 0)
{
pimpl->enqueue(msWheelUp, true);
pimpl->enqueue(msWheelUp, false);
}
else if ([ev deltaY] < 0)
{
pimpl->enqueue(msWheelDown, true);
pimpl->enqueue(msWheelDown, false);
}
else
return false;
return true;
}
// Handle other keys.
if (type == NSKeyDown || type == NSKeyUp)
{
pimpl->enqueue([ev keyCode], type == NSKeyDown);
return true;
}
return false;
}
wchar_t Gosu::Input::idToChar(Button btn)
{
if (btn.id() < numScancodes)
return idChars[btn.id()];
else
return 0;
}
Gosu::Button Gosu::Input::charToId(wchar_t ch)
{
std::map<wchar_t, unsigned>::const_iterator iter = charIds.find(ch);
if (iter == charIds.end())
return noButton;
return Gosu::Button(iter->second);
}
bool Gosu::Input::down(Gosu::Button btn) const
{
if (btn == noButton || btn.id() >= numButtons)
return false;
return buttonStates.at(btn.id());
}
double Gosu::Input::mouseX() const
{
return pimpl->mouseX * pimpl->mouseFactorX;
}
double Gosu::Input::mouseY() const
{
return pimpl->mouseY * pimpl->mouseFactorY;
}
void Gosu::Input::setMousePosition(double x, double y)
{
NSPoint mousePos = NSMakePoint(x / pimpl->mouseFactorX, y / pimpl->mouseFactorY);
if (pimpl->window)
{
mousePos.y = [[pimpl->window contentView] frame].size.height - mousePos.y;
mousePos = [pimpl->window convertBaseToScreen: mousePos];
mousePos.y = CGDisplayBounds(CGMainDisplayID()).size.height - mousePos.y;
}
CGSetLocalEventsSuppressionInterval(0.0);
CGWarpMouseCursorPosition(CGPointMake(mousePos.x, mousePos.y));
pimpl->refreshMousePosition();
}
void Gosu::Input::setMouseFactors(double factorX, double factorY)
{
pimpl->mouseFactorX = factorX;
pimpl->mouseFactorY = factorY;
}
const Gosu::Touches& Gosu::Input::currentTouches() const
{
static Gosu::Touches none;
return none;
}
double Gosu::Input::accelerometerX() const
{
return 0.0;
}
double Gosu::Input::accelerometerY() const
{
return 0.0;
}
double Gosu::Input::accelerometerZ() const
{
return 0.0;
}
void Gosu::Input::update()
{
pimpl->refreshMousePosition();
for (unsigned i = 0; i < pimpl->queue.size(); ++i)
{
Impl::WaitingButton& wb = pimpl->queue[i];
buttonStates.at(wb.btn.id()) = wb.down;
if (wb.down && onButtonDown)
onButtonDown(wb.btn);
else if (!wb.down && onButtonUp)
onButtonUp(wb.btn);
}
pimpl->queue.clear();
static System sys;
std::tr1::array<bool, gpNum> gpState = sys.poll();
for (unsigned i = 0; i < gpNum; ++i)
{
if (buttonStates[i + gpRangeBegin] != gpState[i])
{
buttonStates[i + gpRangeBegin] = gpState[i];
if (gpState[i] && onButtonDown)
onButtonDown(Button(gpRangeBegin + i));
else if (!gpState[i] && onButtonUp)
onButtonUp(Button(gpRangeBegin + i));
}
}
}
Gosu::TextInput* Gosu::Input::textInput() const
{
return pimpl->textInput;
}
void Gosu::Input::setTextInput(TextInput* textInput)
{
pimpl->textInput = textInput;
}
Jump to Line
Something went wrong with that request. Please try again.