Browse Source

Merge branch 'input-overhaul'

rdb 7 years ago
parent
commit
7ed8b01a84
100 changed files with 7760 additions and 1554 deletions
  1. 63 0
      direct/src/showbase/ShowBase.py
  2. 5 0
      dtool/src/parser-inc/XInput.h
  3. 2 0
      dtool/src/parser-inc/windows.h
  4. 32 29
      makepanda/makepanda.py
  5. 1 0
      panda/src/cocoadisplay/cocoaGraphicsWindow.h
  6. 32 27
      panda/src/cocoadisplay/cocoaGraphicsWindow.mm
  7. 3 12
      panda/src/device/analogNode.I
  8. 25 14
      panda/src/device/analogNode.cxx
  9. 2 1
      panda/src/device/analogNode.h
  10. 6 20
      panda/src/device/buttonNode.I
  11. 19 19
      panda/src/device/buttonNode.cxx
  12. 2 4
      panda/src/device/buttonNode.h
  13. 0 60
      panda/src/device/clientAnalogDevice.I
  14. 2 41
      panda/src/device/clientAnalogDevice.cxx
  15. 0 23
      panda/src/device/clientAnalogDevice.h
  16. 0 1
      panda/src/device/clientBase.cxx
  17. 0 88
      panda/src/device/clientButtonDevice.I
  18. 2 99
      panda/src/device/clientButtonDevice.cxx
  19. 0 38
      panda/src/device/clientButtonDevice.h
  20. 0 41
      panda/src/device/clientDevice.I
  21. 8 11
      panda/src/device/clientDevice.cxx
  22. 5 20
      panda/src/device/clientDevice.h
  23. 1 8
      panda/src/device/clientTrackerDevice.I
  24. 0 6
      panda/src/device/clientTrackerDevice.h
  25. 14 2
      panda/src/device/config_device.cxx
  26. 1 0
      panda/src/device/config_device.h
  27. 3 12
      panda/src/device/dialNode.I
  28. 12 0
      panda/src/device/evdevInputDevice.I
  29. 997 0
      panda/src/device/evdevInputDevice.cxx
  30. 87 0
      panda/src/device/evdevInputDevice.h
  31. 410 0
      panda/src/device/inputDevice.I
  32. 661 0
      panda/src/device/inputDevice.cxx
  33. 366 0
      panda/src/device/inputDevice.h
  34. 25 0
      panda/src/device/inputDeviceManager.I
  35. 128 0
      panda/src/device/inputDeviceManager.cxx
  36. 63 0
      panda/src/device/inputDeviceManager.h
  37. 79 0
      panda/src/device/inputDeviceNode.cxx
  38. 73 0
      panda/src/device/inputDeviceNode.h
  39. 36 0
      panda/src/device/inputDeviceSet.I
  40. 141 0
      panda/src/device/inputDeviceSet.cxx
  41. 64 0
      panda/src/device/inputDeviceSet.h
  42. 798 0
      panda/src/device/ioKitInputDevice.cxx
  43. 55 0
      panda/src/device/ioKitInputDevice.h
  44. 93 0
      panda/src/device/ioKitInputDeviceManager.cxx
  45. 40 0
      panda/src/device/ioKitInputDeviceManager.h
  46. 295 0
      panda/src/device/linuxInputDeviceManager.cxx
  47. 45 0
      panda/src/device/linuxInputDeviceManager.h
  48. 12 0
      panda/src/device/linuxJoystickDevice.I
  49. 423 0
      panda/src/device/linuxJoystickDevice.cxx
  50. 75 0
      panda/src/device/linuxJoystickDevice.h
  51. 11 1
      panda/src/device/p3device_composite1.cxx
  52. 1 2
      panda/src/device/p3device_composite2.cxx
  53. 1 2
      panda/src/device/trackerNode.I
  54. 8 9
      panda/src/device/trackerNode.cxx
  55. 7 6
      panda/src/device/trackerNode.h
  56. 391 0
      panda/src/device/winInputDeviceManager.cxx
  57. 64 0
      panda/src/device/winInputDeviceManager.h
  58. 684 0
      panda/src/device/winRawInputDevice.cxx
  59. 85 0
      panda/src/device/winRawInputDevice.h
  60. 506 0
      panda/src/device/xInputDevice.cxx
  61. 60 0
      panda/src/device/xInputDevice.h
  62. 1 16
      panda/src/display/callbackGraphicsWindow.cxx
  63. 0 1
      panda/src/display/callbackGraphicsWindow.h
  64. 5 1
      panda/src/display/config_display.cxx
  65. 24 76
      panda/src/display/graphicsWindow.cxx
  66. 6 10
      panda/src/display/graphicsWindow.h
  67. 20 188
      panda/src/display/graphicsWindowInputDevice.I
  68. 66 192
      panda/src/display/graphicsWindowInputDevice.cxx
  69. 43 101
      panda/src/display/graphicsWindowInputDevice.h
  70. 15 30
      panda/src/display/mouseAndKeyboard.cxx
  71. 1 5
      panda/src/display/mouseAndKeyboard.h
  72. 1 0
      panda/src/display/p3display_composite2.cxx
  73. 9 10
      panda/src/display/subprocessWindow.cxx
  74. 1 0
      panda/src/display/subprocessWindow.h
  75. 0 38
      panda/src/egldisplay/eglGraphicsWindow.cxx
  76. 0 1
      panda/src/egldisplay/eglGraphicsWindow.h
  77. 1 1
      panda/src/event/buttonEventList.h
  78. 0 53
      panda/src/event/pointerEvent.I
  79. 15 15
      panda/src/event/pointerEvent.h
  80. 51 35
      panda/src/event/pointerEventList.I
  81. 65 0
      panda/src/event/pointerEventList.cxx
  82. 18 12
      panda/src/event/pointerEventList.h
  83. 2 0
      panda/src/putil/config_putil.cxx
  84. 118 0
      panda/src/putil/gamepadButton.cxx
  85. 68 0
      panda/src/putil/gamepadButton.h
  86. 0 73
      panda/src/putil/mouseData.I
  87. 7 31
      panda/src/putil/mouseData.h
  88. 0 1
      panda/src/putil/p3putil_composite1.cxx
  89. 3 1
      panda/src/putil/p3putil_composite2.cxx
  90. 71 0
      panda/src/putil/pointerData.I
  91. 5 5
      panda/src/putil/pointerData.cxx
  92. 72 0
      panda/src/putil/pointerData.h
  93. 1 1
      panda/src/tform/mouseWatcher.I
  94. 1 1
      panda/src/tform/mouseWatcher.cxx
  95. 2 2
      panda/src/tform/mouseWatcher.h
  96. 25 25
      panda/src/tinydisplay/tinyOsxGraphicsWindow.mm
  97. 9 11
      panda/src/tinydisplay/tinySDLGraphicsWindow.cxx
  98. 8 10
      panda/src/tinydisplay/tinyXGraphicsWindow.cxx
  99. 1 9
      panda/src/vrpn/vrpnAnalog.cxx
  100. 1 3
      panda/src/vrpn/vrpnButton.cxx

+ 63 - 0
direct/src/showbase/ShowBase.py

@@ -170,6 +170,7 @@ class ShowBase(DirectObject.DirectObject):
         self.trackball = None
         self.texmem = None
         self.showVertices = None
+        self.deviceButtonThrowers = []
 
         ## This is a NodePath pointing to the Camera object set up for the 3D scene.
         ## This is usually a child of self.camera.
@@ -294,6 +295,7 @@ class ShowBase(DirectObject.DirectObject):
         ## The global job manager, as imported from JobManagerGlobal.
         self.jobMgr = jobMgr
 
+
         ## Particle manager
         self.particleMgr = None
         self.particleMgrEnabled = 0
@@ -303,6 +305,11 @@ class ShowBase(DirectObject.DirectObject):
         self.physicsMgrEnabled = 0
         self.physicsMgrAngular = 0
 
+        ## This is the global input device manager, which keeps track of
+        ## connected input devices.
+        self.devices = InputDeviceManager.getGlobalPtr()
+        self.__inputDeviceNodes = {}
+
         self.createStats()
 
         self.AppHasAudioFocus = 1
@@ -1670,6 +1677,57 @@ class ShowBase(DirectObject.DirectObject):
         return self.mouseWatcherNode.getModifierButtons().isDown(
             KeyboardButton.meta())
 
+    def attachInputDevice(self, device, prefix=None):
+        """
+        This function attaches an input device to the data graph, which will
+        cause the device to be polled and generate events.  If a prefix is
+        given and not None, it is used to prefix events generated by this
+        device, separated by a hyphen.
+
+        If you call this, you should consider calling detachInputDevice when
+        you are done with the device or when it is disconnected.
+        """
+
+        # Protect against the same device being attached multiple times.
+        assert device not in self.__inputDeviceNodes
+
+        idn = self.dataRoot.attachNewNode(InputDeviceNode(device, device.name))
+
+        # Setup the button thrower to generate events for the device.
+        bt = idn.attachNewNode(ButtonThrower(device.name))
+        if prefix is not None:
+            bt.node().setPrefix(prefix + '-')
+
+        assert self.notify.debug("Attached input device {0} with prefix {1}".format(device, prefix))
+        self.__inputDeviceNodes[device] = idn
+        self.deviceButtonThrowers.append(bt)
+
+    def detachInputDevice(self, device):
+        """
+        This should be called after attaching an input device using
+        attachInputDevice and the device is disconnected or you no longer wish
+        to keep polling this device for events.
+
+        You do not strictly need to call this if you expect the device to be
+        reconnected (but be careful that you don't reattach it).
+        """
+
+        if device not in self.__inputDeviceNodes:
+            assert device in self.__inputDeviceNodes
+            return
+
+        assert self.notify.debug("Detached device {0}".format(device.name))
+
+        # Remove the ButtonThrower from the deviceButtonThrowers list.
+        idn = self.__inputDeviceNodes[device]
+        for bt in self.deviceButtonThrowers:
+            if idn.isAncestorOf(bt):
+                self.deviceButtonThrowers.remove(bt)
+                break
+
+        idn.removeNode()
+        del self.__inputDeviceNodes[device]
+
     def addAngularIntegrator(self):
         if not self.physicsMgrAngular:
             physics = importlib.import_module('panda3d.physics')
@@ -1859,6 +1917,9 @@ class ShowBase(DirectObject.DirectObject):
         return Task.cont
 
     def __dataLoop(self, state):
+        # Check if there were newly connected devices.
+        self.devices.update()
+
         # traverse the data graph.  This reads all the control
         # inputs (from the mouse and keyboard, for instance) and also
         # directly acts upon them (for instance, to move the avatar).
@@ -3074,6 +3135,8 @@ class ShowBase(DirectObject.DirectObject):
     setup_mouse = setupMouse
     setup_mouse_cb = setupMouseCB
     enable_software_mouse_pointer = enableSoftwareMousePointer
+    detach_input_device = detachInputDevice
+    attach_input_device = attachInputDevice
     add_angular_integrator = addAngularIntegrator
     enable_particles = enableParticles
     disable_particles = disableParticles

+ 5 - 0
dtool/src/parser-inc/XInput.h

@@ -0,0 +1,5 @@
+typedef struct _XINPUT_STATE XINPUT_STATE, *PXINPUT_STATE;
+typedef struct _XINPUT_CAPABILITIES XINPUT_CAPABILITIES, *PXINPUT_CAPABILITIES;
+typedef struct _XINPUT_VIBRATION XINPUT_VIBRATION, *PXINPUT_VIBRATION;
+typedef struct _XINPUT_GAMEPAD XINPUT_GAMEPAD, *PXINPUT_GAMEPAD;
+typedef struct _XINPUT_KEYSTROKE XINPUT_KEYSTROKE, *PXINPUT_KEYSTROKE;

+ 2 - 0
dtool/src/parser-inc/windows.h

@@ -38,8 +38,10 @@ typedef unsigned long ULONG_PTR;
 // http://msdn.microsoft.com/en-us/library/cc230309.aspx
 typedef bool BOOL;
 typedef unsigned long DWORD;
+typedef unsigned short WORD;
 typedef long LONG;
 typedef long UINT;
+typedef unsigned char BYTE;
 typedef unsigned long ULONG;
 typedef long long LONGLONG;
 typedef long HRESULT;

+ 32 - 29
makepanda/makepanda.py

@@ -616,6 +616,7 @@ if (COMPILER == "MSVC"):
     LibName("WINGDI", "gdi32.lib")
     LibName("ADVAPI", "advapi32.lib")
     LibName("IPHLPAPI", "iphlpapi.lib")
+    LibName("SETUPAPI", "setupapi.lib")
     LibName("GL", "opengl32.lib")
     LibName("GLES", "libgles_cm.lib")
     LibName("GLES2", "libGLESv2.lib")
@@ -2440,6 +2441,7 @@ def WriteConfigSettings():
         dtool_config["HAVE_GLX"] = 'UNDEF'
         dtool_config["IS_LINUX"] = 'UNDEF'
         dtool_config["HAVE_VIDEO4LINUX"] = 'UNDEF'
+        dtool_config["PHAVE_LINUX_INPUT_H"] = 'UNDEF'
         dtool_config["IS_OSX"] = '1'
         # 10.4 had a broken ucontext implementation
         if int(platform.mac_ver()[0][3]) <= 4:
@@ -3867,6 +3869,34 @@ if (not RUNTIME):
   TargetAdd('libp3cull.in', opts=OPTS, input=IGATEFILES)
   TargetAdd('libp3cull.in', opts=['IMOD:panda3d.core', 'ILIB:libp3cull', 'SRCDIR:panda/src/cull'])
 
+#
+# DIRECTORY: panda/src/dgraph/
+#
+
+if (not RUNTIME):
+  OPTS=['DIR:panda/src/dgraph', 'BUILDING:PANDA']
+  TargetAdd('p3dgraph_composite1.obj', opts=OPTS, input='p3dgraph_composite1.cxx')
+  TargetAdd('p3dgraph_composite2.obj', opts=OPTS, input='p3dgraph_composite2.cxx')
+
+  OPTS=['DIR:panda/src/dgraph']
+  IGATEFILES=GetDirectoryContents('panda/src/dgraph', ["*.h", "*_composite*.cxx"])
+  TargetAdd('libp3dgraph.in', opts=OPTS, input=IGATEFILES)
+  TargetAdd('libp3dgraph.in', opts=['IMOD:panda3d.core', 'ILIB:libp3dgraph', 'SRCDIR:panda/src/dgraph'])
+
+#
+# DIRECTORY: panda/src/device/
+#
+
+if (not RUNTIME):
+  OPTS=['DIR:panda/src/device', 'BUILDING:PANDA']
+  TargetAdd('p3device_composite1.obj', opts=OPTS, input='p3device_composite1.cxx')
+  TargetAdd('p3device_composite2.obj', opts=OPTS, input='p3device_composite2.cxx')
+
+  OPTS=['DIR:panda/src/device']
+  IGATEFILES=GetDirectoryContents('panda/src/device', ["*.h", "*_composite*.cxx"])
+  TargetAdd('libp3device.in', opts=OPTS, input=IGATEFILES)
+  TargetAdd('libp3device.in', opts=['IMOD:panda3d.core', 'ILIB:libp3device', 'SRCDIR:panda/src/device'])
+
 #
 # DIRECTORY: panda/src/display/
 #
@@ -3919,34 +3949,6 @@ if (not RUNTIME):
   TargetAdd('libp3char.in', opts=OPTS, input=IGATEFILES)
   TargetAdd('libp3char.in', opts=['IMOD:panda3d.core', 'ILIB:libp3char', 'SRCDIR:panda/src/char'])
 
-#
-# DIRECTORY: panda/src/dgraph/
-#
-
-if (not RUNTIME):
-  OPTS=['DIR:panda/src/dgraph', 'BUILDING:PANDA']
-  TargetAdd('p3dgraph_composite1.obj', opts=OPTS, input='p3dgraph_composite1.cxx')
-  TargetAdd('p3dgraph_composite2.obj', opts=OPTS, input='p3dgraph_composite2.cxx')
-
-  OPTS=['DIR:panda/src/dgraph']
-  IGATEFILES=GetDirectoryContents('panda/src/dgraph', ["*.h", "*_composite*.cxx"])
-  TargetAdd('libp3dgraph.in', opts=OPTS, input=IGATEFILES)
-  TargetAdd('libp3dgraph.in', opts=['IMOD:panda3d.core', 'ILIB:libp3dgraph', 'SRCDIR:panda/src/dgraph'])
-
-#
-# DIRECTORY: panda/src/device/
-#
-
-if (not RUNTIME):
-  OPTS=['DIR:panda/src/device', 'BUILDING:PANDA']
-  TargetAdd('p3device_composite1.obj', opts=OPTS, input='p3device_composite1.cxx')
-  TargetAdd('p3device_composite2.obj', opts=OPTS, input='p3device_composite2.cxx')
-
-  OPTS=['DIR:panda/src/device']
-  IGATEFILES=GetDirectoryContents('panda/src/device', ["*.h", "*_composite*.cxx"])
-  TargetAdd('libp3device.in', opts=OPTS, input=IGATEFILES)
-  TargetAdd('libp3device.in', opts=['IMOD:panda3d.core', 'ILIB:libp3device', 'SRCDIR:panda/src/device'])
-
 #
 # DIRECTORY: panda/src/pnmtext/
 #
@@ -4111,7 +4113,8 @@ if (not RUNTIME):
 if (not RUNTIME):
   OPTS=['DIR:panda/metalibs/panda', 'BUILDING:PANDA', 'JPEG', 'PNG', 'HARFBUZZ',
       'TIFF', 'OPENEXR', 'ZLIB', 'OPENSSL', 'FREETYPE', 'FFTW', 'ADVAPI', 'WINSOCK2',
-      'SQUISH', 'NVIDIACG', 'VORBIS', 'OPUS', 'WINUSER', 'WINMM', 'WINGDI', 'IPHLPAPI']
+      'SQUISH', 'NVIDIACG', 'VORBIS', 'OPUS', 'WINUSER', 'WINMM', 'WINGDI', 'IPHLPAPI',
+      'SETUPAPI']
 
   TargetAdd('panda_panda.obj', opts=OPTS, input='panda.cxx')
 

+ 1 - 0
panda/src/cocoadisplay/cocoaGraphicsWindow.h

@@ -89,6 +89,7 @@ private:
   NSUInteger _modifier_keys;
   UInt32 _dead_key_state;
   CGDirectDisplayID _display;
+  PT(GraphicsWindowInputDevice) _input;
   bool _mouse_hidden;
   bool _context_needs_update;
 

+ 32 - 27
panda/src/cocoadisplay/cocoaGraphicsWindow.mm

@@ -99,9 +99,10 @@ CocoaGraphicsWindow(GraphicsEngine *engine, GraphicsPipe *pipe,
     [NSApp finishLaunching];
   }
 
-  GraphicsWindowInputDevice device =
+  PT(GraphicsWindowInputDevice) device =
     GraphicsWindowInputDevice::pointer_and_keyboard(this, "keyboard_mouse");
-  add_input_device(device);
+  _input_devices.push_back(device.p());
+  _input = std::move(device);
 
   CocoaGraphicsPipe *cocoa_pipe;
   DCAST_INTO_V(cocoa_pipe, _pipe);
@@ -981,7 +982,7 @@ set_properties_now(WindowProperties &properties) {
 
   if (properties.has_cursor_hidden()) {
     if (properties.get_cursor_hidden() != _properties.get_cursor_hidden()) {
-      if (properties.get_cursor_hidden() && _input_devices[0].get_pointer().get_in_window()) {
+      if (properties.get_cursor_hidden() && _input->get_pointer().get_in_window()) {
         [NSCursor hide];
         _mouse_hidden = true;
       } else if (_mouse_hidden) {
@@ -1558,7 +1559,7 @@ handle_key_event(NSEvent *event) {
     // flaws are: - OS eats unmodified F11, F12, scroll lock, pause - no up
     // events for caps lock - no robust way to distinguish updown for modkeys
     if ([event type] == NSKeyUp) {
-      _input_devices[0].raw_button_up(raw_button);
+      _input->raw_button_up(raw_button);
 
     } else if ([event type] == NSFlagsChanged) {
       bool down = false;
@@ -1580,15 +1581,15 @@ handle_key_event(NSEvent *event) {
         down = (modifierFlags & 0x0010);
       } else if (raw_button == KeyboardButton::caps_lock()) {
         // Emulate down-up, annoying hack!
-        _input_devices[0].raw_button_down(raw_button);
+        _input->raw_button_down(raw_button);
       }
       if (down) {
-        _input_devices[0].raw_button_down(raw_button);
+        _input->raw_button_down(raw_button);
       } else {
-        _input_devices[0].raw_button_up(raw_button);
+        _input->raw_button_up(raw_button);
       }
     } else if (![event isARepeat]) {
-      _input_devices[0].raw_button_down(raw_button);
+      _input->raw_button_down(raw_button);
     }
   }
 
@@ -1622,7 +1623,7 @@ handle_key_event(NSEvent *event) {
         }
         cocoadisplay_cat.spam(false) << "\n";
       }
-      _input_devices[0].keystroke(c);
+      _input->keystroke(c);
     }
   }
 
@@ -1634,6 +1635,7 @@ handle_key_event(NSEvent *event) {
   unichar c = [str characterAtIndex: 0];
 
   ButtonHandle button = map_key(c);
+
   if (button == ButtonHandle::none()) {
     // That done, continue trying to find out the button handle.
     if ([str canBeConvertedToEncoding: NSASCIIStringEncoding]) {
@@ -1659,9 +1661,9 @@ handle_key_event(NSEvent *event) {
 
   // Let's get it off our chest.
   if ([event type] == NSKeyUp) {
-    _input_devices[0].button_up(button);
+    _input->button_up(button);
   } else {
-    _input_devices[0].button_down(button);
+    _input->button_down(button);
   }
 }
 
@@ -1672,9 +1674,9 @@ void CocoaGraphicsWindow::
 handle_modifier(NSUInteger modifierFlags, NSUInteger mask, ButtonHandle button) {
   if ((modifierFlags ^ _modifier_keys) & mask) {
     if (modifierFlags & mask) {
-      _input_devices[0].button_down(button);
+      _input->button_down(button);
     } else {
-      _input_devices[0].button_up(button);
+      _input->button_up(button);
     }
   }
 }
@@ -1686,14 +1688,14 @@ handle_modifier(NSUInteger modifierFlags, NSUInteger mask, ButtonHandle button)
 void CocoaGraphicsWindow::
 handle_mouse_button_event(int button, bool down) {
   if (down) {
-    _input_devices[0].button_down(MouseButton::button(button));
+    _input->button_down(MouseButton::button(button));
 
 #ifndef NDEBUG
     cocoadisplay_cat.spam()
       << "Mouse button " << button << " down\n";
 #endif
   } else {
-    _input_devices[0].button_up(MouseButton::button(button));
+    _input->button_up(MouseButton::button(button));
 
 #ifndef NDEBUG
     cocoadisplay_cat.spam()
@@ -1712,7 +1714,7 @@ handle_mouse_moved_event(bool in_window, double x, double y, bool absolute) {
 
   if (absolute) {
     if (cocoadisplay_cat.is_spam()) {
-      if (in_window != _input_devices[0].get_pointer().get_in_window()) {
+      if (in_window != _input->get_pointer().get_in_window()) {
         if (in_window) {
           cocoadisplay_cat.spam() << "Mouse pointer entered window\n";
         } else {
@@ -1727,7 +1729,7 @@ handle_mouse_moved_event(bool in_window, double x, double y, bool absolute) {
 
   } else {
     // We received deltas, so add it to the current mouse position.
-    MouseData md = _input_devices[0].get_pointer();
+    MouseData md = _input->get_pointer();
     nx = md.get_x() + x;
     ny = md.get_y() + y;
   }
@@ -1753,8 +1755,11 @@ handle_mouse_moved_event(bool in_window, double x, double y, bool absolute) {
     }
   }
 
-  _input_devices[0].set_pointer(in_window, nx, ny,
-      ClockObject::get_global_clock()->get_frame_time());
+  if (in_window) {
+    _input->set_pointer_in_window(nx, ny);
+  } else {
+    _input->set_pointer_out_of_window();
+  }
 
   if (in_window != _mouse_hidden && _properties.get_cursor_hidden()) {
     // Hide the cursor if the mouse enters the window, and unhide it when the
@@ -1777,20 +1782,20 @@ handle_wheel_event(double x, double y) {
     << "Wheel delta " << x << ", " << y << "\n";
 
   if (y > 0.0) {
-    _input_devices[0].button_down(MouseButton::wheel_up());
-    _input_devices[0].button_up(MouseButton::wheel_up());
+    _input->button_down(MouseButton::wheel_up());
+    _input->button_up(MouseButton::wheel_up());
   } else if (y < 0.0) {
-    _input_devices[0].button_down(MouseButton::wheel_down());
-    _input_devices[0].button_up(MouseButton::wheel_down());
+    _input->button_down(MouseButton::wheel_down());
+    _input->button_up(MouseButton::wheel_down());
   }
 
   // TODO: check if this is correct, I don't own a MacBook
   if (x > 0.0) {
-    _input_devices[0].button_down(MouseButton::wheel_right());
-    _input_devices[0].button_up(MouseButton::wheel_right());
+    _input->button_down(MouseButton::wheel_right());
+    _input->button_up(MouseButton::wheel_right());
   } else if (x < 0.0) {
-    _input_devices[0].button_down(MouseButton::wheel_left());
-    _input_devices[0].button_up(MouseButton::wheel_left());
+    _input->button_down(MouseButton::wheel_left());
+    _input->button_up(MouseButton::wheel_left());
   }
 }
 

+ 3 - 12
panda/src/device/analogNode.I

@@ -35,10 +35,7 @@ is_valid() const {
  */
 INLINE int AnalogNode::
 get_num_controls() const {
-  _analog->acquire();
-  int result = _analog->get_num_controls();
-  _analog->unlock();
-  return result;
+  return _analog->get_num_axes();
 }
 
 /**
@@ -48,10 +45,7 @@ get_num_controls() const {
  */
 INLINE double AnalogNode::
 get_control_state(int index) const {
-  _analog->acquire();
-  double result = _analog->get_control_state(index);
-  _analog->unlock();
-  return result;
+  return _analog->get_axis_value(index);
 }
 
 /**
@@ -60,10 +54,7 @@ get_control_state(int index) const {
  */
 INLINE bool AnalogNode::
 is_control_known(int index) const {
-  _analog->acquire();
-  bool result = _analog->is_control_known(index);
-  _analog->unlock();
-  return result;
+  return _analog->is_axis_known(index);
 }
 
 /**

+ 25 - 14
panda/src/device/analogNode.cxx

@@ -27,7 +27,7 @@ AnalogNode(ClientBase *client, const std::string &device_name) :
   DataNode(device_name)
 {
   _xy_output = define_output("xy", EventStoreVec2::get_class_type());
-  _xy = new EventStoreVec2(LPoint2(0.0f, 0.0f));
+  _xy = new EventStoreVec2(LPoint2(0));
 
   nassertv(client != nullptr);
   PT(ClientDevice) device =
@@ -46,7 +46,21 @@ AnalogNode(ClientBase *client, const std::string &device_name) :
     return;
   }
 
-  _analog = DCAST(ClientAnalogDevice, device);
+  _analog = device;
+}
+
+/**
+ *
+ */
+AnalogNode::
+AnalogNode(InputDevice *device) :
+  DataNode(device->get_name()),
+  _analog(device)
+{
+  _xy_output = define_output("xy", EventStoreVec2::get_class_type());
+  _xy = new EventStoreVec2(LPoint2(0));
+
+  nassertv(device != nullptr);
 }
 
 /**
@@ -67,9 +81,7 @@ write(std::ostream &out, int indent_level) const {
   DataNode::write(out, indent_level);
 
   if (_analog != nullptr) {
-    _analog->acquire();
-    _analog->write_controls(out, indent_level + 2);
-    _analog->unlock();
+    _analog->write_axes(out, indent_level + 2);
   }
 }
 
@@ -88,19 +100,18 @@ do_transmit_data(DataGraphTraverser *, const DataNodeTransmit &,
     _analog->poll();
 
     LPoint2 out(0.0f, 0.0f);
-
-    _analog->acquire();
     for (int i = 0; i < max_outputs; i++) {
-      if (_outputs[i]._index >= 0 &&
-          _analog->is_control_known(_outputs[i]._index)) {
-        if (_outputs[i]._flip) {
-          out[i] = -_analog->get_control_state(_outputs[i]._index);
-        } else {
-          out[i] = _analog->get_control_state(_outputs[i]._index);
+      if (_outputs[i]._index >= 0) {
+        InputDevice::AxisState state = _analog->get_axis(_outputs[i]._index);
+        if (state.known) {
+          if (_outputs[i]._flip) {
+            out[i] = -state.value;
+          } else {
+            out[i] = state.value;
+          }
         }
       }
     }
-    _analog->unlock();
     _xy->set_value(out);
     output.set_data(_xy_output, EventParameter(_xy));
   }

+ 2 - 1
panda/src/device/analogNode.h

@@ -39,6 +39,7 @@
 class EXPCL_PANDA_DEVICE AnalogNode : public DataNode {
 PUBLISHED:
   explicit AnalogNode(ClientBase *client, const std::string &device_name);
+  explicit AnalogNode(InputDevice *device);
   virtual ~AnalogNode();
 
   INLINE bool is_valid() const;
@@ -67,7 +68,7 @@ private:
   enum { max_outputs = 2 };
   OutputData _outputs[max_outputs];
 
-  PT(ClientAnalogDevice) _analog;
+  PT(InputDevice) _analog;
 
 protected:
   // Inherited from DataNode

+ 6 - 20
panda/src/device/buttonNode.I

@@ -17,7 +17,7 @@
  */
 INLINE bool ButtonNode::
 is_valid() const {
-  return (_button != nullptr) && _button->is_connected();
+  return (_device != nullptr) && _device->is_connected();
 }
 
 /**
@@ -28,10 +28,7 @@ is_valid() const {
  */
 INLINE int ButtonNode::
 get_num_buttons() const {
-  _button->acquire();
-  int result = _button->get_num_buttons();
-  _button->unlock();
-  return result;
+  return _device->get_num_buttons();
 }
 
 /**
@@ -46,9 +43,7 @@ get_num_buttons() const {
  */
 INLINE void ButtonNode::
 set_button_map(int index, ButtonHandle button) {
-  _button->acquire();
-  _button->set_button_map(index, button);
-  _button->unlock();
+  _device->map_button(index, button);
 }
 
 /**
@@ -58,10 +53,7 @@ set_button_map(int index, ButtonHandle button) {
  */
 INLINE ButtonHandle ButtonNode::
 get_button_map(int index) const {
-  _button->acquire();
-  ButtonHandle result = _button->get_button_map(index);
-  _button->unlock();
-  return result;
+  return _device->get_button_map(index);
 }
 
 /**
@@ -70,10 +62,7 @@ get_button_map(int index) const {
  */
 INLINE bool ButtonNode::
 get_button_state(int index) const {
-  _button->acquire();
-  bool result = _button->get_button_state(index);
-  _button->unlock();
-  return result;
+  return _device->is_button_pressed(index);
 }
 
 /**
@@ -82,8 +71,5 @@ get_button_state(int index) const {
  */
 INLINE bool ButtonNode::
 is_button_known(int index) const {
-  _button->acquire();
-  bool result = _button->is_button_known(index);
-  _button->unlock();
-  return result;
+  return _device->is_button_known(index);
 }

+ 19 - 19
panda/src/device/buttonNode.cxx

@@ -27,7 +27,6 @@ ButtonNode(ClientBase *client, const std::string &device_name) :
   DataNode(device_name)
 {
   _button_events_output = define_output("button_events", ButtonEventList::get_class_type());
-  _button_events = new ButtonEventList;
 
   nassertv(client != nullptr);
   PT(ClientDevice) device =
@@ -46,7 +45,19 @@ ButtonNode(ClientBase *client, const std::string &device_name) :
     return;
   }
 
-  _button = DCAST(ClientButtonDevice, device);
+  _device = device;
+}
+
+/**
+ *
+ */
+ButtonNode::
+ButtonNode(InputDevice *device) :
+  DataNode(device->get_name()),
+  _device(device)
+{
+  _button_events_output = define_output("button_events", ButtonEventList::get_class_type());
+  _device = device;
 }
 
 /**
@@ -66,11 +77,9 @@ void ButtonNode::
 output(std::ostream &out) const {
   DataNode::output(out);
 
-  if (_button != nullptr) {
+  if (_device != nullptr) {
     out << " (";
-    _button->acquire();
-    _button->output_buttons(out);
-    _button->unlock();
+    _device->output_buttons(out);
     out << ")";
   }
 }
@@ -82,10 +91,8 @@ void ButtonNode::
 write(std::ostream &out, int indent_level) const {
   DataNode::write(out, indent_level);
 
-  if (_button != nullptr) {
-    _button->acquire();
-    _button->write_buttons(out, indent_level + 2);
-    _button->unlock();
+  if (_device != nullptr) {
+    _device->write_buttons(out, indent_level + 2);
   }
 }
 
@@ -101,14 +108,7 @@ void ButtonNode::
 do_transmit_data(DataGraphTraverser *, const DataNodeTransmit &,
                  DataNodeTransmit &output) {
   if (is_valid()) {
-    _button->poll();
-    _button->acquire();
-
-    (*_button_events) = (*_button->get_button_events());
-
-    _button->get_button_events()->clear();
-    _button->unlock();
-
-    output.set_data(_button_events_output, EventParameter(_button_events));
+    PT(ButtonEventList) bel = _device->get_button_events();
+    output.set_data(_button_events_output, EventParameter(bel));
   }
 }

+ 2 - 4
panda/src/device/buttonNode.h

@@ -19,8 +19,6 @@
 #include "clientBase.h"
 #include "clientButtonDevice.h"
 #include "dataNode.h"
-#include "buttonEventList.h"
-
 
 /**
  * This is the primary interface to on/off button devices associated with a
@@ -35,6 +33,7 @@
 class EXPCL_PANDA_DEVICE ButtonNode : public DataNode {
 PUBLISHED:
   explicit ButtonNode(ClientBase *client, const std::string &device_name);
+  explicit ButtonNode(InputDevice *device);
   virtual ~ButtonNode();
 
   INLINE bool is_valid() const;
@@ -52,7 +51,7 @@ public:
   virtual void write(std::ostream &out, int indent_level = 0) const;
 
 private:
-  PT(ClientButtonDevice) _button;
+  PT(InputDevice) _device;
 
 protected:
   // Inherited from DataNode
@@ -63,7 +62,6 @@ protected:
 private:
   // outputs
   int _button_events_output;
-  PT(ButtonEventList) _button_events;
 
 public:
   static TypeHandle get_class_type() {

+ 0 - 60
panda/src/device/clientAnalogDevice.I

@@ -11,16 +11,6 @@
  * @date 2001-01-26
  */
 
-/**
- *
- */
-INLINE ClientAnalogDevice::AnalogState::
-AnalogState() :
-  _state(0.0),
-  _known(false)
-{
-}
-
 /**
  *
  */
@@ -29,53 +19,3 @@ ClientAnalogDevice(ClientBase *client, const std::string &device_name):
   ClientDevice(client, get_class_type(), device_name)
 {
 }
-
-/**
- * Returns the number of analog controls known to the ClientAnalogDevice.
- * This number may change as more controls are discovered.
- */
-INLINE int ClientAnalogDevice::
-get_num_controls() const {
-  return _controls.size();
-}
-
-/**
- * Sets the state of the indicated analog index.  The caller should ensure
- * that acquire() is in effect while this call is made.  This should be a
- * number in the range -1.0 to 1.0, representing the current position of the
- * control within its total range of movement.
- */
-INLINE void ClientAnalogDevice::
-set_control_state(int index, double state) {
-  ensure_control_index(index);
-  nassertv(index >= 0 && index < (int)_controls.size());
-  _controls[index]._state = state;
-  _controls[index]._known = true;
-}
-
-/**
- * Returns the current position of indicated analog control (identified by its
- * index number), or 0.0 if the control is unknown.  The normal range of a
- * single control is -1.0 to 1.0.
- */
-INLINE double ClientAnalogDevice::
-get_control_state(int index) const {
-  if (index >= 0 && index < (int)_controls.size()) {
-    return _controls[index]._state;
-  } else {
-    return 0.0;
-  }
-}
-
-/**
- * Returns true if the state of the indicated analog control is known, or
- * false if we have never heard anything about this particular control.
- */
-INLINE bool ClientAnalogDevice::
-is_control_known(int index) const {
-  if (index >= 0 && index < (int)_controls.size()) {
-    return _controls[index]._known;
-  } else {
-    return false;
-  }
-}

+ 2 - 41
panda/src/device/clientAnalogDevice.cxx

@@ -17,50 +17,11 @@
 
 TypeHandle ClientAnalogDevice::_type_handle;
 
-
-
-/**
- * Guarantees that there is a slot in the array for the indicated index
- * number, by filling the array up to that index if necessary.
- */
-void ClientAnalogDevice::
-ensure_control_index(int index) {
-  nassertv(index >= 0);
-
-  _controls.reserve(index + 1);
-  while ((int)_controls.size() <= index) {
-    _controls.push_back(AnalogState());
-  }
-}
-
 /**
  *
  */
 void ClientAnalogDevice::
 write(std::ostream &out, int indent_level) const {
-  indent(out, indent_level) << get_type() << " " << get_device_name() << ":\n";
-  write_controls(out, indent_level + 2);
-}
-
-/**
- * Writes a multi-line description of the current analog control states.
- */
-void ClientAnalogDevice::
-write_controls(std::ostream &out, int indent_level) const {
-  bool any_controls = false;
-  Controls::const_iterator ai;
-  for (ai = _controls.begin(); ai != _controls.end(); ++ai) {
-    const AnalogState &state = (*ai);
-    if (state._known) {
-      any_controls = true;
-
-      indent(out, indent_level)
-        << (int)(ai - _controls.begin()) << ". " << state._state << "\n";
-    }
-  }
-
-  if (!any_controls) {
-    indent(out, indent_level)
-      << "(no known analog controls)\n";
-  }
+  indent(out, indent_level) << get_type() << " " << get_name() << ":\n";
+  write_axes(out, indent_level + 2);
 }

+ 0 - 23
panda/src/device/clientAnalogDevice.h

@@ -31,30 +31,7 @@ protected:
   INLINE ClientAnalogDevice(ClientBase *client, const std::string &device_name);
 
 public:
-  INLINE int get_num_controls() const;
-
-  INLINE void set_control_state(int index, double state);
-  INLINE double get_control_state(int index) const;
-  INLINE bool is_control_known(int index) const;
-
   virtual void write(std::ostream &out, int indent_level = 0) const;
-  void write_controls(std::ostream &out, int indent_level) const;
-
-private:
-  void ensure_control_index(int index);
-
-protected:
-  class AnalogState {
-  public:
-    INLINE AnalogState();
-
-    double _state;
-    bool _known;
-  };
-
-  typedef pvector<AnalogState> Controls;
-  Controls _controls;
-
 
 public:
   static TypeHandle get_class_type() {

+ 0 - 1
panda/src/device/clientBase.cxx

@@ -128,7 +128,6 @@ get_device(TypeHandle device_type, const std::string &device_name) {
 
   if (device != nullptr) {
     dbn.insert(DevicesByName::value_type(device_name, device));
-    device->_is_connected = true;
   }
 
   return device;

+ 0 - 88
panda/src/device/clientButtonDevice.I

@@ -10,91 +10,3 @@
  * @author drose
  * @date 2001-01-26
  */
-
-/**
- *
- */
-INLINE ClientButtonDevice::ButtonState::
-ButtonState() :
-  _handle(ButtonHandle::none()),
-  _state(S_unknown)
-{
-}
-
-
-/**
- * Returns the number of buttons known to the ClientButtonDevice.  This
- * includes those buttons whose state has been seen, as well as buttons that
- * have been associated with a ButtonHandle even if their state is unknown.
- * This number may change as more buttons are discovered.
- */
-INLINE int ClientButtonDevice::
-get_num_buttons() const {
-  return _buttons.size();
-}
-
-/**
- * Associates the indicated ButtonHandle with the button of the indicated
- * index number.  When the given button index changes state, a corresponding
- * ButtonEvent will be generated with the given ButtonHandle.  Pass
- * ButtonHandle::none() to turn off any association.
- *
- * It is not necessary to call this if you simply want to query the state of
- * the various buttons by index number; this is only necessary in order to
- * generate ButtonEvents when the buttons change state.
- */
-INLINE void ClientButtonDevice::
-set_button_map(int index, ButtonHandle button) {
-  ensure_button_index(index);
-  nassertv(index >= 0 && index < (int)_buttons.size());
-  _buttons[index]._handle = button;
-}
-
-/**
- * Returns the ButtonHandle that was previously associated with the given
- * index number by a call to set_button_map(), or ButtonHandle::none() if no
- * button was associated.
- */
-INLINE ButtonHandle ClientButtonDevice::
-get_button_map(int index) const {
-  if (index >= 0 && index < (int)_buttons.size()) {
-    return _buttons[index]._handle;
-  } else {
-    return ButtonHandle::none();
-  }
-}
-
-/**
- * Returns true if the indicated button (identified by its index number) is
- * currently known to be down, or false if it is up or unknown.
- */
-INLINE bool ClientButtonDevice::
-get_button_state(int index) const {
-  if (index >= 0 && index < (int)_buttons.size()) {
-    return (_buttons[index]._state == S_down);
-  } else {
-    return false;
-  }
-}
-
-/**
- * Returns true if the state of the indicated button is known, or false if we
- * have never heard anything about this particular button.
- */
-INLINE bool ClientButtonDevice::
-is_button_known(int index) const {
-  if (index >= 0 && index < (int)_buttons.size()) {
-    return _buttons[index]._state != S_unknown;
-  } else {
-    return false;
-  }
-}
-
-/**
- * Returns the list of recently-generated ButtonEvents.  This must be
- * periodically cleared, or the buttons will accumulate.
- */
-INLINE ButtonEventList *ClientButtonDevice::
-get_button_events() const {
-  return _button_events;
-}

+ 2 - 99
panda/src/device/clientButtonDevice.cxx

@@ -26,41 +26,6 @@ ClientButtonDevice::
 ClientButtonDevice(ClientBase *client, const std::string &device_name):
   ClientDevice(client, get_class_type(), device_name)
 {
-  _button_events = new ButtonEventList();
-}
-
-
-/**
- * Sets the state of the indicated button index, where true indicates down,
- * and false indicates up.  This may generate a ButtonEvent if the button has
- * an associated ButtonHandle.  The caller should ensure that acquire() is in
- * effect while this call is made.
- */
-void ClientButtonDevice::
-set_button_state(int index, bool down) {
-  ensure_button_index(index);
-  nassertv(index >= 0 && index < (int)_buttons.size());
-  _buttons[index]._state = down ? S_down : S_up;
-
-  ButtonHandle handle = _buttons[index]._handle;
-  if (handle != ButtonHandle::none()) {
-    _button_events->add_event(ButtonEvent(handle, down ? ButtonEvent::T_down : ButtonEvent::T_up));
-  }
-}
-
-
-/**
- * Guarantees that there is a slot in the array for the indicated index
- * number, by filling the array up to that index if necessary.
- */
-void ClientButtonDevice::
-ensure_button_index(int index) {
-  nassertv(index >= 0);
-
-  _buttons.reserve(index + 1);
-  while ((int)_buttons.size() <= index) {
-    _buttons.push_back(ButtonState());
-  }
 }
 
 /**
@@ -68,7 +33,7 @@ ensure_button_index(int index) {
  */
 void ClientButtonDevice::
 output(ostream &out) const {
-  out << get_type() << " " << get_device_name() << " (";
+  out << get_type() << " " << get_name() << " (";
   output_buttons(out);
   out << ")";
 }
@@ -78,68 +43,6 @@ output(ostream &out) const {
  */
 void ClientButtonDevice::
 write(ostream &out, int indent_level) const {
-  indent(out, indent_level) << get_type() << " " << get_device_name() << ":\n";
+  indent(out, indent_level) << get_type() << " " << get_name() << ":\n";
   write_buttons(out, indent_level + 2);
 }
-
-/**
- * Writes a one-line string of all of the current button states.
- */
-void ClientButtonDevice::
-output_buttons(ostream &out) const {
-  bool any_buttons = false;
-  Buttons::const_iterator bi;
-  for (bi = _buttons.begin(); bi != _buttons.end(); ++bi) {
-    const ButtonState &state = (*bi);
-    if (state._state != S_unknown) {
-      if (any_buttons) {
-        out << ", ";
-      }
-      any_buttons = true;
-      out << (int)(bi - _buttons.begin()) << "=";
-      if (state._state == S_up) {
-        out << "up";
-      } else {
-        out << "down";
-      }
-    }
-  }
-
-  if (!any_buttons) {
-    out << "no known buttons";
-  }
-}
-
-/**
- * Writes a multi-line description of the current button states.
- */
-void ClientButtonDevice::
-write_buttons(ostream &out, int indent_level) const {
-  bool any_buttons = false;
-  Buttons::const_iterator bi;
-  for (bi = _buttons.begin(); bi != _buttons.end(); ++bi) {
-    const ButtonState &state = (*bi);
-    if (state._state != S_unknown) {
-      any_buttons = true;
-
-      indent(out, indent_level)
-        << (int)(bi - _buttons.begin()) << ". ";
-
-      if (state._handle != ButtonHandle::none()) {
-        out << "(" << state._handle << ") ";
-      }
-
-      if (state._state == S_up) {
-        out << "up";
-      } else {
-        out << "down";
-      }
-      out << "\n";
-    }
-  }
-
-  if (!any_buttons) {
-    indent(out, indent_level)
-      << "(no known buttons)\n";
-  }
-}

+ 0 - 38
panda/src/device/clientButtonDevice.h

@@ -34,46 +34,9 @@ protected:
   ClientButtonDevice(ClientBase *client, const std::string &device_name);
 
 public:
-  INLINE int get_num_buttons() const;
-
-  INLINE void set_button_map(int index, ButtonHandle button);
-  INLINE ButtonHandle get_button_map(int index) const;
-
-  void set_button_state(int index, bool down);
-  INLINE bool get_button_state(int index) const;
-  INLINE bool is_button_known(int index) const;
-
-  INLINE ButtonEventList *get_button_events() const;
-
   virtual void output(std::ostream &out) const;
   virtual void write(std::ostream &out, int indent_level = 0) const;
 
-  void output_buttons(std::ostream &out) const;
-  void write_buttons(std::ostream &out, int indent_level) const;
-
-private:
-  void ensure_button_index(int index);
-
-protected:
-  enum State {
-    S_unknown,
-    S_up,
-    S_down
-  };
-
-  class ButtonState {
-  public:
-    INLINE ButtonState();
-
-    ButtonHandle _handle;
-    State _state;
-  };
-
-  typedef pvector<ButtonState> Buttons;
-  Buttons _buttons;
-
-  PT(ButtonEventList) _button_events;
-
 public:
   static TypeHandle get_class_type() {
     return _type_handle;
@@ -90,7 +53,6 @@ public:
 
 private:
   static TypeHandle _type_handle;
-  friend class ButtonState;
 };
 
 #include "clientButtonDevice.I"

+ 0 - 41
panda/src/device/clientDevice.I

@@ -19,15 +19,6 @@ get_client() const {
   return _client;
 }
 
-/**
- * Returns true if the device is still connected to its ClientBase, false
- * otherwise.
- */
-INLINE bool ClientDevice::
-is_connected() const {
-  return _is_connected;
-}
-
 /**
  * Returns the type of device this is considered to be to the ClientBase: a
  * ClientTrackerDevice, ClientAnalogDevice, or what have you.  This is not
@@ -39,35 +30,3 @@ INLINE TypeHandle ClientDevice::
 get_device_type() const {
   return _device_type;
 }
-
-/**
- * Returns the device name reported to the ClientBase.  This has some
- * implementation-defined meaning to identify particular devices.
- */
-INLINE const std::string &ClientDevice::
-get_device_name() const {
-  return _device_name;
-}
-
-/**
- * Grabs the mutex associated with this particular device.  The device will
- * not update asynchronously while the mutex is held, allowing the user to
- * copy the data out without fear of getting a partial update during the copy.
- */
-INLINE void ClientDevice::
-acquire() {
-#ifdef OLD_HAVE_IPC
-  _lock.acquire();
-#endif
-}
-
-/**
- * Releases the mutex associated with this particular device.  This should be
- * called after all the data has been successfully copied out.  See acquire().
- */
-INLINE void ClientDevice::
-unlock() {
-#ifdef OLD_HAVE_IPC
-  _lock.unlock();
-#endif
-}

+ 8 - 11
panda/src/device/clientDevice.cxx

@@ -24,14 +24,13 @@ TypeHandle ClientDevice::_type_handle;
 ClientDevice::
 ClientDevice(ClientBase *client, TypeHandle device_type,
              const std::string &device_name) :
+  InputDevice(device_name, DeviceClass::unknown),
   _client(client),
-  _device_type(device_type),
-  _device_name(device_name)
+  _device_type(device_type)
 {
   // We have to explicitly ref the client pointer, since we can't use a
   // PT(ClientBase) for circular include reasons.
   _client->ref();
-  _is_connected = false;
 }
 
 /**
@@ -41,7 +40,7 @@ ClientDevice(ClientBase *client, TypeHandle device_type,
  */
 ClientDevice::
 ~ClientDevice() {
-  nassertv(!_is_connected);
+  nassertv(!is_connected());
 
   // And now we explicitly unref the client pointer.
   unref_delete(_client);
@@ -60,12 +59,10 @@ ClientDevice::
  */
 void ClientDevice::
 disconnect() {
-  if (_is_connected) {
-    acquire();
+  if (is_connected()) {
     bool disconnected =
-      _client->disconnect_device(_device_type, _device_name, this);
-    _is_connected = false;
-    unlock();
+      _client->disconnect_device(_device_type, get_name(), this);
+    set_connected(false);
     nassertv(disconnected);
   }
 }
@@ -79,7 +76,7 @@ disconnect() {
  * ClientDevice to ensure that it is fresh.
  */
 void ClientDevice::
-poll() {
+do_poll() {
   _client->poll();
 }
 
@@ -88,7 +85,7 @@ poll() {
  */
 void ClientDevice::
 output(std::ostream &out) const {
-  out << get_type() << " " << get_device_name();
+  out << get_type() << " " << get_name();
 }
 
 /**

+ 5 - 20
panda/src/device/clientDevice.h

@@ -15,12 +15,7 @@
 #define CLIENTDEVICE_H
 
 #include "pandabase.h"
-
-#include "typedReferenceCount.h"
-
-#ifdef OLD_HAVE_IPC
-#include <ipc_mutex.h>
-#endif
+#include "inputDevice.h"
 
 class ClientBase;
 
@@ -29,7 +24,7 @@ class ClientBase;
  * ClientBase, including trackers, etc.  This is an abstract interface; the
  * actual implementations are in ClientTrackerDevice, etc.
  */
-class EXPCL_PANDA_DEVICE ClientDevice : public TypedReferenceCount {
+class EXPCL_PANDA_DEVICE ClientDevice : public InputDevice {
 protected:
   ClientDevice(ClientBase *client, TypeHandle device_type,
                const std::string &device_name);
@@ -39,14 +34,10 @@ public:
 
   INLINE ClientBase *get_client() const;
   INLINE TypeHandle get_device_type() const;
-  INLINE const std::string &get_device_name() const;
 
-  INLINE bool is_connected() const;
   void disconnect();
 
-  void poll();
-  INLINE void acquire();
-  INLINE void unlock();
+  virtual void do_poll() final;
 
   virtual void output(std::ostream &out) const;
   virtual void write(std::ostream &out, int indent_level = 0) const;
@@ -54,21 +45,15 @@ public:
 private:
   ClientBase *_client;
   TypeHandle _device_type;
-  std::string _device_name;
-  bool _is_connected;
-
-#ifdef OLD_HAVE_IPC
-  mutex _lock;
-#endif
 
 public:
   static TypeHandle get_class_type() {
     return _type_handle;
   }
   static void init_type() {
-    TypedReferenceCount::init_type();
+    InputDevice::init_type();
     register_type(_type_handle, "ClientDevice",
-                  TypedReferenceCount::get_class_type());
+                  InputDevice::get_class_type());
   }
   virtual TypeHandle get_type() const {
     return get_class_type();

+ 1 - 8
panda/src/device/clientTrackerDevice.I

@@ -18,12 +18,5 @@ INLINE ClientTrackerDevice::
 ClientTrackerDevice(ClientBase *client, const std::string &device_name):
   ClientDevice(client, get_class_type(), device_name)
 {
-}
-
-/**
- * Returns the TrackerData that this device is reporting.
- */
-INLINE const TrackerData &ClientTrackerDevice::
-get_data() const {
-  return _data;
+  enable_feature(Feature::tracker);
 }

+ 0 - 6
panda/src/device/clientTrackerDevice.h

@@ -27,12 +27,6 @@ class EXPCL_PANDA_DEVICE ClientTrackerDevice : public ClientDevice {
 protected:
   INLINE ClientTrackerDevice(ClientBase *client, const std::string &device_name);
 
-public:
-  INLINE const TrackerData &get_data() const;
-
-protected:
-  TrackerData _data;
-
 public:
   static TypeHandle get_class_type() {
     return _type_handle;

+ 14 - 2
panda/src/device/config_device.cxx

@@ -21,9 +21,12 @@
 #include "clientDialDevice.h"
 #include "clientTrackerDevice.h"
 #include "dialNode.h"
-#include "mouseAndKeyboard.h"
+#include "evdevInputDevice.h"
+#include "inputDevice.h"
+#include "linuxJoystickDevice.h"
 #include "trackerNode.h"
 #include "virtualMouse.h"
+#include "xInputDevice.h"
 
 #include "dconfig.h"
 
@@ -64,7 +67,16 @@ init_libdevice() {
   ClientDialDevice::init_type();
   ClientTrackerDevice::init_type();
   DialNode::init_type();
-  MouseAndKeyboard::init_type();
+  InputDevice::init_type();
   TrackerNode::init_type();
   VirtualMouse::init_type();
+
+#ifdef PHAVE_LINUX_INPUT_H
+  EvdevInputDevice::init_type();
+  LinuxJoystickDevice::init_type();
+#endif
+
+#ifdef _WIN32
+  XInputDevice::init_type();
+#endif
 }

+ 1 - 0
panda/src/device/config_device.h

@@ -17,6 +17,7 @@
 #include "pandabase.h"
 #include "notifyCategoryProxy.h"
 #include "configVariableBool.h"
+#include "configVariableInt.h"
 
 NotifyCategoryDecl(device, EXPCL_PANDA_DEVICE, EXPTP_PANDA_DEVICE);
 

+ 3 - 12
panda/src/device/dialNode.I

@@ -26,10 +26,7 @@ is_valid() const {
  */
 INLINE int DialNode::
 get_num_dials() const {
-  _dial->acquire();
-  int result = _dial->get_num_dials();
-  _dial->unlock();
-  return result;
+  return _dial->get_num_dials();
 }
 
 /**
@@ -39,10 +36,7 @@ get_num_dials() const {
  */
 INLINE double DialNode::
 read_dial(int index) {
-  _dial->acquire();
-  double result = _dial->read_dial(index);
-  _dial->unlock();
-  return result;
+  return _dial->read_dial(index);
 }
 
 /**
@@ -51,8 +45,5 @@ read_dial(int index) {
  */
 INLINE bool DialNode::
 is_dial_known(int index) const {
-  _dial->acquire();
-  bool result = _dial->is_dial_known(index);
-  _dial->unlock();
-  return result;
+  return _dial->is_dial_known(index);
 }

+ 12 - 0
panda/src/device/evdevInputDevice.I

@@ -0,0 +1,12 @@
+/**
+ * PANDA 3D SOFTWARE
+ * Copyright (c) Carnegie Mellon University.  All rights reserved.
+ *
+ * All use of this software is subject to the terms of the revised BSD
+ * license.  You should have received a copy of this license along
+ * with this source code in a file named "LICENSE."
+ *
+ * @file evdevInputDevice.I
+ * @author rdb
+ * @date 2015-08-24
+ */

+ 997 - 0
panda/src/device/evdevInputDevice.cxx

@@ -0,0 +1,997 @@
+/**
+ * PANDA 3D SOFTWARE
+ * Copyright (c) Carnegie Mellon University.  All rights reserved.
+ *
+ * All use of this software is subject to the terms of the revised BSD
+ * license.  You should have received a copy of this license along
+ * with this source code in a file named "LICENSE."
+ *
+ * @file evdevInputDevice.cxx
+ * @author rdb
+ * @date 2015-08-24
+ */
+
+#include "evdevInputDevice.h"
+
+#ifdef PHAVE_LINUX_INPUT_H
+
+#include "gamepadButton.h"
+#include "keyboardButton.h"
+#include "mouseButton.h"
+#include "linuxInputDeviceManager.h"
+
+#include <fcntl.h>
+#include <linux/input.h>
+
+#ifndef BTN_DPAD_UP
+#define BTN_DPAD_UP 0x220
+#define BTN_DPAD_DOWN 0x221
+#define BTN_DPAD_LEFT 0x222
+#define BTN_DPAD_RIGHT 0x223
+#endif
+
+
+// Android introduces these in API level 21.
+#ifndef BTN_TRIGGER_HAPPY
+#define BTN_TRIGGER_HAPPY 0x2c0
+#define BTN_TRIGGER_HAPPY1 0x2c0
+#define BTN_TRIGGER_HAPPY2 0x2c1
+#define BTN_TRIGGER_HAPPY3 0x2c2
+#define BTN_TRIGGER_HAPPY4 0x2c3
+#endif
+
+#define test_bit(bit, array) ((array)[(bit)>>3] & (1<<((bit)&7)))
+
+enum QuirkBits {
+  // Right stick uses Z and Rz inputs.
+  QB_rstick_from_z = 1,
+
+  // Throttle goes from -1 to 1 rather than from 0 to 1.
+  QB_centered_throttle = 2,
+
+  // Throttle is reversed.
+  QB_reversed_throttle = 4,
+
+  // Only consider the device "connected" if all axes are non-zero.
+  QB_connect_if_nonzero = 8,
+
+  // ABS_THROTTLE maps to rudder
+  QB_rudder_from_throttle = 16,
+};
+
+static const struct DeviceMapping {
+  unsigned short vendor;
+  unsigned short product;
+  InputDevice::DeviceClass device_class;
+  int quirks;
+} mapping_presets[] = {
+  // NVIDIA Shield Controller
+  {0x0955, 0x7214, InputDevice::DeviceClass::gamepad, QB_rstick_from_z},
+  // T.Flight Hotas X
+  {0x044f, 0xb108, InputDevice::DeviceClass::flight_stick, QB_centered_throttle | QB_reversed_throttle | QB_rudder_from_throttle},
+  // Xbox 360 Wireless Controller
+  {0x045e, 0x0719, InputDevice::DeviceClass::gamepad, QB_connect_if_nonzero},
+  // Jess Tech Colour Rumble Pad
+  {0x0f30, 0x0111, InputDevice::DeviceClass::gamepad, 0},
+  // 3Dconnexion Space Traveller 3D Mouse
+  {0x046d, 0xc623, InputDevice::DeviceClass::spatial_mouse, 0},
+  // 3Dconnexion Space Pilot 3D Mouse
+  {0x046d, 0xc625, InputDevice::DeviceClass::spatial_mouse, 0},
+  // 3Dconnexion Space Navigator 3D Mouse
+  {0x046d, 0xc626, InputDevice::DeviceClass::spatial_mouse, 0},
+  // 3Dconnexion Space Explorer 3D Mouse
+  {0x046d, 0xc627, InputDevice::DeviceClass::spatial_mouse, 0},
+  // 3Dconnexion Space Navigator for Notebooks
+  {0x046d, 0xc628, InputDevice::DeviceClass::spatial_mouse, 0},
+  // 3Dconnexion SpacePilot Pro 3D Mouse
+  {0x046d, 0xc629, InputDevice::DeviceClass::spatial_mouse, 0},
+  // 3Dconnexion Space Mouse Pro
+  {0x046d, 0xc62b, InputDevice::DeviceClass::spatial_mouse, 0},
+  {0},
+};
+
+TypeHandle EvdevInputDevice::_type_handle;
+
+/**
+ * Creates a new device representing the evdev device with the given index.
+ */
+EvdevInputDevice::
+EvdevInputDevice(LinuxInputDeviceManager *manager, size_t index) :
+  _manager(manager),
+  _index(index),
+  _fd(-1),
+  _can_write(false),
+  _ff_id(-1),
+  _ff_playing(false),
+  _ff_strong(-1),
+  _ff_weak(-1),
+  _dpad_x_axis(-1),
+  _dpad_y_axis(-1),
+  _dpad_left_button(-1),
+  _dpad_up_button(-1),
+  _ltrigger_code(-1),
+  _rtrigger_code(-1) {
+
+  char path[64];
+  sprintf(path, "/dev/input/event%zd", index);
+
+  _fd = open(path, O_RDWR | O_NONBLOCK);
+  if (_fd >= 0) {
+    _can_write = true;
+  } else {
+    // On failure, open device as read-only.
+    _fd = open(path, O_RDONLY | O_NONBLOCK);
+  }
+
+  if (_fd >= 0) {
+    init_device();
+  } else {
+    _is_connected = false;
+    device_cat.error()
+      << "Opening raw input device: " << strerror(errno) << " " << path << "\n";
+  }
+}
+
+/**
+ *
+ */
+EvdevInputDevice::
+~EvdevInputDevice() {
+  if (_fd != -1) {
+    if (_ff_id != -1) {
+      // Remove force-feedback effect.
+      do_set_vibration(0, 0);
+      ioctl(_fd, EVIOCRMFF, _ff_id);
+      _ff_id = -1;
+    }
+
+    close(_fd);
+    _fd = -1;
+  }
+}
+
+/**
+ * Sets the vibration strength.  The first argument controls a low-frequency
+ * motor, if present, and the latter controls a high-frequency motor.
+ * The values are within the 0-1 range.
+ */
+void EvdevInputDevice::
+do_set_vibration(double strong, double weak) {
+  if (_fd == -1 || !_can_write) {
+    return;
+  }
+
+  int strong_level = strong * 0xffff;
+  int weak_level = weak * 0xffff;
+
+  if (strong_level == _ff_strong && weak_level == _ff_weak) {
+    // No change.
+    return;
+  }
+
+  // Upload the new effect parameters.  Do this even if we are about
+  // to stop the effect, because some drivers don't respond to simply
+  // stopping the effect.
+  struct ff_effect effect;
+  effect.type = FF_RUMBLE;
+  effect.id = _ff_id;
+  effect.direction = 0;
+  effect.trigger.button = 0;
+  effect.trigger.interval = 0;
+  effect.replay.length = 0;
+  effect.replay.delay = 0;
+  effect.u.rumble.strong_magnitude = strong_level;
+  effect.u.rumble.weak_magnitude = weak_level;
+
+  if (ioctl(_fd, EVIOCSFF, &effect) < 0) {
+    return;
+  } else {
+    _ff_id = effect.id;
+    _ff_strong = strong_level;
+    _ff_weak = weak_level;
+  }
+
+  if (!_ff_playing) {
+    // Start the effect.  We could pass 0 as value to stop the effect
+    // when a level of 0 is requested, but my driver seems to ignore it.
+    _ff_playing = true;
+
+    struct input_event play;
+    play.type = EV_FF;
+    play.code = _ff_id;
+    play.value = 1;
+
+    if (write(_fd, &play, sizeof(play)) < 0) {
+      device_cat.warning()
+        << "Failed to write force-feedback event: " << strerror(errno) << "\n";
+    }
+  }
+}
+
+/**
+ * Polls the input device for new activity, to ensure it contains the latest
+ * events.  This will only have any effect for some types of input devices;
+ * others may be updated automatically, and this method will be a no-op.
+ */
+void EvdevInputDevice::
+do_poll() {
+  if (_fd != -1 && process_events()) {
+    while (process_events()) {}
+
+    // If we got events, we are obviously connected.  Mark us so.
+    if (!_is_connected) {
+      _is_connected = true;
+      if (_manager != nullptr) {
+        _manager->add_device(this);
+      }
+    }
+  }
+}
+
+/**
+ * Reads basic properties from the device.
+ */
+bool EvdevInputDevice::
+init_device() {
+  using std::string;
+
+  nassertr(_fd >= 0, false);
+
+  LightMutexHolder holder(_lock);
+
+  uint8_t evtypes[(EV_MAX + 8) >> 3] = {0};
+  char name[128];
+  if (ioctl(_fd, EVIOCGNAME(sizeof(name)), name) < 0 ||
+      ioctl(_fd, EVIOCGBIT(0, sizeof(evtypes)), evtypes) < 0) {
+    close(_fd);
+    _fd = -1;
+    _is_connected = false;
+    device_cat.error() << "Opening raw input device: ioctl failed\n";
+    return false;
+  }
+
+  _name.assign(name);
+  //cerr << "##### Now initializing device " << name << "\n";
+
+  struct input_id id;
+  if (ioctl(_fd, EVIOCGID, &id) >= 0) {
+    _vendor_id = id.vendor;
+    _product_id = id.product;
+  }
+
+  bool all_values_zero = true;
+  bool emulate_dpad = true;
+  bool have_analog_triggers = false;
+
+  bool has_keys = false;
+  bool has_axes = false;
+
+  uint8_t keys[(KEY_MAX + 8) >> 3] = {0};
+  if (test_bit(EV_KEY, evtypes)) {
+    // Check which buttons are on the device.
+    ioctl(_fd, EVIOCGBIT(EV_KEY, sizeof(keys)), keys);
+    has_keys = true;
+
+    if (test_bit(KEY_A, keys) && test_bit(KEY_Z, keys)) {
+      enable_feature(Feature::keyboard);
+    }
+  }
+
+  int num_bits = 0;
+  uint8_t axes[(ABS_MAX + 8) >> 3] = {0};
+  if (test_bit(EV_ABS, evtypes)) {
+    // Check which axes are on the device.
+    num_bits = ioctl(_fd, EVIOCGBIT(EV_ABS, sizeof(axes)), axes) << 3;
+    has_axes = true;
+  }
+
+  // Do we have a preset device mapping?
+  int quirks = 0;
+  const DeviceMapping *mapping = mapping_presets;
+  while (mapping->vendor != 0) {
+    if (_vendor_id == mapping->vendor && _product_id == mapping->product) {
+      _device_class = mapping->device_class;
+      quirks = mapping->quirks;
+      break;
+    }
+    ++mapping;
+  }
+
+  // Try to detect which type of device we have here
+  if (_device_class == DeviceClass::unknown) {
+    int device_scores[(size_t)DeviceClass::spatial_mouse] = {0};
+
+    // Test for specific keys
+    if (test_bit(BTN_GAMEPAD, keys) && test_bit(ABS_X, axes) && test_bit(ABS_RX, axes)) {
+      device_scores[(size_t)DeviceClass::gamepad] += 5;
+      device_scores[(size_t)DeviceClass::steering_wheel] += 5;
+      device_scores[(size_t)DeviceClass::flight_stick] += 5;
+    }
+
+    if (test_bit(ABS_WHEEL, axes) && test_bit(ABS_GAS, axes) && test_bit(ABS_BRAKE, axes)) {
+      device_scores[(size_t)DeviceClass::steering_wheel] += 10;
+    }
+    if (test_bit(BTN_GEAR_DOWN, keys) && test_bit(BTN_GEAR_UP, keys)) {
+      device_scores[(size_t)DeviceClass::steering_wheel] += 10;
+    }
+    if (test_bit(BTN_JOYSTICK, keys) && test_bit(ABS_X, axes)) {
+      device_scores[(size_t)DeviceClass::flight_stick] += 10;
+    }
+    if (test_bit(BTN_MOUSE, keys) && test_bit(EV_REL, evtypes)) {
+      device_scores[(size_t)DeviceClass::mouse] += 20;
+    }
+    uint8_t unknown_keys[] = {KEY_POWER};
+    for (int i = 0; i < 1; i++) {
+      if (test_bit(unknown_keys[i], keys)) {
+        if (unknown_keys[i] == KEY_POWER) {
+        }
+        device_scores[(size_t)DeviceClass::unknown] += 20;
+      }
+    }
+    if (_features & (unsigned int)Feature::keyboard) {
+      device_scores[(size_t)DeviceClass::keyboard] += 20;
+    }
+
+    // Test for specific name tags
+    string lowercase_name = _name;
+    for(size_t x = 0; x < _name.length(); ++x) {
+      lowercase_name[x] = tolower(lowercase_name[x]);
+    }
+    if (lowercase_name.find("gamepad") != string::npos) {
+      device_scores[(size_t)DeviceClass::gamepad] += 10;
+    }
+    if (lowercase_name.find("wheel") != string::npos) {
+      device_scores[(size_t)DeviceClass::steering_wheel] += 10;
+    }
+    if (lowercase_name.find("mouse") != string::npos || lowercase_name.find("touchpad") != string::npos) {
+      device_scores[(size_t)DeviceClass::mouse] += 10;
+    }
+    if (lowercase_name.find("keyboard") != string::npos) {
+      device_scores[(size_t)DeviceClass::keyboard] += 10;
+    }
+    // List of lowercase names that occur in unknown devices
+    string unknown_names[] = {"video bus", "power button", "sleep button"};
+    for(int i = 0; i < 3; i++) {
+      if (lowercase_name.find(unknown_names[i]) != string::npos) {
+        device_scores[(size_t)DeviceClass::unknown] += 20;
+      }
+    }
+
+    // Check which device type got the most points
+    int highest_score = 0;
+    for (size_t i = 0; i < (size_t)DeviceClass::spatial_mouse; i++) {
+      if (device_scores[i] > highest_score) {
+        highest_score = device_scores[i];
+        _device_class = (DeviceClass)i;
+      }
+    }
+    //std::cerr << "Found highscore class " << _device_class << " with this score: " << highest_score << "\n";
+  }
+
+  if (has_keys) {
+    // Also check whether the buttons are currently pressed.
+    uint8_t states[(KEY_MAX + 8) >> 3] = {0};
+    ioctl(_fd, EVIOCGKEY(sizeof(states)), states);
+
+    for (int i = 0; i <= KEY_MAX; ++i) {
+      if (test_bit(i, keys)) {
+        ButtonState button;
+        button.handle = map_button(i, _device_class);
+
+        int button_index = (int)_buttons.size();
+        if (button.handle == ButtonHandle::none()) {
+          if (device_cat.is_debug()) {
+            device_cat.debug() << "Unmapped /dev/input/event" << _index
+              << " button " << button_index << ": 0x" << std::hex << i << std::dec << "\n";
+          }
+        }
+
+        if (test_bit(i, states)) {
+          button._state = S_down;
+          all_values_zero = false;
+        } else {
+          button._state = S_up;
+        }
+        if (button.handle == GamepadButton::dpad_left()) {
+          emulate_dpad = false;
+        } else if (button.handle == GamepadButton::ltrigger()) {
+          _ltrigger_code = i;
+        } else if (button.handle == GamepadButton::rtrigger()) {
+          _rtrigger_code = i;
+        }
+
+        _buttons.push_back(button);
+        if ((size_t)i >= _button_indices.size()) {
+          _button_indices.resize(i + 1, -1);
+        }
+        _button_indices[i] = button_index;
+      }
+    }
+  }
+
+  if (has_axes) {
+    _axis_indices.resize(num_bits, -1);
+
+    for (int i = 0; i < num_bits; ++i) {
+      if (test_bit(i, axes)) {
+        Axis axis = Axis::none;
+        switch (i) {
+        case ABS_X:
+          if (_device_class == DeviceClass::gamepad) {
+            axis = InputDevice::Axis::left_x;
+          } else if (_device_class == DeviceClass::flight_stick) {
+            axis = InputDevice::Axis::roll;
+          } else {
+            axis = InputDevice::Axis::x;
+          }
+          break;
+        case ABS_Y:
+          if (_device_class == DeviceClass::gamepad) {
+            axis = InputDevice::Axis::left_y;
+          } else if (_device_class == DeviceClass::flight_stick) {
+            axis = InputDevice::Axis::pitch;
+          } else {
+            axis = InputDevice::Axis::y;
+          }
+          break;
+        case ABS_Z:
+          if (quirks & QB_rstick_from_z) {
+            axis = InputDevice::Axis::right_x;
+          } else if (_device_class == DeviceClass::gamepad) {
+            axis = InputDevice::Axis::left_trigger;
+            have_analog_triggers = true;
+          } else if (_device_class == DeviceClass::spatial_mouse) {
+            axis = InputDevice::Axis::z;
+          } else {
+            axis = InputDevice::Axis::throttle;
+          }
+          break;
+        case ABS_RX:
+          if (_device_class == DeviceClass::spatial_mouse) {
+            axis = InputDevice::Axis::pitch;
+          } else if ((quirks & QB_rstick_from_z) == 0) {
+            axis = InputDevice::Axis::right_x;
+          }
+          break;
+        case ABS_RY:
+          if (_device_class == DeviceClass::spatial_mouse) {
+            axis = InputDevice::Axis::roll;
+          } else if ((quirks & QB_rstick_from_z) == 0) {
+            axis = InputDevice::Axis::right_y;
+          }
+          break;
+        case ABS_RZ:
+          if (quirks & QB_rstick_from_z) {
+            axis = InputDevice::Axis::right_y;
+          } else if (_device_class == DeviceClass::gamepad) {
+            axis = InputDevice::Axis::right_trigger;
+            have_analog_triggers = true;
+          } else {
+            axis = InputDevice::Axis::yaw;
+          }
+          break;
+        case ABS_THROTTLE:
+          if (quirks & QB_rudder_from_throttle) {
+            axis = InputDevice::Axis::rudder;
+          } else {
+            axis = InputDevice::Axis::throttle;
+          }
+          break;
+        case ABS_RUDDER:
+          axis = InputDevice::Axis::rudder;
+          break;
+        case ABS_WHEEL:
+          axis = InputDevice::Axis::wheel;
+          break;
+        case ABS_GAS:
+          if (_device_class == DeviceClass::gamepad) {
+            axis = InputDevice::Axis::right_trigger;
+            have_analog_triggers = true;
+          } else {
+            axis = InputDevice::Axis::accelerator;
+          }
+          break;
+        case ABS_BRAKE:
+          if (_device_class == DeviceClass::gamepad) {
+            axis = InputDevice::Axis::left_trigger;
+            have_analog_triggers = true;
+          } else {
+            axis = InputDevice::Axis::brake;
+          }
+          break;
+        case ABS_HAT0X:
+          if (emulate_dpad) {
+            _dpad_x_axis = i;
+            _dpad_left_button = (int)_buttons.size();
+            if (_device_class == DeviceClass::gamepad) {
+              _buttons.push_back(ButtonState(GamepadButton::dpad_left()));
+              _buttons.push_back(ButtonState(GamepadButton::dpad_right()));
+            } else {
+              _buttons.push_back(ButtonState(GamepadButton::hat_left()));
+              _buttons.push_back(ButtonState(GamepadButton::hat_right()));
+            }
+          }
+          break;
+        case ABS_HAT0Y:
+          if (emulate_dpad) {
+            _dpad_y_axis = i;
+            _dpad_up_button = (int)_buttons.size();
+            if (_device_class == DeviceClass::gamepad) {
+              _buttons.push_back(ButtonState(GamepadButton::dpad_up()));
+              _buttons.push_back(ButtonState(GamepadButton::dpad_down()));
+            } else {
+              _buttons.push_back(ButtonState(GamepadButton::hat_up()));
+              _buttons.push_back(ButtonState(GamepadButton::hat_down()));
+            }
+          }
+          break;
+        }
+
+        // Check the initial value and ranges.
+        struct input_absinfo absinfo;
+        if (ioctl(_fd, EVIOCGABS(i), &absinfo) >= 0) {
+          int index;
+          // We'd like to reverse the Y axis to match the XInput behavior.
+          // Also reverse the yaw axis to match right-hand coordinate system.
+          // Also T.Flight Hotas X throttle is reversed and can go backwards.
+          if (axis == Axis::yaw || axis == Axis::rudder || axis == Axis::left_y || axis == Axis::right_y ||
+              (axis == Axis::throttle && (quirks & QB_reversed_throttle) != 0) ||
+              (_device_class == DeviceClass::spatial_mouse && (axis == Axis::y || axis == Axis::z || axis == Axis::roll))) {
+            std::swap(absinfo.maximum, absinfo.minimum);
+          }
+          if (axis == Axis::throttle && (quirks & QB_centered_throttle) != 0) {
+            index = add_axis(axis, absinfo.minimum, absinfo.maximum, true);
+          } else {
+            index = add_axis(axis, absinfo.minimum, absinfo.maximum);
+          }
+          axis_changed(index, absinfo.value);
+          _axis_indices[i] = index;
+
+          if (absinfo.value != 0) {
+            all_values_zero = false;
+          }
+        }
+      }
+    }
+  }
+
+  if (test_bit(EV_REL, evtypes)) {
+    enable_feature(Feature::pointer);
+    add_pointer(PointerType::unknown, 0);
+  }
+
+  if (test_bit(EV_FF, evtypes)) {
+    uint8_t effects[(FF_MAX + 8) >> 3] = {0};
+    ioctl(_fd, EVIOCGBIT(EV_FF, sizeof(effects)), effects);
+
+    if (test_bit(FF_RUMBLE, effects)) {
+      if (_can_write) {
+        enable_feature(Feature::vibration);
+      } else {
+        // Let the user know what he's missing out on.
+        device_cat.warning()
+          << "/dev/input/event" << _index << " is not writable, vibration "
+          << "effects will be unavailable.\n";
+      }
+    }
+  }
+
+  if (_ltrigger_code >= 0 && _rtrigger_code >= 0 && !have_analog_triggers) {
+    // Emulate analog triggers.
+    _ltrigger_axis = (int)_axes.size();
+    add_axis(Axis::left_trigger, 0, 1, false);
+    add_axis(Axis::right_trigger, 0, 1, false);
+  } else {
+    _ltrigger_code = -1;
+    _rtrigger_code = -1;
+  }
+
+  char path[64];
+  char buffer[256];
+  const char *parent = "";
+  sprintf(path, "/sys/class/input/event%zd/device/device/../product", _index);
+  FILE *f = fopen(path, "r");
+  if (!f) {
+    parent = "../";
+    sprintf(path, "/sys/class/input/event%zd/device/device/%s../product", _index, parent);
+    f = fopen(path, "r");
+  }
+  if (f) {
+    if (fgets(buffer, sizeof(buffer), f) != nullptr) {
+      buffer[strcspn(buffer, "\r\n")] = 0;
+      if (buffer[0] != 0) {
+        _name.assign(buffer);
+      }
+    }
+    fclose(f);
+  }
+  sprintf(path, "/sys/class/input/event%zd/device/device/%s../manufacturer", _index, parent);
+  f = fopen(path, "r");
+  if (f) {
+    if (fgets(buffer, sizeof(buffer), f) != nullptr) {
+      buffer[strcspn(buffer, "\r\n")] = 0;
+      _manufacturer.assign(buffer);
+    }
+    fclose(f);
+  }
+  sprintf(path, "/sys/class/input/event%zd/device/device/%s../serial", _index, parent);
+  f = fopen(path, "r");
+  if (f) {
+    if (fgets(buffer, sizeof(buffer), f) != nullptr) {
+      buffer[strcspn(buffer, "\r\n")] = 0;
+      _serial_number.assign(buffer);
+    }
+    fclose(f);
+  }
+
+  // Special-case fix for Xbox 360 Wireless Receiver: the Linux kernel
+  // driver always reports 4 connected gamepads, regardless of the number
+  // of gamepads actually present.  This hack partially remedies this.
+  if (all_values_zero && (quirks & QB_connect_if_nonzero) != 0) {
+    _is_connected = false;
+  } else {
+    _is_connected = true;
+  }
+  return true;
+}
+
+/**
+ * Reads a number of events from the device.  Returns true if events were read,
+ * meaning this function should keep being called until it returns false.
+ */
+bool EvdevInputDevice::
+process_events() {
+  // Read 8 events at a time.
+  struct input_event events[8];
+
+  int n_read = read(_fd, events, sizeof(events));
+  if (n_read < 0) {
+    if (errno == EAGAIN || errno == EWOULDBLOCK) {
+      // No data available for now.
+
+    } else if (errno == ENODEV || errno == EINVAL) {
+      // The device ceased to exist, so we better close it.  No need
+      // to worry about removing it from the InputDeviceManager, as it
+      // will get an inotify event sooner or later about this.
+      close(_fd);
+      _fd = -1;
+      //_is_connected = false;
+      errno = 0;
+
+    } else {
+      device_cat.error() << "read: " << strerror(errno) << "\n";
+    }
+    return false;
+  }
+
+  if (n_read == 0) {
+    return false;
+  }
+
+  n_read /= sizeof(struct input_event);
+
+  int rel_x = 0;
+  int rel_y = 0;
+  double time = ClockObject::get_global_clock()->get_frame_time();
+  int index;
+
+  // It seems that some devices send a single EV_SYN event when being
+  // unplugged.  Boo.  Ignore it.
+  if (n_read == 1 && events[0].code == EV_SYN) {
+    return false;
+  }
+
+  for (int i = 0; i < n_read; ++i) {
+    int code = events[i].code;
+
+    switch (events[i].type) {
+    case EV_SYN:
+      break;
+
+    case EV_REL:
+      if (code == REL_X) rel_x += events[i].value;
+      if (code == REL_Y) rel_y += events[i].value;
+      break;
+
+    case EV_ABS:
+      if (code == _dpad_x_axis) {
+        button_changed(_dpad_left_button, events[i].value < 0);
+        button_changed(_dpad_left_button+1, events[i].value > 0);
+      } else if (code == _dpad_y_axis) {
+        button_changed(_dpad_up_button, events[i].value < 0);
+        button_changed(_dpad_up_button+1, events[i].value > 0);
+      }
+      nassertd(code >= 0 && (size_t)code < _axis_indices.size()) break;
+      index = _axis_indices[code];
+      if (index >= 0) {
+        axis_changed(index, events[i].value);
+      }
+      break;
+
+    case EV_KEY:
+      nassertd(code >= 0 && (size_t)code < _button_indices.size()) break;
+      index = _button_indices[code];
+      if (index >= 0) {
+        button_changed(index, events[i].value != 0);
+      }
+      if (code == _ltrigger_code) {
+        axis_changed(_ltrigger_axis, events[i].value);
+      } else if (code == _rtrigger_code) {
+        axis_changed(_ltrigger_axis + 1, events[i].value);
+      }
+      break;
+
+    default:
+      //cerr << "event " << events[i].type << " - " << events[i].code << " - " << events[i].value << "\n";
+      break;
+    }
+  }
+
+  if (rel_x != 0 || rel_y != 0) {
+    pointer_moved(0, rel_x, rel_y, time);
+  }
+
+  return true;
+}
+
+/**
+ * Static function to map an evdev code to a ButtonHandle.
+ */
+ButtonHandle EvdevInputDevice::
+map_button(int code, DeviceClass device_class) {
+  if (code >= 0 && code < 0x80) {
+    // See linux/input.h for the source of this mapping.
+    static const ButtonHandle keyboard_map[] = {
+      ButtonHandle::none(),
+      KeyboardButton::escape(),
+      KeyboardButton::ascii_key('1'),
+      KeyboardButton::ascii_key('2'),
+      KeyboardButton::ascii_key('3'),
+      KeyboardButton::ascii_key('4'),
+      KeyboardButton::ascii_key('5'),
+      KeyboardButton::ascii_key('6'),
+      KeyboardButton::ascii_key('7'),
+      KeyboardButton::ascii_key('8'),
+      KeyboardButton::ascii_key('9'),
+      KeyboardButton::ascii_key('0'),
+      KeyboardButton::ascii_key('-'),
+      KeyboardButton::ascii_key('='),
+      KeyboardButton::backspace(),
+      KeyboardButton::tab(),
+      KeyboardButton::ascii_key('q'),
+      KeyboardButton::ascii_key('w'),
+      KeyboardButton::ascii_key('e'),
+      KeyboardButton::ascii_key('r'),
+      KeyboardButton::ascii_key('t'),
+      KeyboardButton::ascii_key('y'),
+      KeyboardButton::ascii_key('u'),
+      KeyboardButton::ascii_key('i'),
+      KeyboardButton::ascii_key('o'),
+      KeyboardButton::ascii_key('p'),
+      KeyboardButton::ascii_key('['),
+      KeyboardButton::ascii_key(']'),
+      KeyboardButton::enter(),
+      KeyboardButton::lcontrol(),
+      KeyboardButton::ascii_key('a'),
+      KeyboardButton::ascii_key('s'),
+      KeyboardButton::ascii_key('d'),
+      KeyboardButton::ascii_key('f'),
+      KeyboardButton::ascii_key('g'),
+      KeyboardButton::ascii_key('h'),
+      KeyboardButton::ascii_key('j'),
+      KeyboardButton::ascii_key('k'),
+      KeyboardButton::ascii_key('l'),
+      KeyboardButton::ascii_key(';'),
+      KeyboardButton::ascii_key('\''),
+      KeyboardButton::ascii_key('`'),
+      KeyboardButton::lshift(),
+      KeyboardButton::ascii_key('\\'),
+      KeyboardButton::ascii_key('z'),
+      KeyboardButton::ascii_key('x'),
+      KeyboardButton::ascii_key('c'),
+      KeyboardButton::ascii_key('v'),
+      KeyboardButton::ascii_key('b'),
+      KeyboardButton::ascii_key('n'),
+      KeyboardButton::ascii_key('m'),
+      KeyboardButton::ascii_key(','),
+      KeyboardButton::ascii_key('.'),
+      KeyboardButton::ascii_key('/'),
+      KeyboardButton::rshift(),
+      KeyboardButton::ascii_key('*'),
+      KeyboardButton::lalt(),
+      KeyboardButton::space(),
+      KeyboardButton::caps_lock(),
+      KeyboardButton::f1(),
+      KeyboardButton::f2(),
+      KeyboardButton::f3(),
+      KeyboardButton::f4(),
+      KeyboardButton::f5(),
+      KeyboardButton::f6(),
+      KeyboardButton::f7(),
+      KeyboardButton::f8(),
+      KeyboardButton::f9(),
+      KeyboardButton::f10(),
+      KeyboardButton::num_lock(),
+      KeyboardButton::scroll_lock(),
+      KeyboardButton::ascii_key('7'),
+      KeyboardButton::ascii_key('8'),
+      KeyboardButton::ascii_key('9'),
+      KeyboardButton::ascii_key('-'),
+      KeyboardButton::ascii_key('4'),
+      KeyboardButton::ascii_key('5'),
+      KeyboardButton::ascii_key('6'),
+      KeyboardButton::ascii_key('+'),
+      KeyboardButton::ascii_key('1'),
+      KeyboardButton::ascii_key('2'),
+      KeyboardButton::ascii_key('3'),
+      KeyboardButton::ascii_key('0'),
+      KeyboardButton::ascii_key('.'),
+      ButtonHandle::none(),
+      ButtonHandle::none(),
+      ButtonHandle::none(),
+      KeyboardButton::f11(),
+      KeyboardButton::f12(),
+      ButtonHandle::none(),
+      ButtonHandle::none(),
+      ButtonHandle::none(),
+      ButtonHandle::none(),
+      ButtonHandle::none(),
+      ButtonHandle::none(),
+      ButtonHandle::none(),
+      KeyboardButton::enter(),
+      KeyboardButton::rcontrol(),
+      KeyboardButton::ascii_key('/'),
+      KeyboardButton::print_screen(),
+      KeyboardButton::ralt(),
+      ButtonHandle::none(),
+      KeyboardButton::home(),
+      KeyboardButton::up(),
+      KeyboardButton::page_up(),
+      KeyboardButton::left(),
+      KeyboardButton::right(),
+      KeyboardButton::end(),
+      KeyboardButton::down(),
+      KeyboardButton::page_down(),
+      KeyboardButton::insert(),
+      KeyboardButton::del(),
+      ButtonHandle::none(),
+      ButtonHandle::none(),
+      ButtonHandle::none(),
+      ButtonHandle::none(),
+      ButtonHandle::none(),
+      ButtonHandle::none(),
+      ButtonHandle::none(),
+      KeyboardButton::pause(),
+      ButtonHandle::none(),
+      ButtonHandle::none(),
+      ButtonHandle::none(),
+      ButtonHandle::none(),
+      ButtonHandle::none(),
+      KeyboardButton::lmeta(),
+      KeyboardButton::rmeta(),
+      KeyboardButton::menu(),
+    };
+    return keyboard_map[code];
+
+  } else if (code == KEY_BACK) {
+    // Used by NVIDIA Shield Controller
+    return GamepadButton::back();
+
+  } else if (code == KEY_SEARCH) {
+    // Used by NVIDIA Shield Controller
+    return GamepadButton::guide();
+
+  } else if (code < 0x100) {
+    return ButtonHandle::none();
+
+  } else if ((code & 0xfff0) == BTN_MOUSE) {
+    // The number for these is reversed in Panda.
+    if (code == BTN_RIGHT) {
+      return MouseButton::three();
+    } else if (code == BTN_MIDDLE) {
+      return MouseButton::two();
+    } else {
+      return MouseButton::button(code - BTN_MOUSE);
+    }
+
+  } else if ((code & 0xfff0) == BTN_JOYSTICK) {
+    if (device_class == DeviceClass::gamepad) {
+      // Based on "Jess Tech Colour Rumble Pad"
+      static const ButtonHandle mapping[] = {
+        GamepadButton::face_x(),
+        GamepadButton::face_y(),
+        GamepadButton::face_a(),
+        GamepadButton::face_b(),
+        GamepadButton::lshoulder(),
+        GamepadButton::ltrigger(),
+        GamepadButton::rshoulder(),
+        GamepadButton::rtrigger(),
+        GamepadButton::back(),
+        GamepadButton::start(),
+        GamepadButton::lstick(),
+        GamepadButton::rstick(),
+      };
+      if ((code & 0xf) < 12) {
+        return mapping[code & 0xf];
+      }
+    } else {
+      return GamepadButton::joystick(code & 0xf);
+    }
+  }
+
+  switch (code) {
+  case BTN_A:
+    return GamepadButton::face_a();
+
+  case BTN_B:
+    return GamepadButton::face_b();
+
+  case BTN_C:
+    return GamepadButton::face_c();
+
+  case BTN_X:
+    return GamepadButton::face_x();
+
+  case BTN_Y:
+    return GamepadButton::face_y();
+
+  case BTN_Z:
+    return GamepadButton::face_z();
+
+  case BTN_TL:
+    return GamepadButton::lshoulder();
+
+  case BTN_TR:
+    return GamepadButton::rshoulder();
+
+  case BTN_TL2:
+    return GamepadButton::ltrigger();
+
+  case BTN_TR2:
+    return GamepadButton::rtrigger();
+
+  case BTN_1:
+    return GamepadButton::face_1();
+
+  case BTN_2:
+    return GamepadButton::face_2();
+
+  case BTN_SELECT:
+  case KEY_PREVIOUS:
+    return GamepadButton::back();
+
+  case BTN_START:
+  case KEY_NEXT:
+    return GamepadButton::start();
+
+  case BTN_MODE:
+    return GamepadButton::guide();
+
+  case BTN_THUMBL:
+    return GamepadButton::lstick();
+
+  case BTN_THUMBR:
+    return GamepadButton::rstick();
+
+  case BTN_DPAD_LEFT:
+  case BTN_TRIGGER_HAPPY1:
+    return GamepadButton::dpad_left();
+
+  case BTN_DPAD_RIGHT:
+  case BTN_TRIGGER_HAPPY2:
+    return GamepadButton::dpad_right();
+
+  case BTN_DPAD_UP:
+  case BTN_TRIGGER_HAPPY3:
+    return GamepadButton::dpad_up();
+
+  case BTN_DPAD_DOWN:
+  case BTN_TRIGGER_HAPPY4:
+    return GamepadButton::dpad_down();
+
+  default:
+    return ButtonHandle::none();
+  }
+}
+
+#endif

+ 87 - 0
panda/src/device/evdevInputDevice.h

@@ -0,0 +1,87 @@
+/**
+ * PANDA 3D SOFTWARE
+ * Copyright (c) Carnegie Mellon University.  All rights reserved.
+ *
+ * All use of this software is subject to the terms of the revised BSD
+ * license.  You should have received a copy of this license along
+ * with this source code in a file named "LICENSE."
+ *
+ * @file evdevInputDevice.h
+ * @author rdb
+ * @date 2015-08-24
+ */
+
+#ifndef EVDEVINPUTDEVICE_H
+#define EVDEVINPUTDEVICE_H
+
+#include "inputDevice.h"
+
+#ifdef PHAVE_LINUX_INPUT_H
+
+class LinuxInputDeviceManager;
+
+/**
+ * This is a type of device that uses the Linux /dev/input/event# API to read
+ * data from a raw mouse or other input device.  Unlike the joystick API, the
+ * evdev API supports sending force feedback (ie. rumble) events.
+ */
+class EXPCL_PANDA_DEVICE EvdevInputDevice : public InputDevice {
+public:
+  EvdevInputDevice(LinuxInputDeviceManager *manager, size_t index);
+  virtual ~EvdevInputDevice();
+
+private:
+  virtual void do_set_vibration(double strong, double weak);
+  virtual void do_poll();
+
+  bool init_device();
+  bool process_events();
+
+private:
+  LinuxInputDeviceManager *_manager;
+
+  int _fd;
+  size_t _index;
+
+  bool _can_write;
+  int _ff_id;
+  bool _ff_playing;
+  int _ff_strong;
+  int _ff_weak;
+
+  pvector<int> _axis_indices;
+  pvector<int> _button_indices;
+
+  // These are used for D-pad emulation.
+  int _dpad_x_axis;
+  int _dpad_y_axis;
+  int _dpad_left_button;
+  int _dpad_up_button;
+
+  // This is used for axis emulation.
+  int _ltrigger_axis;
+  int _ltrigger_code;
+  int _rtrigger_code;
+
+public:
+  static ButtonHandle map_button(int code, DeviceClass device_class = DeviceClass::unknown);
+
+public:
+  static TypeHandle get_class_type() {
+    return _type_handle;
+  }
+  static void init_type() {
+    InputDevice::init_type();
+    register_type(_type_handle, "EvdevInputDevice",
+                  InputDevice::get_class_type());
+  }
+
+private:
+  static TypeHandle _type_handle;
+};
+
+#include "evdevInputDevice.I"
+
+#endif  // PHAVE_LINUX_INPUT_H
+
+#endif  // EVDEVINPUTDEVICE_H

+ 410 - 0
panda/src/device/inputDevice.I

@@ -0,0 +1,410 @@
+/**
+ * PANDA 3D SOFTWARE
+ * Copyright (c) Carnegie Mellon University.  All rights reserved.
+ *
+ * All use of this software is subject to the terms of the revised BSD
+ * license.  You should have received a copy of this license along
+ * with this source code in a file named "LICENSE."
+ *
+ * @file inputDevice.I
+ * @author rdb
+ * @date 2015-12-11
+ */
+
+#include "config_device.h"
+
+/**
+ *
+ */
+INLINE InputDevice::
+InputDevice() {
+  _button_events = new ButtonEventList;
+  _pointer_events = new PointerEventList;
+}
+
+/**
+ * Returns a human-readable name for the device.  Not necessarily unique.
+ */
+INLINE std::string InputDevice::
+get_name() const {
+  LightMutexHolder holder(_lock);
+  return _name;
+}
+
+/**
+ * Returns a string containing the manufacturer of the device, if this
+ * information is known.
+ */
+INLINE std::string InputDevice::
+get_manufacturer() const {
+  LightMutexHolder holder(_lock);
+  return _manufacturer;
+}
+
+/**
+ * Returns a string containing the serial number of the device, if this
+ * information is known.
+ */
+INLINE std::string InputDevice::
+get_serial_number() const {
+  LightMutexHolder holder(_lock);
+  return _serial_number;
+}
+
+/**
+ * Returns a string containing the USB vendor ID of the device, if this
+ * information is known.
+ */
+INLINE unsigned short InputDevice::
+get_vendor_id() const {
+  LightMutexHolder holder(_lock);
+  return _vendor_id;
+}
+
+/**
+ * Returns a string containing the USB product ID of the device, if this
+ * information is known.
+ */
+INLINE unsigned short InputDevice::
+get_product_id() const {
+  LightMutexHolder holder(_lock);
+  return _product_id;
+}
+
+/**
+ * Returns true if the device is still connected and able to receive data,
+ * false otherwise.  May return false positives.
+ */
+INLINE bool InputDevice::
+is_connected() const {
+  LightMutexHolder holder(_lock);
+  return _is_connected;
+}
+
+/**
+ * Returns an identification of the general type of device.  If this could not
+ * be determined, returns DeviceClass.unknown.
+ */
+INLINE InputDevice::DeviceClass InputDevice::
+get_device_class() const {
+  LightMutexHolder holder(_lock);
+  return _device_class;
+}
+
+/**
+ * Returns true if the device supports the indicated feature.
+ */
+INLINE bool InputDevice::
+has_feature(Feature feature) const {
+  LightMutexHolder holder(_lock);
+  return (_features & (1 << (unsigned int)feature)) != 0;
+}
+
+/**
+ * Returns true if this is a pointing device.
+ */
+INLINE bool InputDevice::
+has_pointer() const {
+  return has_feature(Feature::pointer);
+}
+
+/**
+ * Returns true if the device has a physical keyboard designed for text entry.
+ */
+INLINE bool InputDevice::
+has_keyboard() const {
+  return has_feature(Feature::keyboard);
+}
+
+/**
+ * Returns true if the device features a tracker that can track position and/or
+ * orientation in 3D space.
+ */
+INLINE bool InputDevice::
+has_tracker() const {
+  return has_feature(Feature::tracker);
+}
+
+/**
+ * Returns true if the device has vibration motors that can be controlled by
+ * calling set_vibration().
+ */
+INLINE bool InputDevice::
+has_vibration() const {
+  return has_feature(Feature::vibration);
+}
+
+/**
+ * Returns true if the device may be able to provide information about its
+ * battery life.
+ */
+INLINE bool InputDevice::
+has_battery() const {
+  return has_feature(Feature::battery);
+}
+
+/**
+ * Returns the TrackerData associated with the input device's tracker.  This
+ * only makes sense if has_tracker() also returns true.
+ */
+INLINE TrackerData InputDevice::
+get_tracker() const {
+  LightMutexHolder holder(_lock);
+  return _tracker_data;
+}
+
+/**
+ * Returns a rough indication of the battery level, ranging from 0 (completely
+ * empty battery) to the indicated max_level value.
+ */
+INLINE InputDevice::BatteryData InputDevice::
+get_battery() const {
+  LightMutexHolder holder(_lock);
+  return _battery_data;
+}
+
+/**
+ * Returns the number of buttons known to the device.  This includes those
+ * buttons whose state has been seen, as well as buttons that have been
+ * associated with a ButtonHandle even if their state is unknown.  This number
+ * may change as more buttons are discovered.
+ */
+INLINE size_t InputDevice::
+get_num_buttons() const {
+  LightMutexHolder holder(_lock);
+  return _buttons.size();
+}
+
+/**
+ * Associates the indicated ButtonHandle with the button of the indicated index
+ * number.  When the given button index changes state, a corresponding
+ * ButtonEvent will be generated with the given ButtonHandle.  Pass
+ * ButtonHandle::none() to turn off any association.
+ *
+ * It is not necessary to call this if you simply want to query the state of
+ * the various buttons by index number; this is only necessary in order to
+ * generate ButtonEvents when the buttons change state.
+ */
+INLINE void InputDevice::
+map_button(size_t index, ButtonHandle button) {
+  LightMutexHolder holder(_lock);
+  if (index >= _buttons.size()) {
+    _buttons.resize(index + 1, ButtonState());
+  }
+
+  _buttons[index].handle = button;
+}
+
+/**
+ * Returns the ButtonHandle that was previously associated with the given index
+ * number by a call to map_button(), or ButtonHandle::none() if no button
+ * was associated.
+ */
+INLINE ButtonHandle InputDevice::
+get_button_map(size_t index) const {
+  if (index < _buttons.size()) {
+    return _buttons[index].handle;
+  } else {
+    return ButtonHandle::none();
+  }
+}
+
+/**
+ * Returns true if the indicated button (identified by its index number) is
+ * currently known to be down, or false if it is up or unknown.
+ */
+INLINE bool InputDevice::
+is_button_pressed(size_t index) const {
+  if (index < _buttons.size()) {
+    return (_buttons[index]._state == S_down);
+  } else {
+    return false;
+  }
+}
+
+/**
+ * Returns true if the state of the indicated button is known, or false if we
+ * have never heard anything about this particular button.
+ */
+INLINE bool InputDevice::
+is_button_known(size_t index) const {
+  if (index < _buttons.size()) {
+    return _buttons[index]._state != S_unknown;
+  } else {
+    return false;
+  }
+}
+
+/**
+ * Returns the ButtonState that is set at the given index, or throw an assert
+ * if the index was not found in the list.
+ */
+INLINE InputDevice::ButtonState InputDevice::
+get_button(size_t index) const {
+  nassertr_always(index < _buttons.size(), ButtonState());
+  return _buttons[index];
+}
+
+/**
+ * Returns the first ButtonState found with the given axis, or throw an assert
+ * if the button handle was not found in the list.
+ */
+INLINE InputDevice::ButtonState InputDevice::
+find_button(ButtonHandle handle) const {
+  for (size_t i = 0; i < _buttons.size(); ++i) {
+    if (_buttons[i].handle == handle) {
+      return _buttons[i];
+    }
+  }
+  return ButtonState();
+}
+
+/**
+ * Returns the number of analog axes known to the InputDevice.  This number
+ * may change as more axes are discovered.
+ */
+INLINE size_t InputDevice::
+get_num_axes() const {
+  return _axes.size();
+}
+
+/**
+ * Associates the indicated Axis with the axis of the indicated index
+ * number.  Pass Axis::none to turn off any association.
+ *
+ * It is not necessary to call this if you simply want to query the state of
+ * the various axes by index number.
+ */
+INLINE void InputDevice::
+map_axis(size_t index, InputDevice::Axis axis) {
+  LightMutexHolder holder(_lock);
+  if (index >= _axes.size()) {
+    _axes.resize(index + 1, AxisState());
+  }
+
+  _axes[index].axis = axis;
+}
+
+/**
+ * Returns the current position of indicated analog axis (identified by its
+ * index number), or 0.0 if the axis is unknown.  The normal range of a
+ * single axis is -1.0 to 1.0.
+ */
+INLINE double InputDevice::
+get_axis_value(size_t index) const {
+  if (index < _axes.size()) {
+    return _axes[index].value;
+  } else {
+    return 0.0;
+  }
+}
+
+/**
+ * Returns the axis state that is set at the given index, or throw an assert
+ * if the index was not found in the list.
+ */
+INLINE InputDevice::AxisState InputDevice::
+get_axis(size_t index) const {
+  nassertr_always(index < _axes.size(), AxisState());
+  return _axes[index];
+}
+
+/**
+ * Returns the first AnalogAxis found with the given axis, or throw an assert
+ * if the axis was not found in the list.
+ */
+INLINE InputDevice::AxisState InputDevice::
+find_axis(InputDevice::Axis axis) const {
+  for (size_t i = 0; i < _axes.size(); ++i) {
+    if (_axes[i].axis == axis) {
+      return _axes[i];
+    }
+  }
+  return AxisState();
+}
+
+/**
+ * Returns true if the state of the indicated analog axis is known, or false
+ * if we have never heard anything about this particular axis.
+ */
+INLINE bool InputDevice::
+is_axis_known(size_t index) const {
+  if (index < _axes.size()) {
+    return _axes[index].known;
+  } else {
+    return false;
+  }
+}
+
+/**
+ * Sets the strength of the vibration effect, if supported.  The values are
+ * clamped to 0-1 range. The first value axes the low-frequency rumble
+ * motor, whereas the second axes the high-frequency motor, if present.
+ */
+INLINE void InputDevice::
+set_vibration(double strong, double weak) {
+  LightMutexHolder holder(_lock);
+  do_set_vibration(std::max(std::min(strong, 1.0), 0.0), std::max(std::min(weak, 1.0), 0.0));
+}
+
+/**
+ * Enables the generation of mouse-movement events.
+ */
+INLINE void InputDevice::
+enable_pointer_events() {
+  LightMutexHolder holder(_lock);
+  _enable_pointer_events = true;
+}
+
+/**
+ * Disables the generation of mouse-movement events.
+ */
+INLINE void InputDevice::
+disable_pointer_events() {
+  LightMutexHolder holder(_lock);
+  _enable_pointer_events = false;
+  _pointer_events.clear();
+}
+
+/**
+ * Called to indicate that the device supports the given feature.
+ * Assumes the lock is held.
+ */
+INLINE void InputDevice::
+enable_feature(Feature feature) {
+  _features |= (1 << (unsigned int)feature);
+}
+
+/**
+ * Called to indicate that the device has been disconnected or connected from
+ * its host.
+ */
+INLINE void InputDevice::
+set_connected(bool connected) {
+  LightMutexHolder holder(_lock);
+  _is_connected = connected;
+}
+
+/**
+ *
+ */
+INLINE InputDevice::ButtonState::
+ButtonState(ButtonHandle handle) :
+  handle(handle) {
+}
+
+/**
+ * True if the button state is currently known.
+ */
+ALWAYS_INLINE bool InputDevice::ButtonState::
+is_known() const {
+  return (_state != S_unknown);
+}
+
+/**
+ * True if the button is currently known to be pressed.
+ */
+ALWAYS_INLINE bool InputDevice::ButtonState::
+is_pressed() const {
+  return (_state == S_down);
+}

+ 661 - 0
panda/src/device/inputDevice.cxx

@@ -0,0 +1,661 @@
+/**
+ * PANDA 3D SOFTWARE
+ * Copyright (c) Carnegie Mellon University.  All rights reserved.
+ *
+ * All use of this software is subject to the terms of the revised BSD
+ * license.  You should have received a copy of this license along
+ * with this source code in a file named "LICENSE."
+ *
+ * @file inputDevice.cxx
+ * @author rdb
+ * @date 2015-12-11
+ */
+
+#include "inputDevice.h"
+
+#if defined(_MSC_VER) && _MSC_VER < 1700
+#define fma(a, b, c) ((a) * (b) + (c))
+#endif
+
+TypeHandle InputDevice::_type_handle;
+
+/**
+ * Defines a new InputDevice.
+ */
+InputDevice::
+InputDevice(const std::string &name, DeviceClass dev_class) :
+  _name(name),
+  _device_class(dev_class),
+  _is_connected(true)
+{
+  _button_events = new ButtonEventList;
+  _pointer_events = new PointerEventList;
+}
+
+/**
+ *
+ */
+InputDevice::
+~InputDevice() {
+}
+
+/**
+ * Polls the input device for new activity, to ensure it contains the latest
+ * events.  This will only have any effect for some types of input devices;
+ * others may be updated automatically, and this method will be a no-op.
+ */
+void InputDevice::
+poll() {
+  LightMutexHolder holder(_lock);
+  do_poll();
+}
+
+/**
+ * Returns true if this device has a pending button event (a mouse button or
+ * keyboard button down/up), false otherwise.  If this returns true, the
+ * particular event may be extracted via get_button_event().
+ */
+bool InputDevice::
+has_button_event() const {
+  LightMutexHolder holder(_lock);
+  return !_button_events.is_null() && _button_events->get_num_events() > 0;
+}
+
+/**
+ * Returns the list of recently-generated ButtonEvents.
+ * The list is also cleared.
+ */
+PT(ButtonEventList) InputDevice::
+get_button_events() {
+  LightMutexHolder holder(_lock);
+  PT(ButtonEventList) result = new ButtonEventList;
+  swap(_button_events, result);
+  return result;
+}
+
+/**
+ * Returns true if this device has a pending pointer event (a mouse movement),
+ * or false otherwise.  If this returns true, the particular event may be
+ * extracted via get_pointer_event().
+ */
+bool InputDevice::
+has_pointer_event() const {
+  LightMutexHolder holder(_lock);
+  return _pointer_events != nullptr && !_pointer_events->empty();
+}
+
+/**
+ * Returns a PointerEventList containing all the recent pointer events.
+ * Clears the list.
+ */
+PT(PointerEventList) InputDevice::
+get_pointer_events() {
+  LightMutexHolder holder(_lock);
+  PT(PointerEventList) result = new PointerEventList;
+  swap(_pointer_events, result);
+  return result;
+}
+
+/**
+ * Called by the implementation to add a new known button.
+ */
+int InputDevice::
+add_button(ButtonHandle button) {
+  int index = (int)_buttons.size();
+  _buttons.push_back(ButtonState(button));
+  return index;
+}
+
+/**
+ * Called by the implementation to add a new known axis.
+ */
+int InputDevice::
+add_axis(Axis axis, int minimum, int maximum, bool centered) {
+  AxisState state;
+  state.axis = axis;
+  if (centered) {
+    // Centered, eg. for sticks.
+    state._scale = 2.0 / (maximum - minimum);
+    state._bias = (maximum + minimum) / (double)(minimum - maximum);
+  } else {
+    // 0-based, eg. for triggers.
+    state._scale = 1.0 / maximum;
+    state._bias = 0.0;
+  }
+  int index = (int)_axes.size();
+  _axes.push_back(state);
+  return index;
+}
+
+/**
+ * Called by the implementation to add a new known axis.  This version tries
+ * to guess whether the axis is centered or not.
+ */
+int InputDevice::
+add_axis(Axis axis, int minimum, int maximum) {
+  bool centered = (minimum < 0)
+    || axis == Axis::x
+    || axis == Axis::y
+    || axis == Axis::z
+    || axis == Axis::yaw
+    || axis == Axis::pitch
+    || axis == Axis::roll
+    || axis == Axis::left_x
+    || axis == Axis::left_y
+    || axis == Axis::right_x
+    || axis == Axis::right_y
+    || axis == Axis::wheel
+    || axis == Axis::rudder;
+  return add_axis(axis, minimum, maximum, centered);
+}
+
+/**
+ * Records that a new pointer was found.
+ */
+int InputDevice::
+add_pointer(PointerType type, int id, bool primary) {
+  //nassertr(_lock.debug_is_locked(), -1);
+
+  PointerData data;
+  data._id = id;
+  data._type = type;
+
+  int index = (int)_pointers.size();
+  if (_num_pointers == _pointers.size()) {
+    _pointers.push_back(data);
+  } else {
+    _pointers[index] = data;
+  }
+  ++_num_pointers;
+
+  return index;
+}
+
+/**
+ * Removes a previously added pointer.  If the current pressure is not zero,
+ * it will generate an event doing so.
+ */
+void InputDevice::
+remove_pointer(int id) {
+  nassertv(_lock.debug_is_locked());
+
+  size_t i;
+  for (i = 0; i < _pointers.size(); ++i) {
+    if (_pointers[i]._id == id) {
+      break;
+    }
+  }
+
+  if (i < _pointers.size()) {
+    if (_pointers[i]._pressure != 0.0) {
+      _pointers[i]._pressure = 0.0;
+
+      if (_enable_pointer_events) {
+        int seq = _event_sequence++;
+        double time = ClockObject::get_global_clock()->get_frame_time();
+        _pointer_events->add_event(_pointers[i], seq, time);
+      }
+    }
+
+    // Replace it with the last one.
+    if (i != _pointers.size() - 1) {
+      _pointers[i] = _pointers.back();
+    }
+    --_num_pointers;
+  }
+}
+
+/**
+ * Records that pointer data for a pointer has changed.  This can also be used
+ * to add a new pointer.
+ */
+void InputDevice::
+update_pointer(PointerData data, double time) {
+  nassertv(_lock.debug_is_locked());
+
+  PointerData *ptr = nullptr;
+  for (size_t i = 0; i < _pointers.size(); ++i) {
+    if (_pointers[i]._id == data._id) {
+      ptr = &_pointers[i];
+      *ptr = data;
+      break;
+    }
+  }
+  if (ptr == nullptr) {
+    _pointers.push_back(data);
+    ptr = &_pointers.back();
+  }
+
+  if (_enable_pointer_events) {
+    int seq = _event_sequence++;
+    _pointer_events->add_event(*ptr, seq, time);
+  }
+}
+
+/**
+ * Records that a relative pointer movement has taken place.
+ */
+void InputDevice::
+pointer_moved(int id, double x, double y, double time) {
+  nassertv(_lock.debug_is_locked());
+
+  PointerData *ptr = nullptr;
+  for (size_t i = 0; i < _pointers.size(); ++i) {
+    if (_pointers[i]._id == id) {
+      ptr = &_pointers[i];
+      _pointers[i]._xpos = x;
+      _pointers[i]._ypos = y;
+      break;
+    }
+  }
+  nassertv_always(ptr != nullptr);
+
+  if (device_cat.is_spam() && (x != 0 || y != 0)) {
+    device_cat.spam()
+      << "Pointer " << id << " moved by " << x << " x " << y << "\n";
+  }
+
+  if (_enable_pointer_events) {
+    int seq = _event_sequence++;
+    _pointer_events->add_event(ptr->_in_window,
+                               ptr->_xpos,
+                               ptr->_ypos,
+                               x, y, seq, time);
+  }
+}
+
+/**
+ * Sets the state of the indicated button index, where true indicates down,
+ * and false indicates up.  This may generate a ButtonEvent if the button has
+ * an associated ButtonHandle.  The caller should ensure that the lock is held
+ * while this call is made.
+ */
+void InputDevice::
+button_changed(int index, bool down) {
+  nassertv(_lock.debug_is_locked());
+  nassertv(index >= 0);
+  if (index >= (int)_buttons.size()) {
+    _buttons.resize(index + 1, ButtonState());
+  }
+
+  State new_state = down ? S_down : S_up;
+  if (_buttons[index]._state == new_state) {
+    return;
+  }
+  _buttons[index]._state = new_state;
+
+  ButtonHandle handle = _buttons[index].handle;
+
+  if (device_cat.is_spam()) {
+    device_cat.spam()
+      << "Changed button " << index;
+
+    if (handle != ButtonHandle::none()) {
+      device_cat.spam(false) << " (" << handle << ")";
+    }
+
+    device_cat.spam(false) << " to " << (down ? "down" : "up") << "\n";
+  }
+
+  if (handle != ButtonHandle::none()) {
+    _button_events->add_event(ButtonEvent(handle, down ? ButtonEvent::T_down : ButtonEvent::T_up));
+  }
+}
+
+/**
+ * Sets the state of the indicated analog index.  The caller should ensure that
+ * the lock is held while this call is made.  This should be a number in the
+ * range -1.0 to 1.0, representing the current position of the axis within its
+ * total range of movement.
+ */
+void InputDevice::
+set_axis_value(int index, double value) {
+  LightMutexHolder holder(_lock);
+
+  nassertv(index >= 0);
+  if ((size_t)index >= _axes.size()) {
+    _axes.resize((size_t)index + 1u, AxisState());
+  }
+
+  if (device_cat.is_spam() && _axes[index].value != value) {
+    device_cat.spam()
+      << "Changed axis " << index;
+
+    if (_axes[index].axis != Axis::none) {
+      device_cat.spam(false) << " (" << _axes[index].axis << ")";
+    }
+
+    device_cat.spam(false) << " to " << value << "\n";
+  }
+
+  _axes[index].value = value;
+  _axes[index].known = true;
+}
+
+/**
+ * Called by the implementation during do_poll to indicate that the indicated
+ * axis has received a new raw value.  Assumes the lock is held.
+ */
+void InputDevice::
+axis_changed(int index, int state) {
+  nassertv(_lock.debug_is_locked());
+  nassertv(index >= 0);
+  if ((size_t)index >= _axes.size()) {
+    _axes.resize((size_t)index + 1u, AxisState());
+  }
+
+  double value = fma((double)state, _axes[index]._scale, _axes[index]._bias);
+
+  if (device_cat.is_spam() && !IS_NEARLY_EQUAL(_axes[index].value, value)) {
+    device_cat.spam()
+      << "Changed axis " << index;
+
+    if (_axes[index].axis != Axis::none) {
+      device_cat.spam(false) << " (" << _axes[index].axis << ")";
+    }
+
+    device_cat.spam(false) << " to " << value << " (raw value " << state << ")\n";
+  }
+
+  _axes[index].value = value;
+  _axes[index].known = true;
+}
+
+/**
+ * Records that a tracker movement has taken place.
+ */
+void InputDevice::
+tracker_changed(const LPoint3 &pos, const LOrientation &orient, double time) {
+  nassertv(_lock.debug_is_locked());
+
+  _tracker_data.set_pos(pos);
+  _tracker_data.set_orient(orient);
+  _tracker_data.set_time(time);
+}
+
+/**
+ * Writes a one-line string describing the device.
+ */
+void InputDevice::
+output(std::ostream &out) const {
+  LightMutexHolder holder(_lock);
+
+  out << _name << " (";
+
+  if (!_is_connected) {
+    out << "dis";
+  }
+
+  out << "connected)";
+
+  if (_device_class != DeviceClass::unknown) {
+    out << ", " << _device_class;
+  }
+
+  if (_buttons.size() > 0) {
+    out << ", " << _buttons.size() << " button";
+    if (_buttons.size() != 1) {
+      out.put('s');
+    }
+  }
+
+  if (_axes.size() > 0) {
+    out << ", " << _axes.size() << " ax"
+        << (_axes.size() != 1 ? 'e' : 'i') << 's';
+  }
+
+  if (_features & (unsigned int)Feature::pointer) {
+    out << ", pointer";
+  }
+  if (_features & (unsigned int)Feature::keyboard) {
+    out << ", keyboard";
+  }
+  if (_features & (unsigned int)Feature::tracker) {
+    out << ", tracker";
+  }
+  if (_features & (unsigned int)Feature::vibration) {
+    out << ", vibration";
+  }
+  if (_features & (unsigned int)Feature::battery) {
+    out << ", battery";
+
+    if (_battery_data.level > 0 && _battery_data.max_level > 0) {
+      out << " [";
+      short i = 0;
+      for (; i < _battery_data.level; ++i) {
+        out << '=';
+      }
+      for (; i < _battery_data.max_level; ++i) {
+        out << ' ';
+      }
+      out << ']';
+    }
+  }
+}
+
+/**
+ * Writes a one-line string of all of the current button states.
+ */
+void InputDevice::
+output_buttons(std::ostream &out) const {
+  LightMutexHolder holder(_lock);
+
+  bool any_buttons = false;
+  Buttons::const_iterator bi;
+  for (bi = _buttons.begin(); bi != _buttons.end(); ++bi) {
+    const ButtonState &state = (*bi);
+    if (state.is_known()) {
+      if (any_buttons) {
+        out << ", ";
+      }
+      any_buttons = true;
+      out << (int)(bi - _buttons.begin()) << "=";
+      if (state._state == S_up) {
+        out << "up";
+      } else {
+        out << "down";
+      }
+    }
+  }
+
+  if (!any_buttons) {
+    out << "no known buttons";
+  }
+}
+
+/**
+ * Writes a multi-line description of the current button states.
+ */
+void InputDevice::
+write_buttons(std::ostream &out, int indent_level) const {
+  bool any_buttons = false;
+  Buttons::const_iterator bi;
+  for (bi = _buttons.begin(); bi != _buttons.end(); ++bi) {
+    const ButtonState &state = (*bi);
+    if (state.is_known()) {
+      any_buttons = true;
+
+      indent(out, indent_level)
+        << (int)(bi - _buttons.begin()) << ". ";
+
+      if (state.handle != ButtonHandle::none()) {
+        out << "(" << state.handle << ") ";
+      }
+
+      if (state._state == S_up) {
+        out << "up";
+      } else {
+        out << "down";
+      }
+      out << "\n";
+    }
+  }
+
+  if (!any_buttons) {
+    indent(out, indent_level)
+      << "(no known buttons)\n";
+  }
+}
+
+/**
+ * Writes a multi-line description of the current analog axis states.
+ */
+void InputDevice::
+write_axes(std::ostream &out, int indent_level) const {
+  LightMutexHolder holder(_lock);
+
+  bool any_axis = false;
+  Axes::const_iterator ai;
+  for (ai = _axes.begin(); ai != _axes.end(); ++ai) {
+    const AxisState &state = (*ai);
+    if (state.known) {
+      any_axis = true;
+
+      indent(out, indent_level)
+        << (int)(ai - _axes.begin()) << ". " << state.value << "\n";
+    }
+  }
+
+  if (!any_axis) {
+    indent(out, indent_level)
+      << "(no known analog axes)\n";
+  }
+}
+
+/**
+ * Sets the vibration strength.  The first argument controls a low-frequency
+ * motor, if present, and the latter controls a high-frequency motor.
+ * The values are within the 0-1 range.
+ */
+void InputDevice::
+do_set_vibration(double strong, double weak) {
+}
+
+/**
+ * Polls the input device for new activity, to ensure it contains the latest
+ * events.  This will only have any effect for some types of input devices;
+ * others may be updated automatically, and this method will be a no-op.
+ */
+void InputDevice::
+do_poll() {
+}
+
+/**
+ * Returns a string describing the given device class enumerant.
+ */
+std::string InputDevice::
+format_device_class(DeviceClass dc) {
+  switch (dc) {
+  case InputDevice::DeviceClass::unknown:
+    return "unknown";
+
+  case InputDevice::DeviceClass::virtual_device:
+    return "virtual_device";
+
+  case InputDevice::DeviceClass::keyboard:
+    return "keyboard";
+
+  case InputDevice::DeviceClass::mouse:
+    return "mouse";
+
+  case InputDevice::DeviceClass::touch:
+    return "touch";
+
+  case InputDevice::DeviceClass::gamepad:
+    return "gamepad";
+
+  case InputDevice::DeviceClass::flight_stick:
+    return "flight_stick";
+
+  case InputDevice::DeviceClass::steering_wheel:
+    return "steering_wheel";
+
+  case InputDevice::DeviceClass::dance_pad:
+    return "dance_pad";
+
+  case InputDevice::DeviceClass::hmd:
+    return "hmd";
+
+  case InputDevice::DeviceClass::spatial_mouse:
+    return "spatial_mouse";
+  }
+  return "**invalid**";
+}
+
+/**
+ * Returns a string describing the given axis enumerant.
+ */
+std::string InputDevice::
+format_axis(Axis axis) {
+  switch (axis) {
+  case InputDevice::Axis::none:
+    return "none";
+
+  case InputDevice::Axis::x:
+    return "x";
+
+  case InputDevice::Axis::y:
+    return "y";
+
+  case InputDevice::Axis::z:
+    return "z";
+
+  case InputDevice::Axis::yaw:
+    return "yaw";
+
+  case InputDevice::Axis::pitch:
+    return "pitch";
+
+  case InputDevice::Axis::roll:
+    return "roll";
+
+  case InputDevice::Axis::left_x:
+    return "left_x";
+
+  case InputDevice::Axis::left_y:
+    return "left_y";
+
+  case InputDevice::Axis::left_trigger:
+    return "left_trigger";
+
+  case InputDevice::Axis::right_x:
+    return "right_x";
+
+  case InputDevice::Axis::right_y:
+    return "right_y";
+
+  case InputDevice::Axis::right_trigger:
+    return "right_trigger";
+
+  //case InputDevice::Axis::trigger:
+  //  return "trigger";
+
+  case InputDevice::Axis::throttle:
+    return "throttle";
+
+  case InputDevice::Axis::rudder:
+    return "rudder";
+
+  case InputDevice::Axis::wheel:
+    return "wheel";
+
+  case InputDevice::Axis::accelerator:
+    return "accelerator";
+
+  case InputDevice::Axis::brake:
+    return "brake";
+  }
+  return "**invalid**";
+}
+
+std::ostream &
+operator << (std::ostream &out, InputDevice::DeviceClass dc) {
+  out << InputDevice::format_device_class(dc);
+  return out;
+}
+
+std::ostream &
+operator << (std::ostream &out, InputDevice::Axis axis) {
+  out << InputDevice::format_axis(axis);
+  return out;
+}

+ 366 - 0
panda/src/device/inputDevice.h

@@ -0,0 +1,366 @@
+/**
+ * PANDA 3D SOFTWARE
+ * Copyright (c) Carnegie Mellon University.  All rights reserved.
+ *
+ * All use of this software is subject to the terms of the revised BSD
+ * license.  You should have received a copy of this license along
+ * with this source code in a file named "LICENSE."
+ *
+ * @file inputDevice.h
+ * @author rdb
+ * @date 2015-12-11
+ */
+
+#ifndef INPUTDEVICE_H
+#define INPUTDEVICE_H
+
+#include "pandabase.h"
+
+#include "buttonEvent.h"
+#include "buttonEventList.h"
+#include "pointerEvent.h"
+#include "pointerEventList.h"
+#include "pointerData.h"
+#include "trackerData.h"
+#include "clockObject.h"
+
+#include "pdeque.h"
+#include "pvector.h"
+#include "lightMutex.h"
+#include "lightMutexHolder.h"
+
+/**
+ * This is a structure representing a single input device.  Input devices may
+ * have zero or more buttons, pointers, or axes associated with them, and
+ * optionally a motion tracker.
+ *
+ * These devices are brought under a common interface because there is such a
+ * large range of devices out there that may support any number of these types
+ * of axes, we couldn't even begin to cover them with type-specific
+ * subclasses.
+ *
+ * Use the various has_() and get_num_() methods to determine information about
+ * the device capabilities. For instance, has_keyboard() will give an
+ * indication that you can receive keystroke events from this device, and
+ * get_num_buttons() will tell you that the device may send button events.
+ *
+ * There is the DeviceType enumeration, however, which will (if known) contain
+ * identification of the general category of devices this fits in, such as
+ * keyboard, mouse, gamepad, or flight stick.
+ */
+class EXPCL_PANDA_DEVICE InputDevice : public TypedReferenceCount {
+PUBLISHED:
+  // This enum contains information that can be used to identify the
+  // type of input device.
+  enum class DeviceClass {
+    // It is not known what type of device this is.
+    unknown,
+
+    // This means that the device doesn't correspond to a physical
+    // device, but rather to a dynamic source of input events.
+    virtual_device,
+
+    // A physical, alphabetical keyboard.
+    keyboard,
+
+    mouse,
+    touch,
+
+    // A gamepad with action buttons, a D-pad, and thumbsticks.
+    gamepad,
+
+    flight_stick,
+    steering_wheel,
+    dance_pad,
+
+    // Head-mounted display.
+    hmd,
+
+    // 3D mouse, such as produced by 3Dconnexion.
+    spatial_mouse,
+  };
+
+  enum class Feature {
+    // The device provides absolute screen coordinates.
+    pointer,
+
+    // The device has an interface for providing text input.
+    keyboard,
+
+    // The device has a motion tracker, such as an HMD.
+    tracker,
+
+    // The device can produce force feedback.
+    vibration,
+
+    // The device provides information about battery life.
+    battery,
+  };
+
+  enum class Axis {
+    none,
+
+    // Generic translational axes
+    x,
+    y,
+    z,
+
+    // Generic rotational axes, used by joysticks and 3D mice
+    yaw,
+    pitch,
+    roll,
+
+    // Gamepad
+    left_x,
+    left_y,
+    left_trigger,
+    right_x,
+    right_y,
+    right_trigger,
+
+    // Flight stick specific
+    throttle,
+    rudder, // When available separately from yaw
+
+    // Steering wheel / pedals
+    wheel,
+    accelerator,
+    brake,
+  };
+
+  enum State {
+    S_unknown,
+    S_up,
+    S_down
+  };
+
+  class ButtonState {
+  public:
+    constexpr ButtonState() = default;
+    INLINE ButtonState(ButtonHandle handle);
+    ALWAYS_INLINE bool is_known() const;
+    ALWAYS_INLINE bool is_pressed() const;
+
+  PUBLISHED:
+    operator bool() { return _state != S_unknown; }
+
+    MAKE_PROPERTY(known, is_known);
+    MAKE_PROPERTY(pressed, is_pressed);
+
+    ButtonHandle handle = ButtonHandle::none();
+
+  public:
+    State _state = S_unknown;
+  };
+
+  class AxisState {
+  public:
+    constexpr AxisState() = default;
+
+  PUBLISHED:
+    operator bool() { return known && value != 0.0; }
+
+    Axis axis = Axis::none;
+    double value = 0.0;
+    bool known = false;
+
+  public:
+    double _scale = 1.0;
+    double _bias = 0.0;
+  };
+
+  class BatteryData {
+  PUBLISHED:
+    // Ranges from 0 through max_level.
+    short level = -1;
+
+    // Maximum value of 'level' field.
+    short max_level = -1;
+  };
+
+protected:
+  InputDevice(const std::string &name, DeviceClass dev_class);
+
+public:
+  InputDevice();
+  InputDevice(const InputDevice &copy) = delete;
+  InputDevice &operator = (const InputDevice &copy) = delete;
+  virtual ~InputDevice();
+
+  INLINE std::string get_name() const;
+  INLINE std::string get_manufacturer() const;
+  INLINE std::string get_serial_number() const;
+  INLINE unsigned short get_vendor_id() const;
+  INLINE unsigned short get_product_id() const;
+  INLINE bool is_connected() const;
+  INLINE DeviceClass get_device_class() const;
+
+  INLINE bool has_pointer() const;
+  INLINE bool has_keyboard() const;
+  INLINE bool has_tracker() const;
+  INLINE bool has_vibration() const;
+  INLINE bool has_battery() const;
+
+  INLINE TrackerData get_tracker() const;
+  INLINE BatteryData get_battery() const;
+
+  INLINE size_t get_num_buttons() const;
+  INLINE ButtonHandle get_button_map(size_t index) const;
+  INLINE bool is_button_pressed(size_t index) const;
+  INLINE bool is_button_known(size_t index) const;
+  INLINE ButtonState get_button(size_t index) const;
+
+  INLINE size_t get_num_axes() const;
+  INLINE double get_axis_value(size_t index) const;
+  INLINE bool is_axis_known(size_t index) const;
+  INLINE AxisState get_axis(size_t index) const;
+
+PUBLISHED:
+  // The human-readable name of this input device.
+  MAKE_PROPERTY(name, get_name);
+
+  // The device's manufacturer, or the empty string if not known.
+  MAKE_PROPERTY(manufacturer, get_manufacturer);
+
+  // The device's serial number, or the empty string if not known.
+  MAKE_PROPERTY(serial_number, get_serial_number);
+
+  // USB vendor ID of the device, or 0 if not known.
+  MAKE_PROPERTY(vendor_id, get_vendor_id);
+
+  // USB product ID of the device, or 0 if not known.
+  MAKE_PROPERTY(product_id, get_product_id);
+
+  // This is false if we know that the device is not currently connected.
+  // May report false positives if we can't know this with certainty.
+  MAKE_PROPERTY(connected, is_connected);
+
+  // This contains an identification of the general type of device.  If
+  // this could not be determined, it is set to DC_unknown.
+  MAKE_PROPERTY(device_class, get_device_class);
+
+  // Determine supported features
+  INLINE bool has_feature(Feature feature) const;
+
+  // Getters for the various types of device data.
+  MAKE_PROPERTY2(tracker, has_tracker, get_tracker);
+  MAKE_PROPERTY2(battery, has_battery, get_battery);
+
+  // Make device buttons and axes iterable
+  MAKE_SEQ_PROPERTY(buttons, get_num_buttons, get_button);
+  MAKE_SEQ_PROPERTY(axes, get_num_axes, get_axis);
+
+  // Associate buttons/axes with symbolic handles.
+  INLINE void map_button(size_t index, ButtonHandle handle);
+  INLINE void map_axis(size_t index, Axis axis);
+  INLINE ButtonState find_button(ButtonHandle handle) const;
+  INLINE AxisState find_axis(Axis axis) const;
+
+  // Enable rumble force-feedback effects
+  INLINE void set_vibration(double strong, double weak);
+
+  INLINE void enable_pointer_events();
+  INLINE void disable_pointer_events();
+
+  void poll();
+
+  bool has_button_event() const;
+  PT(ButtonEventList) get_button_events();
+  bool has_pointer_event() const;
+  PT(PointerEventList) get_pointer_events();
+
+  virtual void output(std::ostream &out) const;
+
+public:
+  static std::string format_device_class(DeviceClass dc);
+  static std::string format_axis(Axis axis);
+
+protected:
+  INLINE void enable_feature(Feature feature);
+
+  // Called during the constructor to add new axes or buttons
+  int add_button(ButtonHandle handle);
+  int add_axis(Axis axis, int minimum, int maximum, bool centered);
+  int add_axis(Axis axis, int minimum, int maximum);
+
+  int add_pointer(PointerType type, int id, bool primary = false);
+  void remove_pointer(int id);
+  void update_pointer(PointerData data, double time);
+  void pointer_moved(int id, double x, double y, double time);
+  void button_changed(int index, bool down);
+  void axis_changed(int index, int value);
+  void set_axis_value(int index, double state);
+
+  void tracker_changed(const LPoint3 &pos, const LOrientation &orient, double time);
+
+  virtual void do_set_vibration(double low, double high);
+  virtual void do_poll();
+
+public:
+  INLINE void set_connected(bool connected);
+
+  void output_buttons(std::ostream &out) const;
+  void write_buttons(std::ostream &out, int indent_level) const;
+  void write_axes(std::ostream &out, int indent_level) const;
+
+protected:
+  typedef pdeque<ButtonEvent> ButtonEvents;
+
+  LightMutex _lock {"InputDevice"};
+
+  std::string _name;
+  std::string _serial_number;
+  std::string _manufacturer;
+  DeviceClass _device_class = DeviceClass::unknown;
+  unsigned int _features = 0;
+  int _event_sequence = 0;
+  unsigned short _vendor_id = 0;
+  unsigned short _product_id = 0;
+  bool _is_connected = false;
+  bool _enable_pointer_events = false;
+  PT(ButtonEventList) _button_events;
+  PT(PointerEventList) _pointer_events;
+
+  size_t _num_pointers = 0;
+  typedef pvector<PointerData> Pointers;
+  Pointers _pointers;
+
+PUBLISHED:
+  typedef pvector<ButtonState> Buttons;
+  typedef pvector<AxisState> Axes;
+  Buttons _buttons;
+  Axes _axes;
+
+  PointerData _pointer_data;
+  BatteryData _battery_data;
+  TrackerData _tracker_data;
+
+public:
+  static TypeHandle get_class_type() {
+    return _type_handle;
+  }
+  static void init_type() {
+    TypedReferenceCount::init_type();
+    register_type(_type_handle, "InputDevice",
+                  TypedReferenceCount::get_class_type());
+  }
+  virtual TypeHandle get_type() const {
+    return get_class_type();
+  }
+  virtual TypeHandle force_init_type() {init_type(); return get_class_type();}
+
+private:
+  static TypeHandle _type_handle;
+};
+
+INLINE std::ostream &operator << (std::ostream &out, const InputDevice &device) {
+  device.output(out);
+  return out;
+}
+
+EXPCL_PANDA_DEVICE std::ostream &operator << (std::ostream &out, InputDevice::DeviceClass dc);
+EXPCL_PANDA_DEVICE std::ostream &operator << (std::ostream &out, InputDevice::Axis axis);
+
+#include "inputDevice.I"
+
+#endif

+ 25 - 0
panda/src/device/inputDeviceManager.I

@@ -0,0 +1,25 @@
+/**
+ * PANDA 3D SOFTWARE
+ * Copyright (c) Carnegie Mellon University.  All rights reserved.
+ *
+ * All use of this software is subject to the terms of the revised BSD
+ * license.  You should have received a copy of this license along
+ * with this source code in a file named "LICENSE."
+ *
+ * @file inputDeviceManager.I
+ * @author rdb
+ * @date 2015-12-09
+ */
+
+/**
+ * Returns the singleton InputDeviceManager instance.
+ */
+INLINE InputDeviceManager *InputDeviceManager::
+get_global_ptr() {
+  if (_global_ptr != nullptr) {
+    return _global_ptr;
+  } else {
+    make_global_ptr();
+    return _global_ptr;
+  }
+}

+ 128 - 0
panda/src/device/inputDeviceManager.cxx

@@ -0,0 +1,128 @@
+/**
+ * PANDA 3D SOFTWARE
+ * Copyright (c) Carnegie Mellon University.  All rights reserved.
+ *
+ * All use of this software is subject to the terms of the revised BSD
+ * license.  You should have received a copy of this license along
+ * with this source code in a file named "LICENSE."
+ *
+ * @file inputDeviceManager.cxx
+ * @author rdb
+ * @date 2015-12-09
+ */
+
+#include "inputDeviceManager.h"
+#include "ioKitInputDeviceManager.h"
+#include "linuxInputDeviceManager.h"
+#include "winInputDeviceManager.h"
+#include "throw_event.h"
+#include "config_putil.h"
+
+InputDeviceManager *InputDeviceManager::_global_ptr = nullptr;
+
+/**
+ * Initializes the input device manager by scanning which devices are currently
+ * connected and setting up any platform-dependent structures necessary for
+ * listening for future device connect events.
+ */
+InputDeviceManager::
+InputDeviceManager() : _lock("InputDeviceManager") {
+}
+
+/**
+ * Creates the global input manager.
+ */
+void InputDeviceManager::
+make_global_ptr() {
+  init_libputil();
+
+#ifdef _WIN32
+  _global_ptr = new WinInputDeviceManager;
+#elif defined(__APPLE__)
+  _global_ptr = new IOKitInputDeviceManager;
+#elif defined(PHAVE_LINUX_INPUT_H)
+  _global_ptr = new LinuxInputDeviceManager;
+#else
+  _global_ptr = new InputDeviceManager;
+#endif
+}
+
+/**
+ * Description: Returns all currently connected devices.
+ */
+InputDeviceSet InputDeviceManager::
+get_devices() const {
+  InputDeviceSet devices;
+  LightMutexHolder holder(_lock);
+
+  for (size_t i = 0; i < _connected_devices.size(); ++i) {
+    InputDevice *device = _connected_devices[i];
+    devices.add_device(device);
+  }
+
+  return devices;
+}
+
+/**
+ * Description: Returns all currently connected devices of the given device class.
+ */
+InputDeviceSet InputDeviceManager::
+get_devices(InputDevice::DeviceClass device_class) const {
+  InputDeviceSet devices;
+  LightMutexHolder holder(_lock);
+
+  for (size_t i = 0; i < _connected_devices.size(); ++i) {
+    InputDevice *device = _connected_devices[i];
+    if (device->get_device_class() == device_class) {
+      devices.add_device(device);
+    }
+  }
+
+  return devices;
+}
+
+
+/**
+ * Called when a new device has been discovered.  This may also be used to
+ * register virtual devices.
+ *
+ * This causes a connect-device event to be thrown.
+ */
+void InputDeviceManager::
+add_device(InputDevice *device) {
+  {
+    LightMutexHolder holder(_lock);
+    _connected_devices.add_device(device);
+
+#ifdef PHAVE_LINUX_INPUT_H
+    // If we had added it pending activity on the device, remove it
+    // from the list of inactive devices.
+    _inactive_devices.remove_device(device);
+#endif
+  }
+  throw_event("connect-device", device);
+}
+
+/**
+ * Called when a device has been removed, or when a device should otherwise no
+ * longer be tracked.
+ *
+ * This causes a disconnect-device event to be thrown.
+ */
+void InputDeviceManager::
+remove_device(InputDevice *device) {
+  {
+    LightMutexHolder holder(_lock);
+    _connected_devices.remove_device(device);
+  }
+
+  throw_event("disconnect-device", device);
+}
+
+/**
+ * Polls the system to see if there are any new devices.  In some
+ * implementations this is a no-op.
+ */
+void InputDeviceManager::
+update() {
+}

+ 63 - 0
panda/src/device/inputDeviceManager.h

@@ -0,0 +1,63 @@
+/**
+ * PANDA 3D SOFTWARE
+ * Copyright (c) Carnegie Mellon University.  All rights reserved.
+ *
+ * All use of this software is subject to the terms of the revised BSD
+ * license.  You should have received a copy of this license along
+ * with this source code in a file named "LICENSE."
+ *
+ * @file inputDeviceManager.h
+ * @author rdb
+ * @date 2015-12-09
+ */
+
+#ifndef INPUTDEVICEMANAGER_H
+#define INPUTDEVICEMANAGER_H
+
+#include "pandabase.h"
+#include "lightMutex.h"
+#include "inputDevice.h"
+#include "inputDeviceSet.h"
+
+#ifdef _WIN32
+#include "xinputDevice.h"
+class WinRawInputDevice;
+#endif
+
+/**
+ * This class keeps track of all the devices on a system, and sends out events
+ * when a device has been hot-plugged.
+ */
+class EXPCL_PANDA_DEVICE InputDeviceManager {
+protected:
+  InputDeviceManager();
+  ~InputDeviceManager() = default;
+
+  static void make_global_ptr();
+
+PUBLISHED:
+  InputDeviceSet get_devices() const;
+  InputDeviceSet get_devices(InputDevice::DeviceClass device_class) const;
+
+  void add_device(InputDevice *device);
+  void remove_device(InputDevice *device);
+
+  virtual void update();
+
+  INLINE static InputDeviceManager *get_global_ptr();
+
+protected:
+  LightMutex _lock;
+
+#ifdef PHAVE_LINUX_INPUT_H
+  InputDeviceSet _inactive_devices;
+#endif
+
+  InputDeviceSet _connected_devices;
+
+  static InputDeviceManager *_global_ptr;
+};
+
+#include "inputDeviceManager.I"
+
+#endif

+ 79 - 0
panda/src/device/inputDeviceNode.cxx

@@ -0,0 +1,79 @@
+/**
+ * PANDA 3D SOFTWARE
+ * Copyright (c) Carnegie Mellon University.  All rights reserved.
+ *
+ * All use of this software is subject to the terms of the revised BSD
+ * license.  You should have received a copy of this license along
+ * with this source code in a file named "LICENSE."
+ *
+ * @file InputDeviceNode.cxx
+ * @author fireclaw
+ * @date 2016-07-14
+ */
+
+#include "config_device.h"
+#include "inputDeviceNode.h"
+#include "dataNodeTransmit.h"
+#include "inputDeviceManager.h"
+
+TypeHandle InputDeviceNode::_type_handle;
+
+InputDeviceNode::
+InputDeviceNode(InputDevice *device, const std::string &name) :
+  DataNode(name),
+  _device(device)
+{
+  _button_events_output = define_output("button_events", ButtonEventList::get_class_type());
+}
+
+/**
+ * Redirects the class to get the data from a different device.
+ */
+void InputDeviceNode::
+set_device(InputDevice *device) {
+  _device = device;
+}
+
+/**
+ * Returns the associated device.
+ */
+PT(InputDevice) InputDeviceNode::
+get_device() const {
+  return _device;
+}
+
+/**
+ * The virtual implementation of transmit_data().  This function receives an
+ * array of input parameters and should generate an array of output
+ * parameters.  The input parameters may be accessed with the index numbers
+ * returned by the define_input() calls that were made earlier (presumably in
+ * the constructor); likewise, the output parameters should be set with the
+ * index numbers returned by the define_output() calls.
+ */
+void InputDeviceNode::
+do_transmit_data(DataGraphTraverser *, const DataNodeTransmit &,
+                 DataNodeTransmit &output) {
+
+  if (_device == nullptr && !_device->is_connected()) {
+    return;
+  }
+
+  _device->poll();
+
+  // get all button events of the device and forward them to the data graph
+  if (_device->has_button_event()) {
+    PT(ButtonEventList) bel = _device->get_button_events();
+
+    // Register the current state for each button.
+    for (int i = 0; i < bel->get_num_events(); ++i) {
+      const ButtonEvent &event = bel->get_event(i);
+      if (event._type == ButtonEvent::T_down) {
+        _button_states[event._button] = true;
+      } else if (event._type == ButtonEvent::T_down) {
+        _button_states[event._button] = false;
+      }
+    }
+
+    output.set_data(_button_events_output, EventParameter(bel));
+  }
+}

+ 73 - 0
panda/src/device/inputDeviceNode.h

@@ -0,0 +1,73 @@
+/**
+ * PANDA 3D SOFTWARE
+ * Copyright (c) Carnegie Mellon University.  All rights reserved.
+ *
+ * All use of this software is subject to the terms of the revised BSD
+ * license.  You should have received a copy of this license along
+ * with this source code in a file named "LICENSE."
+ *
+ * @file InputDeviceNode.h
+ * @author fireclaw
+ * @date 2016-07-14
+ */
+
+#ifndef INPUTDEVICENODE_H
+#define INPUTDEVICENODE_H
+
+#include "pandabase.h"
+
+#include "dataNode.h"
+#include "inputDeviceManager.h"
+#include "linmath_events.h"
+
+/**
+ * Reads the controller data sent from the InputDeviceManager, and transmits
+ * it down the data graph.
+ *
+ * This is intended to only be accessed from the app thread.
+ */
+class EXPCL_PANDA_DEVICE InputDeviceNode : public DataNode {
+PUBLISHED:
+  InputDeviceNode(InputDevice *device, const std::string &name);
+
+public:
+  void set_device(InputDevice *device);
+  PT(InputDevice) get_device() const;
+
+PUBLISHED:
+  MAKE_PROPERTY(device, get_device, set_device);
+
+protected:
+  // Inherited from DataNode
+  virtual void do_transmit_data(DataGraphTraverser *trav,
+                                const DataNodeTransmit &input,
+                                DataNodeTransmit &output);
+
+private:
+  pmap<ButtonHandle, bool> _button_states;
+  pmap<InputDevice::Axis, double> _control_states;
+
+  // outputs
+  int _button_events_output;
+
+  PT(InputDevice) _device;
+
+public:
+  static TypeHandle get_class_type() {
+    return _type_handle;
+  }
+  static void init_type() {
+    DataNode::init_type();
+    register_type(_type_handle, "InputDeviceNode",
+                  DataNode::get_class_type());
+  }
+  virtual TypeHandle get_type() const {
+    return get_class_type();
+  }
+  virtual TypeHandle force_init_type() {init_type(); return get_class_type();}
+
+private:
+  static TypeHandle _type_handle;
+};
+
+#endif // INPUTDEVICENODE_H

+ 36 - 0
panda/src/device/inputDeviceSet.I

@@ -0,0 +1,36 @@
+/**
+ * PANDA 3D SOFTWARE
+ * Copyright (c) Carnegie Mellon University.  All rights reserved.
+ *
+ * All use of this software is subject to the terms of the revised BSD
+ * license.  You should have received a copy of this license along
+ * with this source code in a file named "LICENSE."
+ *
+ * @file inputDeviceSet.I
+ * @author rdb
+ * @date 2015-12-16
+ */
+
+/**
+ *
+ */
+INLINE InputDeviceSet::
+~InputDeviceSet() {
+}
+
+/**
+ * Returns the nth InputDevice in the collection.
+ */
+INLINE InputDevice *InputDeviceSet::
+operator [] (size_t index) const {
+  nassertr(index < _devices.size(), nullptr);
+  return _devices[index];
+}
+
+/**
+ * Returns the number of devices in the collection.
+ */
+INLINE size_t InputDeviceSet::
+size() const {
+  return _devices.size();
+}

+ 141 - 0
panda/src/device/inputDeviceSet.cxx

@@ -0,0 +1,141 @@
+/**
+ * PANDA 3D SOFTWARE
+ * Copyright (c) Carnegie Mellon University.  All rights reserved.
+ *
+ * All use of this software is subject to the terms of the revised BSD
+ * license.  You should have received a copy of this license along
+ * with this source code in a file named "LICENSE."
+ *
+ * @file inputDeviceSet.cxx
+ * @author rdb
+ * @date 2015-12-16
+ */
+
+#include "inputDeviceSet.h"
+#include "indent.h"
+
+/**
+ *
+ */
+InputDeviceSet::
+InputDeviceSet() {
+}
+
+/**
+ *
+ */
+InputDeviceSet::
+InputDeviceSet(const InputDeviceSet &copy) :
+  _devices(copy._devices)
+{
+}
+
+/**
+ *
+ */
+void InputDeviceSet::
+operator = (const InputDeviceSet &copy) {
+  _devices = copy._devices;
+}
+
+/**
+ * Adds a new InputDevice to the collection.
+ */
+void InputDeviceSet::
+add_device(InputDevice *device) {
+  _devices.insert(device);
+}
+
+/**
+ * Removes the indicated InputDevice from the collection.
+ * @return true if the device was removed, false if it was not a member.
+ */
+bool InputDeviceSet::
+remove_device(InputDevice *device) {
+  return _devices.erase(device) > 0;
+}
+
+/**
+ * Adds all the InputDevices indicated in the other collection to this set.
+ */
+void InputDeviceSet::
+add_devices_from(const InputDeviceSet &other) {
+  size_t other_num_devices = other.size();
+  for (size_t i = 0; i < other_num_devices; i++) {
+    _devices.push_back(other[i]);
+  }
+  _devices.sort();
+}
+
+/**
+ * Removes from this collection all of the devices listed in the other set.
+ */
+void InputDeviceSet::
+remove_devices_from(const InputDeviceSet &other) {
+  InputDevices new_devices;
+  InputDevices::const_iterator it;
+  for (it = _devices.begin(); it != _devices.end(); ++it) {
+    if (!other.has_device(*it)) {
+      new_devices.push_back(*it);
+    }
+  }
+  new_devices.sort();
+  _devices.swap(new_devices);
+}
+
+/**
+ * Returns true if the indicated InputDevice appears in this collection, false
+ * otherwise.
+ */
+bool InputDeviceSet::
+has_device(InputDevice *device) const {
+  InputDevices::const_iterator it = _devices.find(device);
+  return (it != _devices.end());
+}
+
+/**
+ * Removes all InputDevices from the collection.
+ */
+void InputDeviceSet::
+clear() {
+  _devices.clear();
+}
+
+/**
+ * This is a hint to Panda to allocate enough memory to hold the given number
+ * of InputDevices, if you know ahead of time how many you will be adding.
+ */
+void InputDeviceSet::
+reserve(size_t num) {
+  _devices.reserve(num);
+}
+
+/**
+ * Writes a brief one-line description of the InputDeviceSet to the indicated
+ * output stream.
+ */
+void InputDeviceSet::
+output(std::ostream &out) const {
+  if (_devices.size() == 1) {
+    out << "1 input device";
+  } else {
+    out << _devices.size() << " input devices";
+  }
+}
+
+/**
+ * Writes a complete multi-line description of the InputDeviceSet to the
+ * indicated output stream.
+ */
+void InputDeviceSet::
+write(std::ostream &out, int indent_level) const {
+  output(indent(out, indent_level));
+  out << ":\n";
+  indent_level += 2;
+
+  InputDevices::const_iterator it;
+  for (it = _devices.begin(); it != _devices.end(); ++it) {
+    (*it)->output(indent(out, indent_level));
+    out << '\n';
+  }
+}

+ 64 - 0
panda/src/device/inputDeviceSet.h

@@ -0,0 +1,64 @@
+/**
+ * PANDA 3D SOFTWARE
+ * Copyright (c) Carnegie Mellon University.  All rights reserved.
+ *
+ * All use of this software is subject to the terms of the revised BSD
+ * license.  You should have received a copy of this license along
+ * with this source code in a file named "LICENSE."
+ *
+ * @file inputDeviceSet.h
+ * @author rdb
+ * @date 2015-12-16
+ */
+
+#ifndef INPUTDEVICESET_H
+#define INPUTDEVICESET_H
+
+#include "pandabase.h"
+#include "ordered_vector.h"
+#include "inputDevice.h"
+
+/**
+ * Manages a list of InputDevice objects, as returned by various
+ * InputDeviceManager methods.  This is implemented like a set, meaning the
+ * same device cannot occur more than once.
+ */
+class EXPCL_PANDA_DEVICE InputDeviceSet {
+PUBLISHED:
+  InputDeviceSet();
+  InputDeviceSet(const InputDeviceSet &copy);
+  void operator = (const InputDeviceSet &copy);
+  INLINE ~InputDeviceSet();
+
+public:
+  void add_device(InputDevice *device);
+  bool remove_device(InputDevice *device);
+  void add_devices_from(const InputDeviceSet &other);
+  void remove_devices_from(const InputDeviceSet &other);
+  bool has_device(InputDevice *device) const;
+
+PUBLISHED:
+  void clear();
+  void reserve(size_t num);
+
+  INLINE InputDevice *operator [] (size_t index) const;
+  INLINE size_t size() const;
+
+  void output(std::ostream &out) const;
+  void write(std::ostream &out, int indent_level = 0) const;
+
+private:
+  // This is currently implemented as ov_set instead of a regular set so that
+  // we can still support random access.
+  typedef ov_set<PT(InputDevice)> InputDevices;
+  InputDevices _devices;
+};
+
+INLINE std::ostream &operator << (std::ostream &out, const InputDeviceSet &col) {
+  col.output(out);
+  return out;
+}
+
+#include "inputDeviceSet.I"
+
+#endif

+ 798 - 0
panda/src/device/ioKitInputDevice.cxx

@@ -0,0 +1,798 @@
+/**
+ * PANDA 3D SOFTWARE
+ * Copyright (c) Carnegie Mellon University.  All rights reserved.
+ *
+ * All use of this software is subject to the terms of the revised BSD
+ * license.  You should have received a copy of this license along
+ * with this source code in a file named "LICENSE."
+ *
+ * @file ioKitInputDevice.cxx
+ * @author rdb
+ * @date 2017-12-21
+ */
+
+#include "ioKitInputDevice.h"
+
+#if defined(__APPLE__) && !defined(CPPPARSER)
+
+#include <IOKit/hid/IOHIDElement.h>
+
+#include "keyboardButton.h"
+#include "gamepadButton.h"
+#include "mouseButton.h"
+
+static void removal_callback(void *ctx, IOReturn result, void *sender) {
+  IOKitInputDevice *input_device = (IOKitInputDevice *)ctx;
+  nassertv(input_device != nullptr);
+  input_device->on_remove();
+}
+
+/**
+ * Protected constructor.
+ */
+IOKitInputDevice::
+IOKitInputDevice(IOHIDDeviceRef device) :
+  _device(device),
+  _hat_element(nullptr),
+  _pointer_x(nullptr),
+  _pointer_y(nullptr),
+  _scroll_wheel(nullptr),
+  _pointer_x_timestamp(0),
+  _pointer_y_timestamp(0),
+  _scroll_wheel_timestamp(0) {
+  nassertv(device);
+
+  char buffer[4096];
+
+  CFStringRef name = (CFStringRef)IOHIDDeviceGetProperty(device, CFSTR(kIOHIDProductKey));
+  if (name) {
+    buffer[0] = 0;
+    CFStringGetCString(name, buffer, sizeof(buffer), kCFStringEncodingUTF8);
+
+    // Strip trailing spaces.
+    size_t len = strlen(buffer);
+    while (isspace(buffer[len - 1])) {
+      --len;
+    }
+    _name.assign(buffer, len);
+  }
+
+  CFStringRef mfg = (CFStringRef)IOHIDDeviceGetProperty(device, CFSTR(kIOHIDManufacturerKey));
+  if (mfg) {
+    CFStringGetCString(mfg, buffer, sizeof(buffer), kCFStringEncodingUTF8);
+    _manufacturer = buffer;
+  }
+
+  CFStringRef serial = (CFStringRef)IOHIDDeviceGetProperty(device, CFSTR(kIOHIDSerialNumberKey));
+  if (serial) {
+    CFStringGetCString(serial, buffer, sizeof(buffer), kCFStringEncodingUTF8);
+    _serial_number = buffer;
+  }
+
+  CFNumberRef vendor = (CFNumberRef)IOHIDDeviceGetProperty(device, CFSTR(kIOHIDVendorIDKey));
+  if (vendor) {
+    int32_t value = 0;
+    CFNumberGetValue(vendor, kCFNumberSInt32Type, &value);
+    _vendor_id = (unsigned short)value;
+  }
+
+  CFNumberRef product = (CFNumberRef)IOHIDDeviceGetProperty(device, CFSTR(kIOHIDProductIDKey));
+  if (product) {
+    int32_t value = 0;
+    CFNumberGetValue(product, kCFNumberSInt32Type, &value);
+    _product_id = (unsigned short)value;
+  }
+
+  if (IOHIDDeviceConformsTo(device, kHIDPage_GenericDesktop, kHIDUsage_GD_Mouse)) {
+    _device_class = DeviceClass::mouse;
+  } else if (IOHIDDeviceConformsTo(device, kHIDPage_GenericDesktop, kHIDUsage_GD_Keyboard)) {
+    _device_class = DeviceClass::keyboard;
+  } else if (IOHIDDeviceConformsTo(device, kHIDPage_GenericDesktop, kHIDUsage_GD_GamePad)) {
+    _device_class = DeviceClass::gamepad;
+  } else if (IOHIDDeviceConformsTo(device, kHIDPage_Simulation, kHIDUsage_Sim_FlightStick)) {
+    _device_class = DeviceClass::flight_stick;
+  } else if (IOHIDDeviceConformsTo(device, kHIDPage_Simulation, kHIDUsage_GD_Joystick)) {
+    _device_class = DeviceClass::flight_stick;
+  } else if (_vendor_id == 0x044f && _product_id == 0xb108) {
+    // T.Flight Hotas X
+    _device_class = DeviceClass::flight_stick;
+  } else if (_vendor_id == 0x046d &&
+      (_product_id == 0xc623 ||
+       _product_id == 0xc625 ||
+       _product_id == 0xc626 ||
+       _product_id == 0xc627 ||
+       _product_id == 0xc628 ||
+       _product_id == 0xc629 ||
+       _product_id == 0xc62b)) {
+    // 3Dconnexion SpaceNavigator and friends.
+    _device_class = DeviceClass::spatial_mouse;
+  } else if (_name == "usb gamepad") {
+    _device_class = DeviceClass::gamepad;
+  }
+
+  CFArrayRef elements = IOHIDDeviceCopyMatchingElements(device, nullptr, 0);
+  CFIndex count = CFArrayGetCount(elements);
+  for (CFIndex i = 0; i < count; ++i) {
+    IOHIDElementRef element = (IOHIDElementRef)CFArrayGetValueAtIndex(elements, i);
+    parse_element(element);
+  }
+  CFRelease(elements);
+
+  if (_hat_element != nullptr) {
+    _hat_left_button = (int)_buttons.size();
+    _buttons.push_back(ButtonState(GamepadButton::hat_left()));
+    _buttons.push_back(ButtonState(GamepadButton::hat_right()));
+    _buttons.push_back(ButtonState(GamepadButton::hat_down()));
+    _buttons.push_back(ButtonState(GamepadButton::hat_up()));
+  }
+
+  if (_pointer_x != nullptr && _pointer_y != nullptr) {
+    enable_feature(Feature::pointer);
+    add_pointer(PointerType::unknown, 0);
+  }
+
+  _is_connected = true;
+  IOHIDDeviceRegisterRemovalCallback(device, removal_callback, this);
+}
+
+/**
+ *
+ */
+IOKitInputDevice::
+~IOKitInputDevice() {
+}
+
+/**
+ * The nonstatic version of on_remove_device.
+ */
+void IOKitInputDevice::
+on_remove() {
+  {
+    LightMutexHolder holder(_lock);
+    if (!_is_connected) {
+      return;
+    }
+    _is_connected = false;
+  }
+
+  if (device_cat.is_debug()) {
+    device_cat.debug()
+      << "Removed input device " << *this << "\n";
+  }
+
+  IOHIDDeviceClose(_device, kIOHIDOptionsTypeNone);
+
+  InputDeviceManager *mgr = InputDeviceManager::get_global_ptr();
+  nassertv(mgr != nullptr);
+  mgr->remove_device(this);
+}
+
+/**
+ *
+ */
+void IOKitInputDevice::
+parse_element(IOHIDElementRef element) {
+  ButtonHandle handle = ButtonHandle::none();
+  Axis axis = Axis::none;
+  uint32_t page = IOHIDElementGetUsagePage(element);
+  uint32_t usage = IOHIDElementGetUsage(element);
+
+  switch (IOHIDElementGetType(element)) {
+  case kIOHIDElementTypeInput_Misc:
+    switch (page) {
+    case kHIDPage_GenericDesktop:
+      switch (usage) {
+      case kHIDUsage_GD_X:
+        if (_device_class == DeviceClass::gamepad) {
+          axis = Axis::left_x;
+        } else if (_device_class == DeviceClass::flight_stick) {
+          axis = Axis::roll;
+        } else if (_device_class == DeviceClass::mouse) {
+          _pointer_x = element;
+          return;
+        } else {
+          axis = Axis::x;
+        }
+        break;
+      case kHIDUsage_GD_Y:
+        if (_device_class == DeviceClass::gamepad) {
+          axis = Axis::left_y;
+        } else if (_device_class == DeviceClass::flight_stick) {
+          axis = Axis::pitch;
+        } else if (_device_class == DeviceClass::mouse) {
+          _pointer_y = element;
+          return;
+        } else {
+          axis = Axis::y;
+        }
+        break;
+      case kHIDUsage_GD_Z:
+        if (_device_class == DeviceClass::gamepad) {
+          axis = Axis::left_trigger;
+        } else if (_device_class == DeviceClass::flight_stick) {
+          axis = Axis::throttle;
+        } else {
+          axis = Axis::z;
+        }
+        break;
+      case kHIDUsage_GD_Rx:
+        if (_device_class == DeviceClass::gamepad) {
+          axis = Axis::right_x;
+        } else {
+          axis = Axis::pitch;
+        }
+        break;
+      case kHIDUsage_GD_Ry:
+        if (_device_class == DeviceClass::gamepad) {
+          axis = Axis::right_y;
+        } else {
+          axis = Axis::roll;
+        }
+        break;
+      case kHIDUsage_GD_Rz:
+        if (_device_class == DeviceClass::gamepad) {
+          axis = Axis::right_trigger;
+        } else {
+          axis = Axis::yaw;
+        }
+        break;
+      case kHIDUsage_GD_Slider:
+        axis = Axis::rudder;
+        break;
+      case kHIDUsage_GD_Dial:
+        break;
+      case kHIDUsage_GD_Wheel:
+        _scroll_wheel = element;
+        return;
+      case kHIDUsage_GD_Hatswitch:
+        _hat_element = element;
+        return;
+      case kHIDUsage_GD_DPadUp:
+        handle = GamepadButton::dpad_up();
+        break;
+      case kHIDUsage_GD_DPadDown:
+        handle = GamepadButton::dpad_down();
+        break;
+      case kHIDUsage_GD_DPadRight:
+        handle = GamepadButton::dpad_right();
+        break;
+      case kHIDUsage_GD_DPadLeft:
+        handle = GamepadButton::dpad_left();
+        break;
+      case 0xffffffffu:
+        return;
+      default:
+        break;
+      }
+      break;
+
+    case kHIDPage_Simulation:
+      switch (usage) {
+      case kHIDUsage_Sim_Rudder:
+        axis = Axis::rudder;
+        break;
+      case kHIDUsage_Sim_Throttle:
+        axis = Axis::throttle;
+        break;
+      case kHIDUsage_Sim_Accelerator:
+        axis = Axis::accelerator;
+        break;
+      case kHIDUsage_Sim_Brake:
+        axis = Axis::brake;
+        break;
+      }
+      break;
+    }
+    if (axis != Axis::none) {
+      int min = IOHIDElementGetLogicalMin(element);
+      int max = IOHIDElementGetLogicalMax(element);
+      if (_vendor_id == 0x044f && _product_id == 0xb108 && axis == Axis::throttle) {
+        // T.Flight Hotas X throttle is reversed and can go backwards.
+        add_axis(axis, max, min, true);
+      } else if (axis == Axis::yaw || axis == Axis::rudder || axis == Axis::left_y || axis == Axis::right_y ||
+                 (_device_class == DeviceClass::spatial_mouse && (axis == Axis::y || axis == Axis::z || axis == Axis::roll))) {
+        // We'd like to reverse the Y axis to match the XInput behavior.
+        // We also reverse yaw to obey the right-hand rule.
+        add_axis(axis, max, min);
+      } else {
+        add_axis(axis, min, max);
+      }
+
+      _analog_elements.push_back(element);
+    }
+    break;
+
+  case kIOHIDElementTypeInput_Button:
+    switch (page) {
+    case kHIDPage_GenericDesktop:
+      switch (usage) {
+      case kHIDUsage_GD_DPadUp:
+        handle = GamepadButton::dpad_up();
+        break;
+      case kHIDUsage_GD_DPadDown:
+        handle = GamepadButton::dpad_down();
+        break;
+      case kHIDUsage_GD_DPadRight:
+        handle = GamepadButton::dpad_right();
+        break;
+      case kHIDUsage_GD_DPadLeft:
+        handle = GamepadButton::dpad_left();
+        break;
+      default:
+        break;
+      }
+      break;
+
+    case kHIDPage_KeyboardOrKeypad:
+      switch (usage) {
+      case kHIDUsage_KeyboardA:
+      case kHIDUsage_KeyboardB:
+      case kHIDUsage_KeyboardC:
+      case kHIDUsage_KeyboardD:
+      case kHIDUsage_KeyboardE:
+      case kHIDUsage_KeyboardF:
+      case kHIDUsage_KeyboardG:
+      case kHIDUsage_KeyboardH:
+      case kHIDUsage_KeyboardI:
+      case kHIDUsage_KeyboardJ:
+      case kHIDUsage_KeyboardK:
+      case kHIDUsage_KeyboardL:
+      case kHIDUsage_KeyboardM:
+      case kHIDUsage_KeyboardN:
+      case kHIDUsage_KeyboardO:
+      case kHIDUsage_KeyboardP:
+      case kHIDUsage_KeyboardQ:
+      case kHIDUsage_KeyboardR:
+      case kHIDUsage_KeyboardS:
+      case kHIDUsage_KeyboardT:
+      case kHIDUsage_KeyboardU:
+      case kHIDUsage_KeyboardV:
+      case kHIDUsage_KeyboardW:
+      case kHIDUsage_KeyboardX:
+      case kHIDUsage_KeyboardY:
+      case kHIDUsage_KeyboardZ:
+        handle = KeyboardButton::ascii_key('a' + (usage - kHIDUsage_KeyboardA));
+        break;
+      case kHIDUsage_Keyboard1:
+        handle = KeyboardButton::ascii_key('1');
+        break;
+      case kHIDUsage_Keyboard2:
+        handle = KeyboardButton::ascii_key('2');
+        break;
+      case kHIDUsage_Keyboard3:
+        handle = KeyboardButton::ascii_key('3');
+        break;
+      case kHIDUsage_Keyboard4:
+        handle = KeyboardButton::ascii_key('4');
+        break;
+      case kHIDUsage_Keyboard5:
+        handle = KeyboardButton::ascii_key('5');
+        break;
+      case kHIDUsage_Keyboard6:
+        handle = KeyboardButton::ascii_key('6');
+        break;
+      case kHIDUsage_Keyboard7:
+        handle = KeyboardButton::ascii_key('7');
+        break;
+      case kHIDUsage_Keyboard8:
+        handle = KeyboardButton::ascii_key('8');
+        break;
+      case kHIDUsage_Keyboard9:
+        handle = KeyboardButton::ascii_key('9');
+        break;
+      case kHIDUsage_Keyboard0:
+        handle = KeyboardButton::ascii_key('0');
+        break;
+      case kHIDUsage_KeyboardReturnOrEnter:
+        handle = KeyboardButton::enter();
+        break;
+      case kHIDUsage_KeyboardEscape:
+        handle = KeyboardButton::escape();
+        break;
+      case kHIDUsage_KeyboardDeleteOrBackspace:
+        handle = KeyboardButton::backspace();
+        break;
+      case kHIDUsage_KeyboardTab:
+        handle = KeyboardButton::tab();
+        break;
+      case kHIDUsage_KeyboardSpacebar:
+        handle = KeyboardButton::ascii_key(' ');
+        break;
+      case kHIDUsage_KeyboardHyphen:
+        handle = KeyboardButton::ascii_key('-');
+        break;
+      case kHIDUsage_KeyboardEqualSign:
+        handle = KeyboardButton::ascii_key('=');
+        break;
+      case kHIDUsage_KeyboardOpenBracket:
+        handle = KeyboardButton::ascii_key('[');
+        break;
+      case kHIDUsage_KeyboardCloseBracket:
+        handle = KeyboardButton::ascii_key(']');
+        break;
+      case kHIDUsage_KeyboardBackslash:
+        handle = KeyboardButton::ascii_key('\\');
+        break;
+      case kHIDUsage_KeyboardNonUSPound:
+        handle = KeyboardButton::ascii_key('$');
+        break;
+      case kHIDUsage_KeyboardSemicolon:
+        handle = KeyboardButton::ascii_key(';');
+        break;
+      case kHIDUsage_KeyboardQuote:
+        handle = KeyboardButton::ascii_key('\'');
+        break;
+      case kHIDUsage_KeyboardGraveAccentAndTilde:
+        handle = KeyboardButton::ascii_key('`');
+        break;
+      case kHIDUsage_KeyboardComma:
+        handle = KeyboardButton::ascii_key(',');
+        break;
+      case kHIDUsage_KeyboardPeriod:
+        handle = KeyboardButton::ascii_key('.');
+        break;
+      case kHIDUsage_KeyboardSlash:
+        handle = KeyboardButton::ascii_key('/');
+        break;
+      case kHIDUsage_KeyboardCapsLock:
+        handle = KeyboardButton::caps_lock();
+        break;
+      case kHIDUsage_KeyboardF1:
+        handle = KeyboardButton::f1();
+        break;
+      case kHIDUsage_KeyboardF2:
+        handle = KeyboardButton::f2();
+        break;
+      case kHIDUsage_KeyboardF3:
+        handle = KeyboardButton::f3();
+        break;
+      case kHIDUsage_KeyboardF4:
+        handle = KeyboardButton::f4();
+        break;
+      case kHIDUsage_KeyboardF5:
+        handle = KeyboardButton::f5();
+        break;
+      case kHIDUsage_KeyboardF6:
+        handle = KeyboardButton::f6();
+        break;
+      case kHIDUsage_KeyboardF7:
+        handle = KeyboardButton::f7();
+        break;
+      case kHIDUsage_KeyboardF8:
+        handle = KeyboardButton::f8();
+        break;
+      case kHIDUsage_KeyboardF9:
+        handle = KeyboardButton::f9();
+        break;
+      case kHIDUsage_KeyboardF10:
+        handle = KeyboardButton::f10();
+        break;
+      case kHIDUsage_KeyboardF11:
+        handle = KeyboardButton::f11();
+        break;
+      case kHIDUsage_KeyboardF12:
+        handle = KeyboardButton::f12();
+        break;
+      case kHIDUsage_KeyboardPrintScreen:
+        handle = KeyboardButton::print_screen();
+        break;
+      case kHIDUsage_KeyboardScrollLock:
+        handle = KeyboardButton::scroll_lock();
+        break;
+      case kHIDUsage_KeyboardPause:
+        handle = KeyboardButton::pause();
+        break;
+      case kHIDUsage_KeyboardInsert:
+        handle = KeyboardButton::insert();
+        break;
+      case kHIDUsage_KeyboardHome:
+        handle = KeyboardButton::home();
+        break;
+      case kHIDUsage_KeyboardPageUp:
+        handle = KeyboardButton::page_up();
+        break;
+      case kHIDUsage_KeyboardDeleteForward:
+        handle = KeyboardButton::del();
+        break;
+      case kHIDUsage_KeyboardEnd:
+        handle = KeyboardButton::end();
+        break;
+      case kHIDUsage_KeyboardPageDown:
+        handle = KeyboardButton::page_down();
+        break;
+      case kHIDUsage_KeyboardRightArrow:
+        handle = KeyboardButton::right();
+        break;
+      case kHIDUsage_KeyboardLeftArrow:
+        handle = KeyboardButton::left();
+        break;
+      case kHIDUsage_KeyboardDownArrow:
+        handle = KeyboardButton::down();
+        break;
+      case kHIDUsage_KeyboardUpArrow:
+        handle = KeyboardButton::up();
+        break;
+      case kHIDUsage_KeypadNumLock:
+        handle = KeyboardButton::num_lock();
+        break;
+      case kHIDUsage_KeypadSlash:
+        handle = KeyboardButton::ascii_key('/');
+        break;
+      case kHIDUsage_KeypadAsterisk:
+        handle = KeyboardButton::ascii_key('*');
+        break;
+      case kHIDUsage_KeypadHyphen:
+        handle = KeyboardButton::ascii_key('-');
+        break;
+      case kHIDUsage_KeypadPlus:
+        handle = KeyboardButton::ascii_key('+');
+        break;
+      case kHIDUsage_KeypadEnter:
+        handle = KeyboardButton::enter();
+        break;
+      case kHIDUsage_Keypad1:
+        handle = KeyboardButton::ascii_key('1');
+        break;
+      case kHIDUsage_Keypad2:
+        handle = KeyboardButton::ascii_key('2');
+        break;
+      case kHIDUsage_Keypad3:
+        handle = KeyboardButton::ascii_key('3');
+        break;
+      case kHIDUsage_Keypad4:
+        handle = KeyboardButton::ascii_key('4');
+        break;
+      case kHIDUsage_Keypad5:
+        handle = KeyboardButton::ascii_key('5');
+        break;
+      case kHIDUsage_Keypad6:
+        handle = KeyboardButton::ascii_key('6');
+        break;
+      case kHIDUsage_Keypad7:
+        handle = KeyboardButton::ascii_key('7');
+        break;
+      case kHIDUsage_Keypad8:
+        handle = KeyboardButton::ascii_key('8');
+        break;
+      case kHIDUsage_Keypad9:
+        handle = KeyboardButton::ascii_key('9');
+        break;
+      case kHIDUsage_Keypad0:
+        handle = KeyboardButton::ascii_key('0');
+        break;
+      case kHIDUsage_KeypadPeriod:
+        handle = KeyboardButton::ascii_key('.');
+        break;
+      case kHIDUsage_KeyboardNonUSBackslash:
+        handle = KeyboardButton::ascii_key('\\');
+        break;
+      case kHIDUsage_KeypadEqualSign:
+        handle = KeyboardButton::ascii_key('=');
+        break;
+      case kHIDUsage_KeyboardF13:
+        handle = KeyboardButton::f13();
+        break;
+      case kHIDUsage_KeyboardF14:
+        handle = KeyboardButton::f14();
+        break;
+      case kHIDUsage_KeyboardF15:
+        handle = KeyboardButton::f15();
+        break;
+      case kHIDUsage_KeyboardF16:
+        handle = KeyboardButton::f16();
+        break;
+      case kHIDUsage_KeyboardExecute:
+        break;
+      case kHIDUsage_KeyboardHelp:
+        handle = KeyboardButton::help();
+        break;
+      case kHIDUsage_KeyboardMenu:
+        handle = KeyboardButton::menu();
+        break;
+      case kHIDUsage_KeypadComma:
+        handle = KeyboardButton::ascii_key(',');
+        break;
+      case kHIDUsage_KeypadEqualSignAS400:
+        handle = KeyboardButton::ascii_key('=');
+        break;
+      case kHIDUsage_KeyboardReturn:
+        handle = KeyboardButton::enter();
+        break;
+      case kHIDUsage_KeyboardLeftControl:
+        handle = KeyboardButton::lcontrol();
+        break;
+      case kHIDUsage_KeyboardLeftShift:
+        handle = KeyboardButton::lshift();
+        break;
+      case kHIDUsage_KeyboardLeftAlt:
+        handle = KeyboardButton::lalt();
+        break;
+      case kHIDUsage_KeyboardLeftGUI:
+        handle = KeyboardButton::lmeta();
+        break;
+      case kHIDUsage_KeyboardRightControl:
+        handle = KeyboardButton::rcontrol();
+        break;
+      case kHIDUsage_KeyboardRightShift:
+        handle = KeyboardButton::rshift();
+        break;
+      case kHIDUsage_KeyboardRightAlt:
+        handle = KeyboardButton::ralt();
+        break;
+      case kHIDUsage_KeyboardRightGUI:
+        handle = KeyboardButton::rmeta();
+        break;
+      default:
+        break;
+      }
+      break;
+
+    case kHIDPage_Button:
+      if (_device_class == DeviceClass::gamepad) {
+        if (_vendor_id == 0x0810 && _product_id == 0xe501) {
+          // SNES-style USB gamepad
+          static const ButtonHandle gamepad_buttons[] = {
+            ButtonHandle::none(),
+            GamepadButton::face_x(),
+            GamepadButton::face_a(),
+            GamepadButton::face_b(),
+            GamepadButton::face_y(),
+            GamepadButton::lshoulder(),
+            GamepadButton::rshoulder(),
+            ButtonHandle::none(),
+            ButtonHandle::none(),
+            GamepadButton::back(),
+            GamepadButton::start(),
+          };
+          if (usage < sizeof(gamepad_buttons) / sizeof(ButtonHandle)) {
+            handle = gamepad_buttons[usage];
+          }
+        } else {
+          // These seem to be the button mappings exposed by the 360Controller
+          // driver.  I don't know if other drivers do the same thing at all.
+          static const ButtonHandle gamepad_buttons[] = {
+            ButtonHandle::none(),
+            GamepadButton::face_a(),
+            GamepadButton::face_b(),
+            GamepadButton::face_x(),
+            GamepadButton::face_y(),
+            GamepadButton::lshoulder(),
+            GamepadButton::rshoulder(),
+            GamepadButton::lstick(),
+            GamepadButton::rstick(),
+            GamepadButton::start(),
+            GamepadButton::back(),
+            GamepadButton::guide(),
+            GamepadButton::dpad_up(),
+            GamepadButton::dpad_down(),
+            GamepadButton::dpad_left(),
+            GamepadButton::dpad_right(),
+          };
+          if (usage < sizeof(gamepad_buttons) / sizeof(ButtonHandle)) {
+            handle = gamepad_buttons[usage];
+          }
+        }
+      } else if (_device_class == DeviceClass::flight_stick) {
+        if (usage > 0) {
+          handle = GamepadButton::joystick(usage - 1);
+        }
+      } else if (_device_class == DeviceClass::mouse) {
+        // In Panda, wheel and right button are flipped around...
+        int button = (usage == 2 || usage == 3) ? (4 - usage) : (usage - 1);
+        handle = MouseButton::button(button);
+      }
+      break;
+    }
+    _buttons.push_back(ButtonState(handle));
+    _button_elements.push_back(element);
+    break;
+
+  case kIOHIDElementTypeInput_Axis:
+    break;
+
+  case kIOHIDElementTypeInput_ScanCodes:
+    break;
+
+  case kIOHIDElementTypeOutput:
+    break;
+
+  case kIOHIDElementTypeFeature:
+    break;
+
+  case kIOHIDElementTypeCollection:
+    {
+      // This doesn't seem to be necessary and instead leads to duplication of
+      // axes and buttons.
+      /*
+      CFArrayRef children = IOHIDElementGetChildren(element);
+      CFIndex count = CFArrayGetCount(children);
+      for (CFIndex i = 0; i < count; ++i) {
+        IOHIDElementRef element = (IOHIDElementRef)CFArrayGetValueAtIndex(children, i);
+        parse_element(element, depth + 2);
+      }*/
+    }
+    break;
+  }
+}
+
+/**
+ * Polls the input device for new activity, to ensure it contains the latest
+ * events.  This will only have any effect for some types of input devices;
+ * others may be updated automatically, and this method will be a no-op.
+ */
+void IOKitInputDevice::
+do_poll() {
+  for (size_t i = 0; i < _button_elements.size(); ++i) {
+    IOHIDValueRef value_ref;
+    if (!_button_elements[i]) continue;
+    if (IOHIDDeviceGetValue(_device, _button_elements[i], &value_ref) == kIOReturnSuccess) {
+      int value = IOHIDValueGetIntegerValue(value_ref);
+      button_changed(i, value != 0);
+    }
+  }
+
+  for (size_t i = 0; i < _analog_elements.size(); ++i) {
+    IOHIDValueRef value_ref;
+    if (IOHIDDeviceGetValue(_device, _analog_elements[i], &value_ref) == kIOReturnSuccess) {
+      int value = IOHIDValueGetIntegerValue(value_ref);
+      axis_changed(i, value);
+    }
+  }
+
+  if (_hat_element != nullptr) {
+    IOHIDValueRef value_ref;
+    if (IOHIDDeviceGetValue(_device, _hat_element, &value_ref) == kIOReturnSuccess) {
+      int value = IOHIDValueGetIntegerValue(value_ref);
+      button_changed(_hat_left_button + 0, value >= 5 && value <= 7); // left
+      button_changed(_hat_left_button + 1, value >= 1 && value <= 3); // right
+      button_changed(_hat_left_button + 2, value >= 3 && value <= 5); // down
+      button_changed(_hat_left_button + 3, value == 7 || value == 0 || value == 1); // up
+    }
+  }
+
+  int x = 0, y = 0;
+  if (_pointer_x != nullptr) {
+    IOHIDValueRef value_ref;
+    if (IOHIDDeviceGetValue(_device, _pointer_x, &value_ref) == kIOReturnSuccess) {
+      uint64_t timestamp = IOHIDValueGetTimeStamp(value_ref);
+      if (timestamp != _pointer_x_timestamp) {
+        x = IOHIDValueGetIntegerValue(value_ref);
+        _pointer_x_timestamp = timestamp;
+      }
+    }
+  }
+  if (_pointer_y != nullptr) {
+    IOHIDValueRef value_ref;
+    if (IOHIDDeviceGetValue(_device, _pointer_y, &value_ref) == kIOReturnSuccess) {
+      uint64_t timestamp = IOHIDValueGetTimeStamp(value_ref);
+      if (timestamp != _pointer_y_timestamp) {
+        y = IOHIDValueGetIntegerValue(value_ref);
+        _pointer_y_timestamp = timestamp;
+      }
+    }
+  }
+  if (x != 0 || y != 0) {
+    pointer_moved(0, x, y, ClockObject::get_global_clock()->get_frame_time());
+  }
+
+  // Do we have a scroll wheel axis?
+  if (_scroll_wheel != nullptr) {
+    IOHIDValueRef value_ref;
+    if (IOHIDDeviceGetValue(_device, _scroll_wheel, &value_ref) == kIOReturnSuccess) {
+      uint64_t timestamp = IOHIDValueGetTimeStamp(value_ref);
+      if (timestamp != _scroll_wheel_timestamp) {
+        int value = IOHIDValueGetIntegerValue(value_ref);
+        if (value != 0) {
+          // Just fire off a rapid down/up event.
+          double time = ClockObject::get_global_clock()->get_frame_time();
+          ButtonHandle handle = (value > 0) ? MouseButton::wheel_up() : MouseButton::wheel_down();
+          _button_events->add_event(ButtonEvent(handle, ButtonEvent::T_down, time));
+          _button_events->add_event(ButtonEvent(handle, ButtonEvent::T_up, time));
+        }
+        _scroll_wheel_timestamp = timestamp;
+      }
+    }
+  }
+}
+
+#endif  // __APPLE__

+ 55 - 0
panda/src/device/ioKitInputDevice.h

@@ -0,0 +1,55 @@
+/**
+ * PANDA 3D SOFTWARE
+ * Copyright (c) Carnegie Mellon University.  All rights reserved.
+ *
+ * All use of this software is subject to the terms of the revised BSD
+ * license.  You should have received a copy of this license along
+ * with this source code in a file named "LICENSE."
+ *
+ * @file ioKitInputDevice.h
+ * @author rdb
+ * @date 2017-12-21
+ */
+
+#ifndef IOKITINPUTDEVICE_H
+#define IOKITINPUTDEVICE_H
+
+#include "pandabase.h"
+#include "inputDevice.h"
+
+#if defined(__APPLE__) && !defined(CPPPARSER)
+
+#include <IOKit/hid/IOHIDDevice.h>
+
+/**
+ * This implementation uses the IOKit HID code that was introduced with macOS
+ * 10.5 to interface with USB HID devices.
+ */
+class EXPCL_PANDA_DEVICE IOKitInputDevice final : public InputDevice {
+public:
+  IOKitInputDevice(IOHIDDeviceRef device);
+  ~IOKitInputDevice();
+
+  void on_remove();
+
+private:
+  void parse_element(IOHIDElementRef element);
+
+  virtual void do_poll();
+
+  IOHIDDeviceRef _device;
+  pvector<IOHIDElementRef> _button_elements;
+  pvector<IOHIDElementRef> _analog_elements;
+  IOHIDElementRef _hat_element;
+  int _hat_left_button;
+  IOHIDElementRef _pointer_x;
+  IOHIDElementRef _pointer_y;
+  IOHIDElementRef _scroll_wheel;
+  uint64_t _pointer_x_timestamp;
+  uint64_t _pointer_y_timestamp;
+  uint64_t _scroll_wheel_timestamp;
+};
+
+#endif  // __APPLE__
+
+#endif

+ 93 - 0
panda/src/device/ioKitInputDeviceManager.cxx

@@ -0,0 +1,93 @@
+/**
+ * PANDA 3D SOFTWARE
+ * Copyright (c) Carnegie Mellon University.  All rights reserved.
+ *
+ * All use of this software is subject to the terms of the revised BSD
+ * license.  You should have received a copy of this license along
+ * with this source code in a file named "LICENSE."
+ *
+ * @file ioKitInputDeviceManager.cxx
+ * @author rdb
+ * @date 2018-02-04
+ */
+
+#include "ioKitInputDeviceManager.h"
+#include "ioKitInputDevice.h"
+
+#if defined(__APPLE__) && !defined(CPPPARSER)
+
+/**
+ * Initializes the input device manager by scanning which devices are currently
+ * connected and setting up any platform-dependent structures necessary for
+ * listening for future device connect events.
+ */
+IOKitInputDeviceManager::
+IOKitInputDeviceManager() {
+  _hid_manager = IOHIDManagerCreate(kCFAllocatorDefault, kIOHIDOptionsTypeNone);
+  if (!_hid_manager) {
+    device_cat.error()
+      << "Failed to create an IOHIDManager.\n";
+    return;
+  }
+
+  // The types of devices we're interested in.
+  int page = kHIDPage_GenericDesktop;
+  int usages[] = {kHIDUsage_GD_GamePad,
+                  kHIDUsage_GD_Joystick,
+                  kHIDUsage_GD_Mouse,
+                  kHIDUsage_GD_Keyboard,
+                  kHIDUsage_GD_MultiAxisController, 0};
+  int *usage = usages;
+
+  // This giant mess is necessary to create an array of match dictionaries
+  // that will match the devices we're interested in.
+  CFMutableArrayRef match = CFArrayCreateMutable(kCFAllocatorDefault, 0, &kCFTypeArrayCallBacks);
+  nassertv(match);
+  while (*usage) {
+    CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorDefault, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
+    CFNumberRef page_ref = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &page);
+    CFDictionarySetValue(dict, CFSTR(kIOHIDDeviceUsagePageKey), page_ref);
+    CFRelease(page_ref);
+    CFNumberRef usage_ref = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, usage);
+    CFDictionarySetValue(dict, CFSTR(kIOHIDDeviceUsageKey), usage_ref);
+    CFRelease(usage_ref);
+    CFArrayAppendValue(match, dict);
+    CFRelease(dict);
+    ++usage;
+  }
+  IOHIDManagerSetDeviceMatchingMultiple(_hid_manager, match);
+  CFRelease(match);
+
+  IOHIDManagerRegisterDeviceMatchingCallback(_hid_manager, on_match_device, this);
+  IOHIDManagerScheduleWithRunLoop(_hid_manager, CFRunLoopGetMain(), kCFRunLoopCommonModes);
+  IOHIDManagerOpen(_hid_manager, kIOHIDOptionsTypeNone);
+}
+
+/**
+ * Closes any resources that the device manager was using to listen for events.
+ */
+IOKitInputDeviceManager::
+~IOKitInputDeviceManager() {
+  IOHIDManagerUnscheduleFromRunLoop(_hid_manager, CFRunLoopGetMain(), kCFRunLoopCommonModes);
+  IOHIDManagerClose(_hid_manager, kIOHIDOptionsTypeNone);
+  CFRelease(_hid_manager);
+}
+
+/**
+ * Called by IOKit when an input device matching our filters has been found.
+ */
+void IOKitInputDeviceManager::
+on_match_device(void *ctx, IOReturn result, void *sender, IOHIDDeviceRef device) {
+  InputDeviceManager *mgr = (InputDeviceManager *)ctx;
+  nassertv(mgr != nullptr);
+  nassertv(device);
+
+  PT(InputDevice) input_device = new IOKitInputDevice(device);
+  if (device_cat.is_debug()) {
+    device_cat.debug()
+      << "Discovered input device " << *input_device << "\n";
+  }
+  mgr->add_device(input_device);
+}
+
+#endif

+ 40 - 0
panda/src/device/ioKitInputDeviceManager.h

@@ -0,0 +1,40 @@
+/**
+ * PANDA 3D SOFTWARE
+ * Copyright (c) Carnegie Mellon University.  All rights reserved.
+ *
+ * All use of this software is subject to the terms of the revised BSD
+ * license.  You should have received a copy of this license along
+ * with this source code in a file named "LICENSE."
+ *
+ * @file ioKitInputDeviceManager.h
+ * @author rdb
+ * @date 2018-02-04
+ */
+
+#ifndef IOKITINPUTDEVICEMANAGER_H
+#define IOKITINPUTDEVICEMANAGER_H
+
+#include "inputDeviceManager.h"
+
+#if defined(__APPLE__) && !defined(CPPPARSER)
+#include <IOKit/hid/IOHIDManager.h>
+
+/**
+ * The macOS implementation of InputDeviceManager.
+ */
+class EXPCL_PANDA_DEVICE IOKitInputDeviceManager final : public InputDeviceManager {
+protected:
+  IOKitInputDeviceManager();
+  ~IOKitInputDeviceManager();
+
+protected:
+  IOHIDManagerRef _hid_manager;
+
+  static void on_match_device(void *ctx, IOReturn result, void *sender, IOHIDDeviceRef device);
+
+  friend class InputDeviceManager;
+};
+
+#endif
+
+#endif

+ 295 - 0
panda/src/device/linuxInputDeviceManager.cxx

@@ -0,0 +1,295 @@
+/**
+ * PANDA 3D SOFTWARE
+ * Copyright (c) Carnegie Mellon University.  All rights reserved.
+ *
+ * All use of this software is subject to the terms of the revised BSD
+ * license.  You should have received a copy of this license along
+ * with this source code in a file named "LICENSE."
+ *
+ * @file linuxInputDeviceManager.cxx
+ * @author rdb
+ * @date 2018-01-25
+ */
+
+#include "inputDeviceManager.h"
+#include "throw_event.h"
+
+#ifdef PHAVE_LINUX_INPUT_H
+
+#include "evdevInputDevice.h"
+#include "linuxJoystickDevice.h"
+
+#include <dirent.h>
+#include <fcntl.h>
+#include <sys/inotify.h>
+#include <sys/ioctl.h>
+
+/**
+ * Initializes the device manager by scanning which devices are currently
+ * connected and setting up any platform-dependent structures necessary for
+ * listening for future device connect events.
+ */
+LinuxInputDeviceManager::
+LinuxInputDeviceManager() {
+  // Use inotify to watch /dev/input for hotplugging of devices.
+  _inotify_fd = inotify_init();
+  fcntl(_inotify_fd, F_SETFL, O_NONBLOCK);
+  fcntl(_inotify_fd, F_SETFD, FD_CLOEXEC);
+
+  if (_inotify_fd < 0) {
+    device_cat.error()
+      << "Error initializing inotify: " << strerror(errno) << "\n";
+
+  } else if (inotify_add_watch(_inotify_fd, "/dev/input", IN_CREATE | IN_ATTRIB | IN_DELETE) < 0) {
+    device_cat.error()
+      << "Error adding inotify watch on /dev/input: " << strerror(errno) << "\n";
+  }
+
+  // Scan /dev/input for a list of input devices.
+  DIR *dir = opendir("/dev/input");
+  if (dir) {
+    std::vector<size_t> indices;
+    dirent *entry = readdir(dir);
+    while (entry != nullptr) {
+      size_t index;
+      if (entry->d_type == DT_CHR && sscanf(entry->d_name, "event%zd", &index) == 1) {
+        indices.push_back(index);
+      }
+      entry = readdir(dir);
+    }
+    closedir(dir);
+
+    // We'll want to sort the devices by index, since the order may be
+    // meaningful (eg. for the Xbox wireless receiver).
+    std::sort(indices.begin(), indices.end());
+    _evdev_devices.resize(indices.back() + 1, nullptr);
+
+    for (size_t index : indices) {
+      consider_add_evdev_device(index);
+    }
+  } else {
+    device_cat.error()
+      << "Error opening directory /dev/input: " << strerror(errno) << "\n";
+    return;
+  }
+}
+
+/**
+ * Closes any resources that the device manager was using to listen for events.
+ */
+LinuxInputDeviceManager::
+~LinuxInputDeviceManager() {
+  if (_inotify_fd >= 0) {
+    close(_inotify_fd);
+    _inotify_fd = -1;
+  }
+}
+
+/**
+ * Checks whether the event device with the given index is accessible, and if
+ * so, adds it.  Returns the device if it was newly connected.
+ *
+ * This is the preferred interface for input devices on Linux.
+ */
+InputDevice *LinuxInputDeviceManager::
+consider_add_evdev_device(size_t ev_index) {
+  if (ev_index < _evdev_devices.size()) {
+    if (_evdev_devices[ev_index] != nullptr) {
+      // We already have this device.  FIXME: probe it and add it to the
+      // list of connected devices?
+      return nullptr;
+    }
+  } else {
+    // Make room to store this index.
+    _evdev_devices.resize(ev_index + 1, nullptr);
+  }
+
+  // Check if we can directly read the event device.
+  char path[64];
+  sprintf(path, "/dev/input/event%zd", ev_index);
+
+  if (access(path, R_OK) == 0) {
+    PT(InputDevice) device = new EvdevInputDevice(this, ev_index);
+    if (device_cat.is_debug()) {
+      device_cat.debug()
+        << "Discovered evdev input device " << *device << "\n";
+    }
+
+    _evdev_devices[ev_index] = device;
+
+    if (device->is_connected()) {
+      _connected_devices.add_device(std::move(device));
+    } else {
+      // Wait for activity on the device before it is considered connected.
+      _inactive_devices.add_device(std::move(device));
+    }
+    return _evdev_devices[ev_index];
+  }
+
+  // Nope.  The permissions haven't been configured to allow it.  Check if this
+  // corresponds to a /dev/input/jsX interface, which has a better chance of
+  // having read permissions set, but doesn't export all of the features
+  // (notably, force feedback).
+
+  // We do this by checking for a js# directory inside the sysfs directory.
+  sprintf(path, "/sys/class/input/event%zd/device", ev_index);
+
+  DIR *dir = opendir(path);
+  if (dir == nullptr) {
+    if (device_cat.is_debug()) {
+      device_cat.debug()
+        << "Error opening directory " << path << ": " << strerror(errno) << "\n";
+    }
+    return nullptr;
+  }
+
+  dirent *entry = readdir(dir);
+  while (entry != nullptr) {
+    size_t js_index;
+    if (sscanf(entry->d_name, "js%zd", &js_index) == 1) {
+      // Yes, we fonud a corresponding js device.  Try adding it.
+      closedir(dir);
+
+      InputDevice *device = consider_add_js_device(js_index);
+      if (device != nullptr && device_cat.is_warning()) {
+        // This seemed to work.  Display a warning to the user indicating
+        // that they might want to configure udev properly.
+        device_cat.warning()
+          << "/dev/input/event" << ev_index << " is not readable, some "
+             "features will be unavailable.\n";
+      }
+      _evdev_devices[ev_index] = device;
+      return device;
+    }
+    entry = readdir(dir);
+  }
+
+  closedir(dir);
+  return nullptr;
+}
+
+/**
+ * Checks whether the joystick device with the given index is accessible, and
+ * if so, adds it.  Returns the device if it was newly connected.
+ *
+ * This is only used on Linux as a fallback interface for when an evdev device
+ * cannot be accessed.
+ */
+InputDevice *LinuxInputDeviceManager::
+consider_add_js_device(size_t js_index) {
+  char path[64];
+  sprintf(path, "/dev/input/js%zd", js_index);
+
+  if (access(path, R_OK) == 0) {
+    PT(LinuxJoystickDevice) device = new LinuxJoystickDevice(this, js_index);
+    if (device_cat.is_debug()) {
+      device_cat.debug()
+        << "Discovered joydev input device " << *device << "\n";
+    }
+    InputDevice *device_p = device.p();
+
+    if (device->is_connected()) {
+      _connected_devices.add_device(std::move(device));
+    } else {
+      // Wait for activity on the device before it is considered connected.
+      _inactive_devices.add_device(std::move(device));
+    }
+    return device_p;
+  }
+
+  return nullptr;
+}
+
+/**
+ * Polls the system to see if there are any new devices.  In some
+ * implementations this is a no-op.
+ */
+void LinuxInputDeviceManager::
+update() {
+  // Check for any devices that may be disconnected and need to be probed in
+  // order to see whether they have been reconnected.
+  InputDeviceSet inactive_devices;
+  {
+    LightMutexHolder holder(_lock);
+    inactive_devices = _inactive_devices;
+  }
+  for (size_t i = 0; i < inactive_devices.size(); ++i) {
+    InputDevice *device = inactive_devices[i];
+    if (device != nullptr && !device->is_connected()) {
+      device->poll();
+    }
+  }
+
+  // We use inotify to tell us whether a device was added, removed, or has
+  // changed permissions to allow us to access it.
+  unsigned int avail = 0;
+  ioctl(_inotify_fd, FIONREAD, &avail);
+  if (avail == 0) {
+    return;
+  }
+
+  char buffer[avail];
+  int n_read = read(_inotify_fd, buffer, avail);
+  if (n_read < 0) {
+    if (errno == EAGAIN || errno == EWOULDBLOCK) {
+      // No data available for now.
+
+    } else {
+      device_cat.error() << "read: " << strerror(errno) << "\n";
+    }
+    return;
+  }
+
+  LightMutexHolder holder(_lock);
+
+  // Iterate over the events in the buffer.
+  char *ptr = buffer;
+  char *end = buffer + avail;
+  while (ptr < end) {
+    inotify_event *event = (inotify_event *)ptr;
+
+    std::string name(event->name);
+
+    if (event->mask & IN_DELETE) {
+      // The device was deleted.  If we have it, remove it.
+
+      size_t index;
+      if (sscanf(event->name, "event%zd", &index) == 1) {
+        // Check if we have this evdev device.  If so, disconnect it.
+        if (index < _evdev_devices.size()) {
+          PT(InputDevice) device = _evdev_devices[index];
+          if (device != nullptr) {
+            device->set_connected(false);
+            _evdev_devices[index] = nullptr;
+            _inactive_devices.remove_device(device);
+            if (_connected_devices.remove_device(device)) {
+              throw_event("disconnect-device", device.p());
+            }
+
+            if (device_cat.is_debug()) {
+              device_cat.debug()
+                << "Removed input device " << *device << "\n";
+            }
+          }
+        }
+      }
+
+    } else if (event->mask & (IN_CREATE | IN_ATTRIB)) {
+      // The device was created, or it was chmodded to be accessible.  We need
+      // to check for the latter since it seems that the device may get the
+      // IN_CREATE event before the driver gets the permissions set properly.
+
+      size_t index;
+      if (sscanf(event->name, "event%zd", &index) == 1) {
+        InputDevice *device = consider_add_evdev_device(index);
+        if (device != nullptr && device->is_connected()) {
+          throw_event("connect-device", device);
+        }
+      }
+    }
+
+    ptr += sizeof(inotify_event) + event->len;
+  }
+}
+
+#endif  // PHAVE_LINUX_INPUT_H

+ 45 - 0
panda/src/device/linuxInputDeviceManager.h

@@ -0,0 +1,45 @@
+/**
+ * PANDA 3D SOFTWARE
+ * Copyright (c) Carnegie Mellon University.  All rights reserved.
+ *
+ * All use of this software is subject to the terms of the revised BSD
+ * license.  You should have received a copy of this license along
+ * with this source code in a file named "LICENSE."
+ *
+ * @file linuxInputDeviceManager.h
+ * @author rdb
+ * @date 2018-01-25
+ */
+
+#ifndef LINUXINPUTDEVICEMANAGER_H
+#define LINUXINPUTDEVICEMANAGER_H
+
+#include "inputDeviceManager.h"
+
+#ifdef PHAVE_LINUX_INPUT_H
+
+/**
+ * This class keeps track of all the devices on a system, and sends out events
+ * when a device has been hot-plugged.
+ */
+class EXPCL_PANDA_DEVICE LinuxInputDeviceManager final : public InputDeviceManager {
+private:
+  LinuxInputDeviceManager();
+  ~LinuxInputDeviceManager();
+
+  InputDevice *consider_add_evdev_device(size_t index);
+  InputDevice *consider_add_js_device(size_t index);
+
+  virtual void update();
+
+protected:
+  int _inotify_fd;
+
+  pvector<InputDevice *> _evdev_devices;
+
+  friend class InputDeviceManager;
+};
+
+#endif  // PHAVE_LINUX_INPUT_H
+
+#endif

+ 12 - 0
panda/src/device/linuxJoystickDevice.I

@@ -0,0 +1,12 @@
+/**
+ * PANDA 3D SOFTWARE
+ * Copyright (c) Carnegie Mellon University.  All rights reserved.
+ *
+ * All use of this software is subject to the terms of the revised BSD
+ * license.  You should have received a copy of this license along
+ * with this source code in a file named "LICENSE."
+ *
+ * @file linuxJoystickDevice.I
+ * @author rdb
+ * @date 2015-08-21
+ */

+ 423 - 0
panda/src/device/linuxJoystickDevice.cxx

@@ -0,0 +1,423 @@
+/**
+ * PANDA 3D SOFTWARE
+ * Copyright (c) Carnegie Mellon University.  All rights reserved.
+ *
+ * All use of this software is subject to the terms of the revised BSD
+ * license.  You should have received a copy of this license along
+ * with this source code in a file named "LICENSE."
+ *
+ * @file linuxJoystickDevice.cxx
+ * @author rdb
+ * @date 2015-08-21
+ */
+
+#include "linuxJoystickDevice.h"
+#include "evdevInputDevice.h"
+
+#ifdef PHAVE_LINUX_INPUT_H
+
+#include "gamepadButton.h"
+#include "linuxInputDeviceManager.h"
+
+#include <fcntl.h>
+#include <linux/joystick.h>
+
+TypeHandle LinuxJoystickDevice::_type_handle;
+
+/**
+ * Creates a new device using the Linux joystick device with the given index.
+ */
+LinuxJoystickDevice::
+LinuxJoystickDevice(LinuxInputDeviceManager *manager, size_t index) :
+  _manager(manager),
+  _fd(-1),
+  _index(index),
+  _dpad_x_axis(-1),
+  _dpad_y_axis(-1),
+  _dpad_left_button(-1),
+  _dpad_up_button(-1),
+  _ltrigger_button(-1),
+  _rtrigger_button(-1)
+{
+  LightMutexHolder holder(_lock);
+  if (!open_device()) {
+    device_cat.error()
+      << "Could not open joystick device /dev/input/js" << index
+      << ": " << strerror(errno) << "\n";
+  }
+}
+
+/**
+ *
+ */
+LinuxJoystickDevice::
+~LinuxJoystickDevice() {
+  if (_fd != -1) {
+    close(_fd);
+    _fd = -1;
+  }
+}
+
+/**
+ * Returns true if there are pending events.
+ */
+bool LinuxJoystickDevice::
+check_events() const {
+  unsigned int avail = 0;
+  ioctl(_fd, FIONREAD, &avail);
+  return (avail != 0);
+}
+
+/**
+ * Polls the input device for new activity, to ensure it contains the latest
+ * events.  This will only have any effect for some types of input devices;
+ * others may be updated automatically, and this method will be a no-op.
+ */
+void LinuxJoystickDevice::
+do_poll() {
+  if (_fd != -1 && process_events()) {
+    while (process_events()) {}
+
+    // If we got events, we are obviously connected.  Mark us so.
+    if (!_is_connected) {
+      _is_connected = true;
+      if (_manager != nullptr) {
+        _manager->add_device(this);
+      }
+    }
+  }
+}
+
+/**
+ * Opens or reopens the joystick device, and reads out the button and axis
+ * mappings.  Assumes the lock is already held.
+ */
+bool LinuxJoystickDevice::
+open_device() {
+  nassertr(_lock.debug_is_locked(), false);
+
+  char path[64];
+  sprintf(path, "/dev/input/js%zd", _index);
+
+  _fd = open(path, O_RDONLY | O_NONBLOCK);
+
+  if (_fd == -1) {
+    _is_connected = false;
+    return false;
+  }
+
+  // Read the name from the device.  We'll later try to use sysfs to read
+  // the proper product name from the device, but this is a good fallback.
+  char name[128];
+  name[0] = 0;
+  ioctl(_fd, JSIOCGNAME(sizeof(name)), name);
+  _name = name;
+
+  bool have_analog_triggers = false;
+
+  // Get the number of axes.
+  uint8_t num_axes = 0, num_buttons = 0;
+  ioctl(_fd, JSIOCGAXES, &num_axes);
+  ioctl(_fd, JSIOCGBUTTONS, &num_buttons);
+
+  _buttons.resize(num_buttons);
+  _axes.resize(num_axes);
+
+  if (num_buttons > 0) {
+    uint16_t btnmap[512];
+    ioctl(_fd, JSIOCGBTNMAP, btnmap);
+
+    for (uint8_t i = 0; i < num_buttons; ++i) {
+      ButtonHandle handle = EvdevInputDevice::map_button(btnmap[i]);
+      if (handle == ButtonHandle::none()) {
+        if (device_cat.is_debug()) {
+          device_cat.debug() << "Unmapped /dev/input/js" << _index
+            << " button " << (int)i << ": 0x" << std::hex << btnmap[i] << "\n";
+        }
+      } else if (handle == GamepadButton::face_a()) {
+        _device_class = DeviceClass::gamepad;
+      } else if (handle == GamepadButton::trigger()) {
+        _device_class = DeviceClass::flight_stick;
+      } else if (handle == GamepadButton::ltrigger()) {
+        _ltrigger_button = i;
+      } else if (handle == GamepadButton::rtrigger()) {
+        _rtrigger_button = i;
+      }
+      _buttons[i].handle = handle;
+    }
+  }
+
+  if (num_axes > 0) {
+    uint8_t axmap[512];
+    ioctl(_fd, JSIOCGAXMAP, axmap);
+
+    for (uint8_t i = 0; i < num_axes; ++i) {
+      Axis axis = Axis::none;
+
+      switch (axmap[i]) {
+      case ABS_X:
+        if (_device_class == DeviceClass::gamepad) {
+          axis = InputDevice::Axis::left_x;
+        } else if (_device_class == DeviceClass::flight_stick) {
+          axis = InputDevice::Axis::roll;
+        } else {
+          axis = InputDevice::Axis::x;
+        }
+        break;
+
+      case ABS_Y:
+        if (_device_class == DeviceClass::gamepad) {
+          axis = InputDevice::Axis::left_y;
+        } else if (_device_class == DeviceClass::flight_stick) {
+          axis = InputDevice::Axis::pitch;
+        } else {
+          axis = InputDevice::Axis::y;
+        }
+        break;
+
+      case ABS_Z:
+        if (_device_class == DeviceClass::gamepad) {
+          axis = Axis::left_trigger;
+        } else {
+          //axis = Axis::trigger;
+        }
+        break;
+
+      case ABS_RX:
+        axis = Axis::right_x;
+        break;
+
+      case ABS_RY:
+        axis = Axis::right_y;
+        break;
+
+      case ABS_RZ:
+        if (_device_class == DeviceClass::gamepad) {
+          axis = InputDevice::Axis::right_trigger;
+        } else {
+          axis = InputDevice::Axis::yaw;
+        }
+        break;
+
+      case ABS_THROTTLE:
+        axis = InputDevice::Axis::throttle;
+        break;
+
+      case ABS_RUDDER:
+        axis = InputDevice::Axis::rudder;
+        break;
+
+      case ABS_WHEEL:
+        axis = InputDevice::Axis::wheel;
+        break;
+
+      case ABS_GAS:
+        axis = InputDevice::Axis::accelerator;
+        break;
+
+      case ABS_BRAKE:
+        axis = InputDevice::Axis::brake;
+        break;
+
+      case ABS_HAT0X:
+        if (_dpad_left_button == -1) {
+          // Emulate D-Pad or hat switch.
+          _dpad_x_axis = i;
+          _dpad_left_button = (int)_buttons.size();
+          if (_device_class == DeviceClass::gamepad) {
+            add_button(GamepadButton::dpad_left());
+            add_button(GamepadButton::dpad_right());
+          } else {
+            add_button(GamepadButton::hat_left());
+            add_button(GamepadButton::hat_right());
+          }
+          axis = Axis::none;
+        }
+        break;
+
+      case ABS_HAT0Y:
+        if (_dpad_up_button == -1) {
+          // Emulate D-Pad.
+          _dpad_y_axis = i;
+          _dpad_up_button = (int)_buttons.size();
+          if (_device_class == DeviceClass::gamepad) {
+            add_button(GamepadButton::dpad_up());
+            add_button(GamepadButton::dpad_down());
+          } else {
+            add_button(GamepadButton::hat_up());
+            add_button(GamepadButton::hat_down());
+          }
+          axis = Axis::none;
+        }
+        break;
+
+      default:
+        if (device_cat.is_debug()) {
+          device_cat.debug() << "Unmapped /dev/input/js" << _index
+            << " axis " << (int)i << ": 0x" << std::hex << (int)axmap[i] << "\n";
+        }
+        axis = Axis::none;
+        break;
+      }
+      _axes[i].axis = axis;
+
+      if (axis == Axis::left_trigger || axis == Axis::right_trigger) {
+        // We'd like to use 0.0 to indicate the resting position.
+        _axes[i]._scale = 1.0 / 65534.0;
+        _axes[i]._bias = 0.5;
+        have_analog_triggers = true;
+      } else if (axis == Axis::left_y || axis == Axis::right_y || axis == Axis::y) {
+        _axes[i]._scale = 1.0 / -32767.0;
+        _axes[i]._bias = 0.0;
+      } else {
+        _axes[i]._scale = 1.0 / 32767.0;
+        _axes[i]._bias = 0.0;
+      }
+    }
+  }
+
+  if (_ltrigger_button >= 0 && _rtrigger_button >= 0 && !have_analog_triggers) {
+    // Emulate analog triggers.
+    _ltrigger_control = (int)_axes.size();
+    add_axis(Axis::left_trigger, 0, 1, false);
+    add_axis(Axis::right_trigger, 0, 1, false);
+  } else {
+    _ltrigger_button = -1;
+    _rtrigger_button = -1;
+  }
+
+  // Get additional information from sysfs.
+  sprintf(path, "/sys/class/input/js%zd/device/id/vendor", _index);
+  FILE *f = fopen(path, "r");
+  if (f) {
+    if (fscanf(f, "%hx", &_vendor_id) < 1) {
+      _vendor_id = 0;
+    }
+    fclose(f);
+  }
+  sprintf(path, "/sys/class/input/js%zd/device/id/product", _index);
+  f = fopen(path, "r");
+  if (f) {
+    if (fscanf(f, "%hx", &_product_id) < 1) {
+      _product_id = 0;
+    }
+    fclose(f);
+  }
+  char buffer[256];
+  sprintf(path, "/sys/class/input/js%zd/device/device/../product", _index);
+  f = fopen(path, "r");
+  if (f) {
+    if (fgets(buffer, sizeof(buffer), f) != nullptr) {
+      buffer[strcspn(buffer, "\r\n")] = 0;
+      if (buffer[0] != 0) {
+        _name.assign(buffer);
+      }
+    }
+    fclose(f);
+  }
+  sprintf(path, "/sys/class/input/js%zd/device/device/../manufacturer", _index);
+  f = fopen(path, "r");
+  if (f) {
+    if (fgets(buffer, sizeof(buffer), f) != nullptr) {
+      buffer[strcspn(buffer, "\r\n")] = 0;
+      _manufacturer.assign(buffer);
+    }
+    fclose(f);
+  }
+  sprintf(path, "/sys/class/input/js%zd/device/device/../serial", _index);
+  f = fopen(path, "r");
+  if (f) {
+    if (fgets(buffer, sizeof(buffer), f) != nullptr) {
+      buffer[strcspn(buffer, "\r\n")] = 0;
+      _serial_number.assign(buffer);
+    }
+    fclose(f);
+  }
+
+  // Read the init events.
+  while (process_events()) {};
+
+  // Special case handling for the wireless Xbox receiver - the Linux
+  // joystick API doesn't seem to have a way to report whether the device
+  // is actually turned on.  The best we can do is check whether the axes
+  // are all 0, which indicates that the driver hasn't received any data for
+  // this gamepad yet (which means it hasn't been plugged in for this session)
+  if (strncmp(name, "Xbox 360 Wireless Receiver", 26) == 0) {
+    for (const auto &control : _axes) {
+      if (control.value != 0.0) {
+        _is_connected = true;
+        return true;
+      }
+    }
+    _is_connected = false;
+  } else {
+    _is_connected = true;
+  }
+
+  return true;
+}
+
+/**
+ * Reads a number of events from the device.  Returns true if events were
+ * read, meaning this function should keep being called until it returns
+ * false.
+ */
+bool LinuxJoystickDevice::
+process_events() {
+  // Read 8 events at a time.
+  struct js_event events[8];
+
+  int n_read = read(_fd, events, sizeof(events));
+  if (n_read < 0) {
+    if (errno == EAGAIN || errno == EWOULDBLOCK) {
+      // No data available for now.
+
+    } else if (errno == ENODEV) {
+      // The device ceased to exist, so we better close it.  No need
+      // to worry about removing it from the InputDeviceManager, as it
+      // will get an inotify event sooner or later about this.
+      close(_fd);
+      _fd = -1;
+      //_is_connected = false;
+      errno = 0;
+
+    } else {
+      device_cat.error() << "read: " << strerror(errno) << "\n";
+    }
+    return false;
+  }
+
+  if (n_read == 0) {
+    return false;
+  }
+
+  n_read /= sizeof(struct js_event);
+
+  for (int i = 0; i < n_read; ++i) {
+    int index = events[i].number;
+
+    if (events[i].type & JS_EVENT_BUTTON) {
+      if (index == _ltrigger_button) {
+        axis_changed(_ltrigger_control, events[i].value);
+      } else if (index == _rtrigger_button) {
+        axis_changed(_ltrigger_control + 1, events[i].value);
+      }
+      button_changed(index, (events[i].value != 0));
+
+    } else if (events[i].type & JS_EVENT_AXIS) {
+      if (index == _dpad_x_axis) {
+        button_changed(_dpad_left_button, events[i].value < -1000);
+        button_changed(_dpad_left_button+1, events[i].value > 1000);
+      } else if (index == _dpad_y_axis) {
+        button_changed(_dpad_up_button, events[i].value < -1000);
+        button_changed(_dpad_up_button+1, events[i].value > 1000);
+      }
+
+      axis_changed(index, events[i].value);
+    }
+  }
+
+  return true;
+}
+
+#endif

+ 75 - 0
panda/src/device/linuxJoystickDevice.h

@@ -0,0 +1,75 @@
+/**
+ * PANDA 3D SOFTWARE
+ * Copyright (c) Carnegie Mellon University.  All rights reserved.
+ *
+ * All use of this software is subject to the terms of the revised BSD
+ * license.  You should have received a copy of this license along
+ * with this source code in a file named "LICENSE."
+ *
+ * @file linuxJoystickDevice.h
+ * @author rdb
+ * @date 2015-08-21
+ */
+
+#ifndef LINUXJOYSTICKDEVICE_H
+#define LINUXJOYSTICKDEVICE_H
+
+#include "inputDevice.h"
+
+#ifdef PHAVE_LINUX_INPUT_H
+
+class LinuxInputDeviceManager;
+
+/**
+ * This is a type of device that uses the Linux /dev/input/js# API to read
+ * data from a game controller.
+ */
+class EXPCL_PANDA_DEVICE LinuxJoystickDevice : public InputDevice {
+PUBLISHED:
+  LinuxJoystickDevice(LinuxInputDeviceManager *manager, size_t index);
+  virtual ~LinuxJoystickDevice();
+
+  bool check_events() const;
+
+private:
+  virtual void do_poll();
+
+  bool open_device();
+  bool process_events();
+
+private:
+  LinuxInputDeviceManager *_manager;
+
+  int _fd;
+  size_t _index;
+
+  // These are used for D-pad / hat switch emulation.
+  int _dpad_x_axis;
+  int _dpad_y_axis;
+  int _dpad_left_button;
+  int _dpad_up_button;
+
+  // This is used for axis emulation.
+  int _ltrigger_control;
+  int _ltrigger_button;
+  int _rtrigger_button;
+
+public:
+  static TypeHandle get_class_type() {
+    return _type_handle;
+  }
+  static void init_type() {
+    InputDevice::init_type();
+    register_type(_type_handle, "LinuxJoystickDevice",
+                  InputDevice::get_class_type());
+  }
+
+private:
+  static TypeHandle _type_handle;
+};
+
+#include "linuxJoystickDevice.I"
+
+#endif  // PHAVE_LINUX_INPUT_H
+
+#endif  // LINUXJOYSTICKDEVICE_H

+ 11 - 1
panda/src/device/p3device_composite1.cxx

@@ -5,4 +5,14 @@
 #include "clientDevice.cxx"
 #include "clientDialDevice.cxx"
 #include "clientTrackerDevice.cxx"
-
+#include "evdevInputDevice.cxx"
+#include "inputDevice.cxx"
+#include "inputDeviceManager.cxx"
+#include "inputDeviceNode.cxx"
+#include "inputDeviceSet.cxx"
+#include "ioKitInputDevice.cxx"
+#include "ioKitInputDeviceManager.cxx"
+#include "linuxInputDeviceManager.cxx"
+#include "linuxJoystickDevice.cxx"
+#include "winInputDeviceManager.cxx"
+#include "winRawInputDevice.cxx"

+ 1 - 2
panda/src/device/p3device_composite2.cxx

@@ -3,8 +3,7 @@
 #include "analogNode.cxx"
 #include "buttonNode.cxx"
 #include "dialNode.cxx"
-#include "mouseAndKeyboard.cxx"
 #include "trackerData.cxx"
 #include "trackerNode.cxx"
 #include "virtualMouse.cxx"
-
+#include "xInputDevice.cxx"

+ 1 - 2
panda/src/device/trackerNode.I

@@ -17,10 +17,9 @@
  */
 INLINE bool TrackerNode::
 is_valid() const {
-  return (_tracker != nullptr) && _tracker->is_connected();
+  return (!_tracker.is_null()/* && _tracker->is_connected()*/);
 }
 
-
 /**
  * Returns the current position of the tracker, if it is available.
  */

+ 8 - 9
panda/src/device/trackerNode.cxx

@@ -48,15 +48,15 @@ TrackerNode(ClientBase *client, const std::string &device_name) :
     return;
   }
 
-  _tracker = DCAST(ClientTrackerDevice, device);
+  _tracker = device;
 }
 
 /**
  *
  */
 TrackerNode::
-TrackerNode(ClientTrackerDevice *device) :
-  DataNode(device->get_device_name()),
+TrackerNode(InputDevice *device) :
+  DataNode(device->get_name()),
   _tracker(device)
 {
   _transform_output = define_output("transform", TransformState::get_class_type());
@@ -64,9 +64,10 @@ TrackerNode(ClientTrackerDevice *device) :
   _transform = TransformState::make_identity();
 
   nassertv(device != nullptr);
-  ClientBase *client = device->get_client();
-  nassertv(client != nullptr);
-  set_tracker_coordinate_system(client->get_coordinate_system());
+  nassertv(device->has_tracker());
+
+  //TODO: get coordinate system from tracker object?
+  set_tracker_coordinate_system(CS_default);
   set_graph_coordinate_system(CS_default);
 }
 
@@ -93,9 +94,7 @@ do_transmit_data(DataGraphTraverser *, const DataNodeTransmit &,
                  DataNodeTransmit &output) {
   if (is_valid()) {
     _tracker->poll();
-    _tracker->acquire();
-    _data = _tracker->get_data();
-    _tracker->unlock();
+    _data = _tracker->get_tracker();
 
     _data.get_orient().extract_to_matrix(_mat);
     if (_tracker_cs != _graph_cs) {

+ 7 - 6
panda/src/device/trackerNode.h

@@ -18,21 +18,22 @@
 
 #include "clientBase.h"
 #include "trackerData.h"
-#include "clientTrackerDevice.h"
+#include "inputDevice.h"
 #include "dataNode.h"
 #include "luse.h"
 #include "linmath_events.h"
 #include "pointerTo.h"
 
 /**
- * This is the primary interface to a Tracker object associated with a
- * ClientBase.  It reads the position and orientation information from the
- * tracker and makes it available as a transformation on the data graph.
+ * This class reads the position and orientation information from a tracker
+ * device and makes it available as a transformation on the data graph.
+ * It is also the primary interface to a Tracker object associated with a
+ * ClientBase.
  */
 class EXPCL_PANDA_DEVICE TrackerNode : public DataNode {
 PUBLISHED:
   explicit TrackerNode(ClientBase *client, const std::string &device_name);
-  explicit TrackerNode(ClientTrackerDevice *device);
+  explicit TrackerNode(InputDevice *device);
   virtual ~TrackerNode();
 
   INLINE bool is_valid() const;
@@ -62,7 +63,7 @@ private:
   CPT(TransformState) _transform;
 
 private:
-  PT(ClientTrackerDevice) _tracker;
+  PT(InputDevice) _tracker;
   TrackerData _data;
   LMatrix4 _mat;
   CoordinateSystem _tracker_cs, _graph_cs;

+ 391 - 0
panda/src/device/winInputDeviceManager.cxx

@@ -0,0 +1,391 @@
+/**
+ * PANDA 3D SOFTWARE
+ * Copyright (c) Carnegie Mellon University.  All rights reserved.
+ *
+ * All use of this software is subject to the terms of the revised BSD
+ * license.  You should have received a copy of this license along
+ * with this source code in a file named "LICENSE."
+ *
+ * @file winInputDeviceManager.cxx
+ * @author rdb
+ * @date 2018-01-21
+ */
+
+#include "winInputDeviceManager.h"
+#include "winRawInputDevice.h"
+#include "throw_event.h"
+
+#if defined(_WIN32) && !defined(CPPPARSER)
+
+/**
+ * Initializes the input device manager by scanning which devices are currently
+ * connected and setting up any platform-dependent structures necessary for
+ * listening for future device connect events.
+ */
+WinInputDeviceManager::
+WinInputDeviceManager() :
+  _xinput_device0(0),
+  _xinput_device1(1),
+  _xinput_device2(2),
+  _xinput_device3(3),
+  _message_hwnd(nullptr) {
+
+  // XInput provides four device slots, so we simply create four XInputDevice
+  // objects that are bound to the lifetime of the input manager.
+  _xinput_device0.local_object();
+  _xinput_device1.local_object();
+  _xinput_device2.local_object();
+  _xinput_device3.local_object();
+
+// This function is only available in Vista and later, so we use a wrapper.
+  HMODULE module = LoadLibraryA("cfgmgr32.dll");
+  if (module) {
+    _CM_Get_DevNode_PropertyW = (pCM_Get_DevNode_Property)GetProcAddress(module, "CM_Get_DevNode_PropertyW");
+  } else {
+    _CM_Get_DevNode_PropertyW = nullptr;
+  }
+
+  // Now create a message-only window for the raw input.
+  WNDCLASSEX wc = {};
+  wc.cbSize = sizeof(WNDCLASSEX);
+  wc.lpfnWndProc = window_proc;
+  wc.hInstance = GetModuleHandle(nullptr);
+  wc.lpszClassName = "InputDeviceManager";
+  if (!RegisterClassEx(&wc)) {
+    device_cat.warning()
+      << "Failed to register message-only window class.\n";
+    return;
+   }
+
+  _message_hwnd = CreateWindowEx(0, wc.lpszClassName, "InputDeviceManager", 0, 0, 0, 0, 0, HWND_MESSAGE, nullptr, nullptr, nullptr);
+  if (!_message_hwnd) {
+    device_cat.warning()
+      << "Failed to create message-only window.\n";
+    return;
+  }
+
+  // Now listen for raw input devices using the created message loop.
+  RAWINPUTDEVICE rid[3];
+  rid[0].usUsagePage = 1;
+  rid[0].usUsage = 4; // Joysticks
+  rid[0].dwFlags = RIDEV_DEVNOTIFY | RIDEV_INPUTSINK;
+  rid[0].hwndTarget = _message_hwnd;
+  rid[1].usUsagePage = 1;
+  rid[1].usUsage = 5; // Gamepads
+  rid[1].dwFlags = RIDEV_DEVNOTIFY | RIDEV_INPUTSINK;
+  rid[1].hwndTarget = _message_hwnd;
+  rid[2].usUsagePage = 1;
+  rid[2].usUsage = 8; // Multi-axis controllers (including 3D mice)
+  rid[2].dwFlags = RIDEV_DEVNOTIFY | RIDEV_INPUTSINK;
+  rid[2].hwndTarget = _message_hwnd;
+  if (!RegisterRawInputDevices(rid, 3, sizeof(RAWINPUTDEVICE))) {
+    device_cat.warning()
+      << "Failed to register raw input devices.\n";
+  }
+}
+
+/**
+ * Closes any resources that the device manager was using to listen for events.
+ */
+WinInputDeviceManager::
+~WinInputDeviceManager() {
+  if (_message_hwnd != nullptr) {
+    DestroyWindow(_message_hwnd);
+    _message_hwnd = nullptr;
+  }
+}
+
+/**
+ * Called by the raw input device destructor.
+ */
+void WinInputDeviceManager::
+device_destroyed(WinRawInputDevice *device) {
+  LightMutexHolder holder(_lock);
+  // It shouldn't be in here, but let's check to be sure.
+  if (device->_handle != nullptr) {
+    _raw_devices.erase(device->_handle);
+  }
+
+  _raw_devices_by_path.erase(device->_path);
+}
+
+/**
+ * Called upon receiving a WM_INPUT message.
+ */
+void WinInputDeviceManager::
+on_input(HRAWINPUT handle) {
+  UINT size;
+  if (GetRawInputData(handle, RID_INPUT, nullptr, &size, sizeof(RAWINPUTHEADER)) < 0) {
+    return;
+  }
+
+  PRAWINPUT data = (PRAWINPUT)alloca(size);
+  if (GetRawInputData(handle, RID_INPUT, data, &size, sizeof(RAWINPUTHEADER)) <= 0) {
+    return;
+  }
+
+  // Look up the device in the map.
+  PT(WinRawInputDevice) device;
+  {
+    LightMutexHolder holder(_lock);
+    auto it = _raw_devices.find(data->header.hDevice);
+    if (it != _raw_devices.end()) {
+      device = it->second;
+    }
+  }
+  if (device != nullptr) {
+    device->on_input(data);
+  }
+}
+
+/**
+ * Called upon receiving WM_INPUT_DEVICE_CHANGE with wparam GIDC_ARRIVAL.
+ */
+void WinInputDeviceManager::
+on_input_device_arrival(HANDLE handle) {
+  // Get the device path.
+  UINT size;
+  if (GetRawInputDeviceInfoA(handle, RIDI_DEVICENAME, nullptr, &size) != 0) {
+    return;
+  }
+
+  char *path = (char *)alloca(size);
+  if (path == nullptr ||
+      GetRawInputDeviceInfoA(handle, RIDI_DEVICENAME, (void *)path, &size) < 0) {
+    return;
+  }
+
+  if (device_cat.is_debug()) {
+    device_cat.debug()
+      << "GIDC_ARRIVAL: " << path << "\n";
+  }
+
+  // Get the device info.
+  RID_DEVICE_INFO info;
+  info.cbSize = sizeof(RID_DEVICE_INFO);
+  size = sizeof(RID_DEVICE_INFO);
+  if (GetRawInputDeviceInfoA(handle, RIDI_DEVICEINFO, &info, &size) <= 0) {
+    return;
+  }
+
+  // Strip the \\?\ prefix from the path.
+  while (path[0] == '\\' || path[0] == '?' || path[0] == '.') {
+    ++path;
+  }
+
+  // Now, replace # with \\ in the path, but only up to three components.
+  char *p = path;
+  int i = 0;
+  while (*p != '\0') {
+    if (*p == '#') {
+      if (i++ < 2) {
+        *p = '\\';
+      } else {
+        *p = '\0';
+        break;
+      }
+    }
+    ++p;
+  }
+
+  // Find the device node, which will be something like "HID\VID_0123..."
+  // Then we walk the device tree upward to get the USB node, which which will
+  // be something like a "USB\VID..." node, from which we can fetch the real
+  // USB device information (such as the product name).
+  std::string name, manufacturer;
+  DEVINST inst;
+  CONFIGRET ret = CM_Locate_DevNodeA(&inst, (DEVINSTID_A)path, CM_LOCATE_DEVNODE_PHANTOM);
+  if (ret == CR_SUCCESS) {
+    char buffer[4096];
+    ULONG buflen = 4096;
+    if (CM_Get_DevNode_Registry_Property(inst, CM_DRP_DEVICEDESC, 0, buffer, &buflen, 0) == CR_SUCCESS) {
+      name.assign(buffer);
+    }
+    buflen = 4096;
+    if (CM_Get_DevNode_Registry_Property(inst, CM_DRP_MFG, 0, buffer, &buflen, 0) == CR_SUCCESS) {
+      if (strcmp(buffer, "(Standard system devices)") != 0) {
+        manufacturer.assign(buffer);
+      }
+    }
+
+    // Now walk the device tree upwards so fetch the bus-reported name of the
+    // parent USB device, which we prefer over the regular device description
+    // that is probably boring like "HID-compliant game controller".
+    DEVINST cur = inst;
+    DEVINST parent;
+    while (CM_Get_Parent(&parent, cur, 0) == CR_SUCCESS) {
+      buflen = 4096;
+      std::string dev_class;
+      if (CM_Get_DevNode_Registry_Property(parent, CM_DRP_CLASS, 0, buffer, &buflen, 0) == CR_SUCCESS) {
+        if (strcmp(buffer, "USB") == 0) {
+          // This is some generic USB device, like a hub.  We've gone too far.
+          break;
+        }
+        dev_class.assign(buffer);
+      }
+      cur = parent;
+
+      // While we're at it, maybe this one defines a manufacturer?
+      buflen = 4096;
+      if (manufacturer.empty() &&
+          CM_Get_DevNode_Registry_Property(cur, CM_DRP_MFG, 0, buffer, &buflen, 0) == CR_SUCCESS) {
+        if (strcmp(buffer, "(Standard system devices)") != 0) {
+          manufacturer.assign(buffer);
+        }
+      }
+
+      // If it's a generic HID device, take the name from the USB bus.
+      // See devpkey.h for the available property keys.
+      static const DEVPROPKEY bus_reported_device_desc = {
+        {0x540b947e, 0x8b40, 0x45bc, {0xa8, 0xa2, 0x6a, 0x0b, 0x89, 0x4c, 0xbd, 0xa2}},
+        4,
+      };
+      DEVPROPTYPE type;
+      buflen = 4096;
+      if (dev_class == "HIDClass" && _CM_Get_DevNode_PropertyW != nullptr &&
+          _CM_Get_DevNode_PropertyW(cur, &bus_reported_device_desc, &type, (PBYTE)buffer, &buflen, 0) == CR_SUCCESS &&
+          type == DEVPROP_TYPE_STRING) {
+
+        // Some devices insert quite some trailing space here.
+        wchar_t *wbuffer = (wchar_t *)buffer;
+        size_t wlen = wcslen(wbuffer);
+        while (iswspace(wbuffer[wlen - 1])) {
+          wbuffer[--wlen] = 0;
+        }
+        TextEncoder encoder;
+        name.assign(encoder.encode_wtext(std::wstring(wbuffer, wlen)));
+        break;
+      } else {
+        buflen = 4096;
+        if (CM_Get_DevNode_Registry_Property(cur, CM_DRP_DEVICEDESC, 0, buffer, &buflen, 0) == CR_SUCCESS) {
+          // We'll pass if it has this awfully boring name.  Is there a
+          // language-independent way to check this?
+          if (strcmp(buffer, "USB Input Device") != 0) {
+            name.assign(buffer);
+          }
+        }
+      }
+    }
+  } else if (device_cat.is_debug()) {
+    // No big deal, we just won't be able to get the name.
+    device_cat.debug()
+      << "Could not locate device node " << path << " (" << ret << ")\n";
+  }
+
+  // Is this an XInput device?  If so, handle it via XInput, which allows us
+  // to handle independent left/right triggers as well as vibration output.
+  if (info.dwType == RIM_TYPEHID && strstr(path, "&IG_") != nullptr) {
+    // This is a device we should handle via the XInput API.  Check which of
+    // the four players was the lucky one.
+    WinRawInputDevice idev(this, path);
+    if (_xinput_device0.check_arrival(info, inst, name, manufacturer)) {
+      add_device(&_xinput_device0);
+    }
+    if (_xinput_device1.check_arrival(info, inst, name, manufacturer)) {
+      add_device(&_xinput_device1);
+    }
+    if (_xinput_device2.check_arrival(info, inst, name, manufacturer)) {
+      add_device(&_xinput_device2);
+    }
+    if (_xinput_device3.check_arrival(info, inst, name, manufacturer)) {
+      add_device(&_xinput_device3);
+    }
+    return;
+  }
+
+  LightMutexHolder holder(_lock);
+
+  // Do we have a device by this path already?  This can happen if the
+  // user keeps around a pointer to a disconnected device in the hope that
+  // it will reconnect later.
+  PT(WinRawInputDevice) device;
+  auto it = _raw_devices_by_path.find(path);
+  if (it != _raw_devices_by_path.end()) {
+    device = it->second;
+  } else {
+    device = new WinRawInputDevice(this, path);
+    _raw_devices_by_path[path] = device;
+  }
+
+  if (device->on_arrival(handle, info, move(name))) {
+    _raw_devices[handle] = device;
+    _connected_devices.add_device(device);
+
+    if (device_cat.is_debug()) {
+      device_cat.debug()
+        << "Discovered input device " << *device << "\n";
+    }
+    throw_event("connect-device", device.p());
+  }
+}
+
+/**
+ * Called upon receiving WM_INPUT_DEVICE_CHANGE with wparam GIDC_REMOVAL.
+ */
+void WinInputDeviceManager::
+on_input_device_removal(HANDLE handle) {
+  // The handle will probably no longer be valid after this, so find the
+  // device and remove it from _raw_devices.  However, we keep it in
+  // _raw_devices_by_path in case there's still a pointer around to it.
+
+  // We keep the pointer outside the lock because the input device
+  // destructor calls back to InputDeviceManager.
+  PT(WinRawInputDevice) device;
+  {
+    LightMutexHolder holder(_lock);
+    auto it = _raw_devices.find(handle);
+    if (it != _raw_devices.end()) {
+      device = std::move(it->second);
+      _raw_devices.erase(it);
+      device->on_removal();
+
+      if (_connected_devices.remove_device(device)) {
+        throw_event("disconnect-device", device.p());
+      }
+      if (device_cat.is_debug()) {
+        device_cat.debug()
+          << "Removed input device " << *device << "\n";
+      }
+    }
+  }
+}
+
+/**
+ * Implementation of the message loop.
+ */
+LRESULT WINAPI WinInputDeviceManager::
+window_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) {
+  WinInputDeviceManager *mgr;
+  switch (msg) {
+  case WM_INPUT:
+    mgr = (WinInputDeviceManager *)InputDeviceManager::get_global_ptr();
+    if (mgr != nullptr) {
+      mgr->on_input((HRAWINPUT)lparam);
+    }
+    break;
+
+  case WM_INPUT_DEVICE_CHANGE:
+    switch (LOWORD(wparam)) {
+    case GIDC_ARRIVAL:
+      mgr = (WinInputDeviceManager *)InputDeviceManager::get_global_ptr();
+      if (mgr != nullptr) {
+        mgr->on_input_device_arrival((HANDLE)lparam);
+      }
+      break;
+
+    case GIDC_REMOVAL:
+      mgr = (WinInputDeviceManager *)InputDeviceManager::get_global_ptr();
+      if (mgr != nullptr) {
+        mgr->on_input_device_removal((HANDLE)lparam);
+      }
+      break;
+    }
+    break;
+
+  default:
+    break;
+  }
+  return DefWindowProcW(hwnd, msg, wparam, lparam);
+}
+
+#endif

+ 64 - 0
panda/src/device/winInputDeviceManager.h

@@ -0,0 +1,64 @@
+/**
+ * PANDA 3D SOFTWARE
+ * Copyright (c) Carnegie Mellon University.  All rights reserved.
+ *
+ * All use of this software is subject to the terms of the revised BSD
+ * license.  You should have received a copy of this license along
+ * with this source code in a file named "LICENSE."
+ *
+ * @file winInputDeviceManager.h
+ * @author rdb
+ * @date 2018-01-21
+ */
+
+#ifndef WININPUTDEVICEMANAGER_H
+#define WININPUTDEVICEMANAGER_H
+
+#include "inputDeviceManager.h"
+
+#if defined(_WIN32) && !defined(CPPPARSER)
+
+#include "xinputDevice.h"
+
+#include <CfgMgr32.h>
+#include <devpkey.h>
+
+class WinRawInputDevice;
+
+/**
+ * This is the Windows implementation of InputDeviceManager, managing both
+ * XInput controllers and raw input devices.
+ */
+class EXPCL_PANDA_DEVICE WinInputDeviceManager final : public InputDeviceManager {
+private:
+  WinInputDeviceManager();
+  ~WinInputDeviceManager();
+
+public:
+  void device_destroyed(WinRawInputDevice *device);
+
+  void on_input(HRAWINPUT handle);
+  void on_input_device_arrival(HANDLE handle);
+  void on_input_device_removal(HANDLE handle);
+
+private:
+  // There are always exactly four of these in existence.
+  XInputDevice _xinput_device0;
+  XInputDevice _xinput_device1;
+  XInputDevice _xinput_device2;
+  XInputDevice _xinput_device3;
+
+  HWND _message_hwnd;
+  pmap<HANDLE, WinRawInputDevice *> _raw_devices;
+  pmap<std::string, WinRawInputDevice *> _raw_devices_by_path;
+
+  static LRESULT WINAPI window_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam);
+
+  typedef CONFIGRET (*pCM_Get_DevNode_Property)(DEVINST, const DEVPROPKEY *, DEVPROPTYPE *, PBYTE, PULONG, ULONG);
+  pCM_Get_DevNode_Property _CM_Get_DevNode_PropertyW;
+
+  friend class InputDeviceManager;
+};
+
+#endif
+#endif

+ 684 - 0
panda/src/device/winRawInputDevice.cxx

@@ -0,0 +1,684 @@
+/**
+ * PANDA 3D SOFTWARE
+ * Copyright (c) Carnegie Mellon University.  All rights reserved.
+ *
+ * All use of this software is subject to the terms of the revised BSD
+ * license.  You should have received a copy of this license along
+ * with this source code in a file named "LICENSE."
+ *
+ * @file winRawInputDevice.cxx
+ * @author rdb
+ * @date 2018-01-19
+ */
+
+#include "winRawInputDevice.h"
+#include "gamepadButton.h"
+#include "mouseButton.h"
+
+#if defined(_WIN32) && !defined(CPPPARSER)
+
+#include <CfgMgr32.h>
+#include <devpkey.h>
+
+// Copy definitions from hidusage.h, until we can drop support for the 7.1 SDK
+typedef USHORT USAGE, *PUSAGE;
+
+#define HID_USAGE_PAGE_UNDEFINED       ((USAGE) 0x00)
+#define HID_USAGE_PAGE_GENERIC         ((USAGE) 0x01)
+#define HID_USAGE_PAGE_SIMULATION      ((USAGE) 0x02)
+#define HID_USAGE_PAGE_VR              ((USAGE) 0x03)
+#define HID_USAGE_PAGE_SPORT           ((USAGE) 0x04)
+#define HID_USAGE_PAGE_GAME            ((USAGE) 0x05)
+#define HID_USAGE_PAGE_KEYBOARD        ((USAGE) 0x07)
+#define HID_USAGE_PAGE_LED             ((USAGE) 0x08)
+#define HID_USAGE_PAGE_BUTTON          ((USAGE) 0x09)
+
+#define HID_USAGE_GENERIC_POINTER      ((USAGE) 0x01)
+#define HID_USAGE_GENERIC_MOUSE        ((USAGE) 0x02)
+#define HID_USAGE_GENERIC_JOYSTICK     ((USAGE) 0x04)
+#define HID_USAGE_GENERIC_GAMEPAD      ((USAGE) 0x05)
+#define HID_USAGE_GENERIC_KEYBOARD     ((USAGE) 0x06)
+#define HID_USAGE_GENERIC_KEYPAD       ((USAGE) 0x07)
+#define HID_USAGE_GENERIC_SYSTEM_CTL   ((USAGE) 0x80)
+
+#define HID_USAGE_GENERIC_X            ((USAGE) 0x30)
+#define HID_USAGE_GENERIC_Y            ((USAGE) 0x31)
+#define HID_USAGE_GENERIC_Z            ((USAGE) 0x32)
+#define HID_USAGE_GENERIC_RX           ((USAGE) 0x33)
+#define HID_USAGE_GENERIC_RY           ((USAGE) 0x34)
+#define HID_USAGE_GENERIC_RZ           ((USAGE) 0x35)
+#define HID_USAGE_GENERIC_SLIDER       ((USAGE) 0x36)
+#define HID_USAGE_GENERIC_DIAL         ((USAGE) 0x37)
+#define HID_USAGE_GENERIC_WHEEL        ((USAGE) 0x38)
+#define HID_USAGE_GENERIC_HATSWITCH    ((USAGE) 0x39)
+
+// Copy definitions from hidpi.h, until we can drop support for the 7.1 SDK
+#define HIDP_STATUS_SUCCESS ((NTSTATUS)(0x11 << 16))
+
+typedef enum _HIDP_REPORT_TYPE {
+  HidP_Input,
+  HidP_Output,
+  HidP_Feature
+} HIDP_REPORT_TYPE;
+
+typedef struct _HIDP_BUTTON_CAPS {
+  USAGE UsagePage;
+  UCHAR ReportID;
+  BOOLEAN IsAlias;
+  USHORT BitField;
+  USHORT LinkCollection;
+  USAGE LinkUsage;
+  USAGE LinkUsagePage;
+  BOOLEAN IsRange;
+  BOOLEAN IsStringRange;
+  BOOLEAN IsDesignatorRange;
+  BOOLEAN IsAbsolute;
+  ULONG Reserved[10];
+  union {
+    struct {
+      USAGE UsageMin, UsageMax;
+      USHORT StringMin, StringMax;
+      USHORT DesignatorMin, DesignatorMax;
+      USHORT DataIndexMin, DataIndexMax;
+    } Range;
+    struct  {
+      USAGE Usage, Reserved1;
+      USHORT StringIndex, Reserved2;
+      USHORT DesignatorIndex, Reserved3;
+      USHORT DataIndex, Reserved4;
+    } NotRange;
+  };
+} HIDP_BUTTON_CAPS, *PHIDP_BUTTON_CAPS;
+
+typedef struct _HIDP_VALUE_CAPS {
+  USAGE UsagePage;
+  UCHAR ReportID;
+  BOOLEAN IsAlias;
+  USHORT BitField;
+  USHORT LinkCollection;
+  USAGE LinkUsage;
+  USAGE LinkUsagePage;
+  BOOLEAN IsRange;
+  BOOLEAN IsStringRange;
+  BOOLEAN IsDesignatorRange;
+  BOOLEAN IsAbsolute;
+  BOOLEAN HasNull;
+  UCHAR Reserved;
+  USHORT BitSize;
+  USHORT ReportCount;
+  USHORT Reserved2[5];
+  ULONG UnitsExp;
+  ULONG Units;
+  LONG LogicalMin, LogicalMax;
+  LONG PhysicalMin, PhysicalMax;
+  union {
+    struct {
+      USAGE UsageMin, UsageMax;
+      USHORT StringMin, StringMax;
+      USHORT DesignatorMin, DesignatorMax;
+      USHORT DataIndexMin, DataIndexMax;
+    } Range;
+    struct {
+      USAGE Usage, Reserved1;
+      USHORT StringIndex, Reserved2;
+      USHORT DesignatorIndex, Reserved3;
+      USHORT DataIndex, Reserved4;
+    } NotRange;
+  };
+} HIDP_VALUE_CAPS, *PHIDP_VALUE_CAPS;
+
+typedef PUCHAR PHIDP_REPORT_DESCRIPTOR;
+typedef struct _HIDP_PREPARSED_DATA *PHIDP_PREPARSED_DATA;
+
+typedef struct _HIDP_CAPS {
+  USAGE Usage;
+  USAGE UsagePage;
+  USHORT InputReportByteLength;
+  USHORT OutputReportByteLength;
+  USHORT FeatureReportByteLength;
+  USHORT Reserved[17];
+  USHORT NumberLinkCollectionNodes;
+  USHORT NumberInputButtonCaps;
+  USHORT NumberInputValueCaps;
+  USHORT NumberInputDataIndices;
+  USHORT NumberOutputButtonCaps;
+  USHORT NumberOutputValueCaps;
+  USHORT NumberOutputDataIndices;
+  USHORT NumberFeatureButtonCaps;
+  USHORT NumberFeatureValueCaps;
+  USHORT NumberFeatureDataIndices;
+} HIDP_CAPS, *PHIDP_CAPS;
+
+typedef struct _HIDP_DATA {
+  USHORT DataIndex;
+  USHORT Reserved;
+  union {
+    ULONG RawValue;
+    BOOLEAN On;
+  };
+} HIDP_DATA, *PHIDP_DATA;
+
+typedef LONG NTSTATUS;
+typedef NTSTATUS (*pHidP_GetCaps)(PHIDP_PREPARSED_DATA, PHIDP_CAPS);
+typedef NTSTATUS (*pHidP_GetButtonCaps)(HIDP_REPORT_TYPE, PHIDP_BUTTON_CAPS, PUSHORT, PHIDP_PREPARSED_DATA);
+typedef NTSTATUS (*pHidP_GetValueCaps)(HIDP_REPORT_TYPE, PHIDP_VALUE_CAPS, PUSHORT, PHIDP_PREPARSED_DATA);
+typedef NTSTATUS (*pHidP_GetData)(HIDP_REPORT_TYPE, PHIDP_DATA, PULONG, PHIDP_PREPARSED_DATA, PCHAR, ULONG);
+typedef ULONG (*pHidP_MaxDataListLength)(HIDP_REPORT_TYPE, PHIDP_PREPARSED_DATA);
+
+static pHidP_GetCaps _HidP_GetCaps = nullptr;
+static pHidP_GetButtonCaps _HidP_GetButtonCaps = nullptr;
+static pHidP_GetValueCaps _HidP_GetValueCaps = nullptr;
+static pHidP_GetData _HidP_GetData = nullptr;
+static pHidP_MaxDataListLength _HidP_MaxDataListLength = nullptr;
+
+/**
+ * Static method to initialize the HID parser library.  We load it dynamically
+ * because the Windows 7.1 SDK doesn't ship hid.lib.
+ */
+static bool init_hidp() {
+  HMODULE module = LoadLibraryA("hid.dll");
+  if (module) {
+    if (device_cat.is_debug()) {
+      device_cat.debug()
+        << "Successfully loaded hid.dll\n";
+    }
+
+    _HidP_GetCaps = (pHidP_GetCaps)GetProcAddress(module, "HidP_GetCaps");
+    _HidP_GetButtonCaps = (pHidP_GetButtonCaps)GetProcAddress(module, "HidP_GetButtonCaps");
+    _HidP_GetValueCaps = (pHidP_GetValueCaps)GetProcAddress(module, "HidP_GetValueCaps");
+    _HidP_GetData = (pHidP_GetData)GetProcAddress(module, "HidP_GetData");
+    _HidP_MaxDataListLength = (pHidP_MaxDataListLength)GetProcAddress(module, "HidP_MaxDataListLength");
+
+    if (_HidP_GetCaps == nullptr || _HidP_GetButtonCaps == nullptr ||
+        _HidP_GetValueCaps == nullptr || _HidP_GetData == nullptr ||
+        _HidP_MaxDataListLength == nullptr) {
+      device_cat.error()
+        << "Failed to locate function pointers in hid.dll\n";
+      return false;
+    }
+
+    return true;
+  }
+
+  device_cat.error()
+    << "Failed to load hid.dll.\n";
+  return false;
+}
+
+/**
+ * Protected constructor.  Given a raw device handle.
+ */
+WinRawInputDevice::
+WinRawInputDevice(WinInputDeviceManager *manager, const char *path) :
+  _manager(manager),
+  _path(path),
+  _max_data_count(0),
+  _preparsed(nullptr) {
+}
+
+/**
+ *
+ */
+WinRawInputDevice::
+~WinRawInputDevice() {
+  // Unregister the device from the manager.
+  LightMutexHolder holder(_lock);
+  if (_manager != nullptr) {
+    _manager->device_destroyed(this);
+  }
+  if (_preparsed != nullptr) {
+    free(_preparsed);
+    _preparsed = nullptr;
+  }
+}
+
+/**
+ * Called by InputDeviceManager when this device is connected.  Returns true
+ * if the device was connected successfully.
+ */
+bool WinRawInputDevice::
+on_arrival(HANDLE handle, const RID_DEVICE_INFO &info, std::string name) {
+  using std::hex;
+  using std::dec;
+  using std::swap;
+
+  LightMutexHolder holder(_lock);
+
+  _name = std::move(name);
+
+  switch (info.dwType) {
+  case RIM_TYPEMOUSE:
+    _device_class = DeviceClass::mouse;
+    break;
+
+  case RIM_TYPEKEYBOARD:
+    _device_class = DeviceClass::keyboard;
+    break;
+
+  case RIM_TYPEHID:
+    _vendor_id = info.hid.dwVendorId;
+    _product_id = info.hid.dwProductId;
+
+    // Gamepads
+    if (info.hid.usUsagePage == HID_USAGE_PAGE_GENERIC &&
+        info.hid.usUsage == HID_USAGE_GENERIC_GAMEPAD) {
+      _device_class = DeviceClass::gamepad;
+
+    // Flight sticks
+    } else if (info.hid.usUsagePage == HID_USAGE_PAGE_GENERIC &&
+               info.hid.usUsage == HID_USAGE_GENERIC_JOYSTICK) {
+      _device_class = DeviceClass::flight_stick;
+
+      if (_name == "usb gamepad") {
+        // Well, it claims to be a gamepad...
+        _device_class = DeviceClass::gamepad;
+      }
+
+    // Mice
+    } else if (info.hid.usUsagePage == HID_USAGE_PAGE_GENERIC &&
+               info.hid.usUsage == HID_USAGE_GENERIC_MOUSE) {
+      _device_class = DeviceClass::mouse;
+
+    // Keyboards
+    } else if (info.hid.usUsagePage == HID_USAGE_PAGE_GENERIC &&
+               info.hid.usUsage == HID_USAGE_GENERIC_KEYBOARD) {
+      _device_class = DeviceClass::keyboard;
+
+    // 3Dconnexion SpaceNavigator and friends.
+    } else if (_vendor_id == 0x046d &&
+        (_product_id == 0xc623 ||
+         _product_id == 0xc625 ||
+         _product_id == 0xc626 ||
+         _product_id == 0xc627 ||
+         _product_id == 0xc628 ||
+         _product_id == 0xc629 ||
+         _product_id == 0xc62b)) {
+      _device_class = DeviceClass::spatial_mouse;
+    }
+    break;
+
+  default:
+    return false;
+  }
+
+  // Initialize hid.dll, which provides the HID parser functions.
+  static bool hid_initialized = false;
+  if (!hid_initialized) {
+    if (!init_hidp()) {
+      return false;
+    }
+    hid_initialized = true;
+  }
+
+  // Get the "preparsed data", which we can parse with the HID parser API.
+  UINT size = 0;
+  if (GetRawInputDeviceInfo(handle, RIDI_PREPARSEDDATA, nullptr, &size) < 0) {
+    return false;
+  }
+
+  PHIDP_PREPARSED_DATA buffer = (PHIDP_PREPARSED_DATA)malloc(size);
+  if (GetRawInputDeviceInfo(handle, RIDI_PREPARSEDDATA, buffer, &size) <= 0) {
+    return false;
+  }
+  _preparsed = buffer;
+
+  HIDP_CAPS caps;
+  if (_HidP_GetCaps(buffer, &caps) != HIDP_STATUS_SUCCESS) {
+    device_cat.warning()
+      << "Failed to get capabilities from HID preparsed data.\n";
+    return false;
+  }
+
+  // Do we have a button mapping?
+  static const ButtonHandle gamepad_buttons_common[] = {
+    ButtonHandle::none(),
+    GamepadButton::face_a(),
+    GamepadButton::face_b(),
+    GamepadButton::face_x(),
+    GamepadButton::face_y(),
+    GamepadButton::lshoulder(),
+    GamepadButton::rshoulder(),
+    GamepadButton::start(),
+    GamepadButton::back(),
+    GamepadButton::lstick(),
+    GamepadButton::rstick(),
+  };
+  const ButtonHandle *gamepad_buttons = gamepad_buttons_common;
+  if (_vendor_id == 0x0810 && _product_id == 0xe501) {
+    // SNES-style USB gamepad
+    static const ButtonHandle gamepad_buttons_snes[] = {
+      ButtonHandle::none(),
+      GamepadButton::face_x(),
+      GamepadButton::face_a(),
+      GamepadButton::face_b(),
+      GamepadButton::face_y(),
+      GamepadButton::lshoulder(),
+      GamepadButton::rshoulder(),
+      ButtonHandle::none(),
+      ButtonHandle::none(),
+      GamepadButton::back(),
+      GamepadButton::start(),
+    };
+    gamepad_buttons = gamepad_buttons_snes;
+  }
+
+  // Prepare a mapping of data indices to button/axis indices.
+  _indices.resize(caps.NumberInputDataIndices);
+
+  _buttons.clear();
+  _axes.clear();
+
+  USHORT num_button_caps = caps.NumberInputButtonCaps;
+  PHIDP_BUTTON_CAPS button_caps = (PHIDP_BUTTON_CAPS)alloca(num_button_caps * sizeof(HIDP_BUTTON_CAPS));
+  _HidP_GetButtonCaps(HidP_Input, button_caps, &num_button_caps, buffer);
+
+  for (USHORT i = 0; i < num_button_caps; ++i) {
+    HIDP_BUTTON_CAPS &cap = button_caps[i];
+    int upper = 0;
+    if (cap.IsRange) {
+      upper = (cap.Range.UsageMax - cap.Range.UsageMin);
+
+      if (device_cat.is_debug()) {
+        device_cat.debug()
+          << "Found button range: DataIndex=" << dec
+          << cap.Range.DataIndexMin << ".." << cap.Range.DataIndexMax
+          << ", ReportID=" << (int)cap.ReportID
+          << ", UsagePage=0x" << hex << cap.UsagePage
+          << ", Usage=0x" << cap.Range.UsageMin << "..0x" << cap.Range.UsageMax
+          << dec << "\n";
+      }
+    } else {
+      if (device_cat.is_debug()) {
+        device_cat.debug()
+          << "Found button: DataIndex=" << dec << cap.NotRange.DataIndex
+          << ", ReportID=" << dec << (int)cap.ReportID
+          << ", UsagePage=0x" << cap.UsagePage
+          << ", Usage=0x" << cap.NotRange.Usage
+          << dec << "\n";
+      }
+    }
+
+    // Windows will only tell us which buttons in a report are "on", so we
+    // need to keep track of which buttons exist in which report so that we
+    // can figure out which ones are off.
+    if (cap.ReportID >= _report_buttons.size()) {
+      _report_buttons.resize(cap.ReportID + 1);
+    }
+    for (int j = 0; j <= upper; ++j) {
+      USAGE usage = j + cap.Range.UsageMin;
+      USHORT data_index = j + cap.Range.DataIndexMin;
+      ButtonHandle handle = ButtonHandle::none();
+      switch (cap.UsagePage) {
+      case HID_USAGE_PAGE_BUTTON:
+        if (_device_class == DeviceClass::gamepad) {
+          if (usage < sizeof(gamepad_buttons_common) / sizeof(ButtonHandle)) {
+            handle = gamepad_buttons[usage];
+          }
+        } else if (_device_class == DeviceClass::flight_stick) {
+          if (usage > 0) {
+            handle = GamepadButton::joystick(usage - 1);
+          }
+        } else if (_device_class == DeviceClass::mouse) {
+          // In Panda, wheel and right button are flipped around...
+          int button = (usage == 2 || usage == 3) ? (4 - usage) : (usage - 1);
+          handle = MouseButton::button(button);
+        }
+        break;
+      }
+
+      int button_index = _buttons.size();
+      _report_buttons[cap.ReportID].set_bit(button_index);
+      _indices[data_index] = Index::button(button_index);
+      _buttons.push_back(ButtonState(handle));
+    }
+  }
+
+  USHORT num_value_caps = caps.NumberInputValueCaps;
+  PHIDP_VALUE_CAPS value_caps = (PHIDP_VALUE_CAPS)alloca(num_value_caps * sizeof(HIDP_VALUE_CAPS));
+  _HidP_GetValueCaps(HidP_Input, value_caps, &num_value_caps, buffer);
+
+  _hat_data_index = -1;
+
+  for (USHORT i = 0; i < num_value_caps; ++i) {
+    HIDP_VALUE_CAPS &cap = value_caps[i];
+    int upper = 0;
+    if (cap.IsRange) {
+      upper = (cap.Range.UsageMax - cap.Range.UsageMin);
+
+      if (device_cat.is_debug()) {
+        device_cat.debug()
+          << "Found value range: DataIndex=" << dec
+          << cap.Range.DataIndexMin << ".." << cap.Range.DataIndexMax
+          << ", ReportID=" << (int)cap.ReportID
+          << ", UsagePage=0x" << hex << cap.UsagePage
+          << ", Usage=0x" << cap.Range.UsageMin << "..0x" << cap.Range.UsageMax
+          << dec << ", LogicalMin=" << cap.LogicalMin
+          << ", LogicalMax=" << cap.LogicalMax << "\n";
+      }
+    } else {
+      if (device_cat.is_debug()) {
+        device_cat.debug()
+          << "Found value: DataIndex=" << dec << cap.NotRange.DataIndex
+          << ", ReportID=" << dec << (int)cap.ReportID
+          << ", UsagePage=0x" << hex << cap.UsagePage
+          << ", Usage=0x" << cap.NotRange.Usage
+          << dec << ", LogicalMin=" << cap.LogicalMin
+          << ", LogicalMax=" << cap.LogicalMax << "\n";
+      }
+    }
+
+    for (int j = 0; j <= upper; ++j) {
+      USAGE usage = j + cap.Range.UsageMin;
+      USHORT data_index = j + cap.Range.DataIndexMin;
+      bool is_signed = true;
+
+      // My gamepads give this odd invalid range.
+      if (cap.LogicalMin == 0 && cap.LogicalMax == -1) {
+        cap.LogicalMax = 65535;
+        is_signed = false;
+      }
+
+      Axis axis = Axis::none;
+      switch (cap.UsagePage) {
+      case HID_USAGE_PAGE_GENERIC:
+        switch (usage) {
+          case HID_USAGE_GENERIC_X:
+          if (_device_class == DeviceClass::gamepad) {
+            axis = Axis::left_x;
+          } else if (_device_class == DeviceClass::flight_stick) {
+            axis = Axis::roll;
+          } else {
+            axis = Axis::x;
+          }
+          break;
+        case HID_USAGE_GENERIC_Y:
+          if (_device_class == DeviceClass::gamepad) {
+            axis = Axis::left_y;
+            swap(cap.LogicalMin, cap.LogicalMax);
+          } else if (_device_class == DeviceClass::flight_stick) {
+            axis = Axis::pitch;
+          } else {
+            axis = Axis::y;
+            swap(cap.LogicalMin, cap.LogicalMax);
+          }
+          break;
+        case HID_USAGE_GENERIC_Z:
+          if (_device_class == DeviceClass::gamepad) {
+            axis = Axis::left_trigger;
+          } else if (_device_class == DeviceClass::flight_stick) {
+            axis = Axis::throttle;
+          } else {
+            axis = Axis::z;
+            swap(cap.LogicalMin, cap.LogicalMax);
+          }
+          break;
+        case HID_USAGE_GENERIC_RX:
+          if (_device_class == DeviceClass::gamepad) {
+            axis = Axis::right_x;
+          } else {
+            axis = Axis::pitch;
+          }
+          break;
+        case HID_USAGE_GENERIC_RY:
+          if (_device_class == DeviceClass::gamepad) {
+            axis = Axis::right_y;
+          } else {
+            axis = Axis::roll;
+          }
+          swap(cap.LogicalMin, cap.LogicalMax);
+          break;
+        case HID_USAGE_GENERIC_RZ:
+          if (_device_class == DeviceClass::gamepad) {
+            axis = Axis::right_trigger;
+          } else {
+            // Flip to match Panda's convention for heading.
+            axis = Axis::yaw;
+            swap(cap.LogicalMin, cap.LogicalMax);
+          }
+          break;
+        case HID_USAGE_GENERIC_SLIDER:
+          // Flip to match Panda's convention for heading.
+          axis = Axis::rudder;
+          swap(cap.LogicalMin, cap.LogicalMax);
+          break;
+        case HID_USAGE_GENERIC_WHEEL:
+          axis = Axis::wheel;
+          break;
+        case HID_USAGE_GENERIC_HATSWITCH:
+          // This is handled specially.
+          _hat_data_index = data_index;
+          _hat_data_minimum = cap.LogicalMin;
+          continue;
+        }
+        break;
+      }
+
+      int axis_index;
+      if (_vendor_id == 0x044f && _product_id == 0xb108 && axis == Axis::throttle) {
+        // T.Flight Hotas X throttle is reversed and can go backwards.
+        axis_index = add_axis(axis, cap.LogicalMax, cap.LogicalMin, true);
+      } else if (!is_signed) {
+        // All axes on the weird XInput-style mappings go from -1 to 1
+        axis_index = add_axis(axis, cap.LogicalMin, cap.LogicalMax, true);
+      } else {
+        axis_index = add_axis(axis, cap.LogicalMin, cap.LogicalMax);
+      }
+      _indices[data_index] = Index::axis(axis_index, is_signed);
+    }
+  }
+
+  // Do we need to emulate a hat switch or directional pad?
+  if (_hat_data_index != -1) {
+    _hat_left_button = (int)_buttons.size();
+    if (_device_class == DeviceClass::gamepad) {
+      _buttons.push_back(ButtonState(GamepadButton::dpad_left()));
+      _buttons.push_back(ButtonState(GamepadButton::dpad_right()));
+      _buttons.push_back(ButtonState(GamepadButton::dpad_down()));
+      _buttons.push_back(ButtonState(GamepadButton::dpad_up()));
+    } else {
+      _buttons.push_back(ButtonState(GamepadButton::hat_left()));
+      _buttons.push_back(ButtonState(GamepadButton::hat_right()));
+      _buttons.push_back(ButtonState(GamepadButton::hat_down()));
+      _buttons.push_back(ButtonState(GamepadButton::hat_up()));
+    }
+  }
+
+  _max_data_count = _HidP_MaxDataListLength(HidP_Input, buffer);
+
+  _handle = handle;
+  _is_connected = true;
+  return true;
+}
+
+/**
+ * Called by InputDeviceManager when this device is removed.
+ */
+void WinRawInputDevice::
+on_removal() {
+  LightMutexHolder holder(_lock);
+  _is_connected = false;
+  _handle = nullptr;
+  if (_preparsed != nullptr) {
+    delete _preparsed;
+    _preparsed = nullptr;
+  }
+  _indices.clear();
+  _report_buttons.clear();
+}
+
+void WinRawInputDevice::
+on_input(PRAWINPUT input) {
+  nassertv(input != nullptr);
+  nassertv(_preparsed != nullptr);
+
+  BYTE *ptr = input->data.hid.bRawData;
+  if (input->data.hid.dwSizeHid == 0) {
+    return;
+  }
+
+  PHIDP_DATA data = (PHIDP_DATA)alloca(sizeof(HIDP_DATA) * _max_data_count);
+  nassertv(data != nullptr);
+  ULONG count;
+
+  LightMutexHolder holder(_lock);
+
+  for (DWORD i = 0; i < input->data.hid.dwCount; ++i) {
+    // The first byte is the report identifier.  We need it to figure out
+    // which buttons are off, since each report only contains the buttons that
+    // are "on".
+    UCHAR report_id = ptr[0];
+    BitArray unset_buttons = _report_buttons[report_id];
+
+    count = _max_data_count;
+    NTSTATUS status = _HidP_GetData(HidP_Input, data, &count, (PHIDP_PREPARSED_DATA)_preparsed, (PCHAR)ptr, input->data.hid.dwSizeHid);
+    if (status == HIDP_STATUS_SUCCESS) {
+      for (ULONG di = 0; di < count; ++di) {
+        if (data[di].DataIndex != _hat_data_index) {
+          const Index &idx = _indices[data[di].DataIndex];
+          if (idx._axis >= 0) {
+            if (idx._signed) {
+              axis_changed(idx._axis, (SHORT)data[di].RawValue);
+            } else {
+              axis_changed(idx._axis, data[di].RawValue);
+            }
+          }
+          if (idx._button >= 0) {
+            unset_buttons.clear_bit(idx._button);
+            button_changed(idx._button, (data[di].On != FALSE));
+          }
+        } else {
+          int value = (int)data[di].RawValue - _hat_data_minimum;
+          button_changed(_hat_left_button + 0, value >= 5 && value <= 7); // left
+          button_changed(_hat_left_button + 1, value >= 1 && value <= 3); // right
+          button_changed(_hat_left_button + 2, value >= 3 && value <= 5); // down
+          button_changed(_hat_left_button + 3, value == 7 || value == 0 || value == 1); // up
+        }
+      }
+
+      // Now unset the buttons in this report that aren't pressed.
+      int button_index = unset_buttons.get_lowest_on_bit();
+      while (button_index >= 0) {
+        button_changed(button_index, false);
+        unset_buttons.clear_bit(button_index);
+        button_index = unset_buttons.get_lowest_on_bit();
+      }
+    } else if (device_cat.is_spam()) {
+      device_cat.spam()
+        << "Failed to get data from raw device " << _path
+        << " (error 0x" << std::hex << (status & 0xffffffffu) << std::dec << ")\n";
+    }
+
+    ptr += input->data.hid.dwSizeHid;
+  }
+}
+
+/**
+ * Polls the input device for new activity, to ensure it contains the latest
+ * events.  This will only have any effect for some types of input devices;
+ * others may be updated automatically, and this method will be a no-op.
+ */
+void WinRawInputDevice::
+do_poll() {
+}
+
+#endif  // _WIN32

+ 85 - 0
panda/src/device/winRawInputDevice.h

@@ -0,0 +1,85 @@
+/**
+ * PANDA 3D SOFTWARE
+ * Copyright (c) Carnegie Mellon University.  All rights reserved.
+ *
+ * All use of this software is subject to the terms of the revised BSD
+ * license.  You should have received a copy of this license along
+ * with this source code in a file named "LICENSE."
+ *
+ * @file winRawInputDevice.h
+ * @author rdb
+ * @date 2018-01-19
+ */
+
+#ifndef WINRAWINPUTDEVICE_H
+#define WINRAWINPUTDEVICE_H
+
+#include "pandabase.h"
+#include "inputDevice.h"
+#include "bitArray.h"
+
+#if defined(_WIN32) && !defined(CPPPARSER)
+
+class WinInputDeviceManager;
+
+/**
+ * This implementation of InputDevice uses the Win32 raw input API and the HID
+ * parser library to support a wide range of devices.
+ */
+class EXPCL_PANDA_DEVICE WinRawInputDevice final : public InputDevice {
+public:
+  WinRawInputDevice(WinInputDeviceManager *manager, const char *path);
+  ~WinRawInputDevice();
+
+  bool on_arrival(HANDLE handle, const RID_DEVICE_INFO &info, std::string name);
+  void on_removal();
+  void on_input(PRAWINPUT input);
+
+private:
+  virtual void do_poll();
+
+private:
+  const std::string _path;
+  HANDLE _handle;
+  DWORD _size;
+  void *_preparsed;
+  ULONG _max_data_count;
+  ULONG _max_usage_count;
+
+  // Indexed by report ID
+  pvector<BitArray> _report_buttons;
+
+  // Either a button index or a axis index.
+  struct Index {
+    Index() : _button(-1), _axis(-1) {}
+
+    static Index button(int index) {
+      Index idx;
+      idx._button = index;
+      return idx;
+    }
+    static Index axis(int index, bool is_signed=true) {
+      Index idx;
+      idx._axis = index;
+      idx._signed = is_signed;
+      return idx;
+    }
+
+    int _button;
+    int _axis;
+    bool _signed;
+  };
+
+  // Maps a "data index" to either button index or axis index.
+  pvector<Index> _indices;
+  int _hat_data_index;
+  int _hat_data_minimum;
+  int _hat_left_button;
+
+  WinInputDeviceManager *_manager;
+  friend class WinInputDeviceManager;
+};
+
+#endif  // _WIN32
+
+#endif

+ 506 - 0
panda/src/device/xInputDevice.cxx

@@ -0,0 +1,506 @@
+/**
+ * PANDA 3D SOFTWARE
+ * Copyright (c) Carnegie Mellon University.  All rights reserved.
+ *
+ * All use of this software is subject to the terms of the revised BSD
+ * license.  You should have received a copy of this license along
+ * with this source code in a file named "LICENSE."
+ *
+ * @file xInputDevice.cxx
+ * @author rdb
+ * @date 2015-07-15
+ */
+
+#include "xInputDevice.h"
+
+#if defined(_WIN32) && !defined(CPPPARSER)
+
+#include "gamepadButton.h"
+#include "inputDeviceManager.h"
+#include "string_utils.h"
+
+#include <XInput.h>
+#include <CfgMgr32.h>
+
+#ifndef XUSER_MAX_COUNT
+#define XUSER_MAX_COUNT 4
+#endif
+
+#ifndef XINPUT_CAPS_FFB_SUPPORTED
+#define XINPUT_CAPS_FFB_SUPPORTED 0x0001
+#endif
+#ifndef XINPUT_CAPS_NO_NAVIGATION
+#define XINPUT_CAPS_NO_NAVIGATION 0x0010
+#endif
+
+#ifndef BATTERY_DEVTYPE_GAMEPAD
+#define BATTERY_DEVTYPE_GAMEPAD 0x00
+#endif
+
+#ifndef XINPUT_DEVSUBTYPE_WHEEL
+#define XINPUT_DEVSUBTYPE_WHEEL 0x02
+#endif
+#ifndef XINPUT_DEVSUBTYPE_ARCADE_STICK
+#define XINPUT_DEVSUBTYPE_ARCADE_STICK 0x03
+#endif
+#ifndef XINPUT_DEVSUBTYPE_FLIGHT_STICK
+#define XINPUT_DEVSUBTYPE_FLIGHT_STICK 0x04
+#endif
+#ifndef XINPUT_DEVSUBTYPE_DANCE_PAD
+#define XINPUT_DEVSUBTYPE_DANCE_PAD 0x05
+#endif
+#ifndef XINPUT_DEVSUBTYPE_GUITAR
+#define XINPUT_DEVSUBTYPE_GUITAR 0x06
+#endif
+#ifndef XINPUT_DEVSUBTYPE_DRUM_KIT
+#define XINPUT_DEVSUBTYPE_DRUM_KIT 0x08
+#endif
+
+#ifndef BATTERY_TYPE_DISCONNECTED
+#define BATTERY_TYPE_DISCONNECTED 0x00
+#endif
+
+#ifndef BATTERY_TYPE_WIRED
+#define BATTERY_TYPE_WIRED 0x01
+#endif
+
+#ifndef BATTERY_LEVEL_FULL
+#define BATTERY_LEVEL_FULL 0x03
+#endif
+
+typedef struct _XINPUT_BATTERY_INFORMATION {
+  BYTE BatteryType;
+  BYTE BatteryLevel;
+} XINPUT_BATTERY_INFORMATION;
+
+// Undocumented, I figured out how this looks by trial and error.
+typedef struct _XINPUT_BUSINFO {
+  WORD VendorID;
+  WORD ProductID;
+  WORD RevisionID;
+  WORD Unknown1; // Unknown - padding?
+  DWORD InstanceID;
+  DWORD Unknown2;
+  WORD Unknown3;
+} XINPUT_BUSINFO;
+
+typedef struct _XINPUT_CAPABILITIES_EX {
+  BYTE Type;
+  BYTE SubType;
+  WORD Flags;
+  XINPUT_GAMEPAD Gamepad;
+  XINPUT_VIBRATION Vibration;
+
+  // The following fields are undocumented.
+  WORD VendorID;
+  WORD ProductID;
+  WORD RevisionID;
+  WORD Unknown1;
+  WORD Unknown2;
+} XINPUT_CAPABILITIES_EX;
+
+typedef DWORD (*pXInputGetState)(DWORD, XINPUT_STATE *);
+typedef DWORD (*pXInputSetState)(DWORD, XINPUT_VIBRATION *);
+typedef DWORD (*pXInputGetCapabilities)(DWORD, DWORD, XINPUT_CAPABILITIES *);
+typedef DWORD (*pXInputGetCapabilitiesEx)(DWORD, DWORD, DWORD, XINPUT_CAPABILITIES_EX *);
+typedef DWORD (*pXInputGetBatteryInformation)(DWORD, BYTE, XINPUT_BATTERY_INFORMATION *);
+typedef DWORD (*pXInputGetBaseBusInformation)(DWORD, XINPUT_BUSINFO *);
+
+static pXInputGetState get_state = nullptr;
+static pXInputSetState set_state = nullptr;
+static pXInputGetCapabilities get_capabilities = nullptr;
+static pXInputGetCapabilitiesEx get_capabilities_ex = nullptr;
+static pXInputGetBatteryInformation get_battery_information = nullptr;
+static pXInputGetBaseBusInformation get_base_bus_information = nullptr;
+
+bool XInputDevice::_initialized = false;
+
+/**
+ * Protected constructor.  user_index is a number 0-3.
+ */
+XInputDevice::
+XInputDevice(DWORD user_index) :
+  _index(user_index),
+  _last_packet(-1),
+  _last_buttons(0) {
+
+  nassertv(user_index >= 0 && user_index < XUSER_MAX_COUNT);
+
+  _axes.resize(6);
+  _buttons.resize(16);
+}
+
+/**
+ *
+ */
+XInputDevice::
+~XInputDevice() {
+  do_set_vibration(0, 0);
+}
+
+/**
+ * Called when a new input device arrives in the InputDeviceManager.  This
+ * method checks whether it matches this XInput device.
+ */
+bool XInputDevice::
+check_arrival(const RID_DEVICE_INFO &info, DEVINST inst,
+              const std::string &name, const std::string &manufacturer) {
+  LightMutexHolder holder(_lock);
+  if (_is_connected) {
+    return false;
+  }
+
+  if (!_initialized) {
+    nassertr_always(init_xinput(), false);
+  }
+
+  XINPUT_CAPABILITIES_EX caps = {0};
+  XINPUT_STATE state;
+  if ((get_capabilities_ex && get_capabilities_ex(1, _index, 0, &caps) != ERROR_SUCCESS) &&
+       get_capabilities(_index, 0, (XINPUT_CAPABILITIES *)&caps) != ERROR_SUCCESS) {
+    return false;
+  }
+
+  // Extra check for VID/PID if we have it, just to be sure.
+  if ((caps.VendorID != 0 && caps.VendorID != info.hid.dwVendorId) ||
+      (caps.ProductID != 0 && caps.ProductID != info.hid.dwProductId)) {
+    return false;
+  }
+
+  // Yes, take the name and manufacturer.
+  if (!name.empty()) {
+    _name = name;
+  } else {
+    _name = "XInput Device #";
+    _name += format_string(_index + 1);
+  }
+  _manufacturer = manufacturer;
+
+  if (inst && caps.ProductID == 0 && caps.RevisionID != 0) {
+    // XInput does not report a product ID for the Xbox 360 wireless adapter.
+    // Instead, we check that the RevisionID matches.
+    char buffer[4096];
+    ULONG buflen = sizeof(buffer);
+    if (CM_Get_DevNode_Registry_Property(inst, CM_DRP_HARDWAREID, 0, buffer, &buflen, 0) == CR_SUCCESS) {
+      std::string ids(buffer, buflen);
+      char revstr[16];
+      sprintf(revstr, "REV_%04x", caps.RevisionID);
+      if (ids.find(revstr) == std::string::npos) {
+        return false;
+      }
+    }
+  }
+
+  _is_connected = true;
+  init_device(caps, state);
+  _vendor_id = info.hid.dwVendorId;
+  _product_id = info.hid.dwProductId;
+  return true;
+}
+
+/**
+ * Called periodically by the InputDeviceManager to detect whether the device
+ * is currently connected.
+ * Returns true if the device wasn't connected, but now is.
+ */
+void XInputDevice::
+detect(InputDeviceManager *mgr) {
+  bool connected = false;
+
+  XINPUT_CAPABILITIES_EX caps = {0};
+  XINPUT_STATE state;
+  if (((get_capabilities_ex && get_capabilities_ex(1, _index, 0, &caps) == ERROR_SUCCESS) ||
+       get_capabilities(_index, 0, (XINPUT_CAPABILITIES *)&caps) == ERROR_SUCCESS) &&
+      get_state(_index, &state) == ERROR_SUCCESS) {
+    connected = true;
+  } else {
+    connected = false;
+  }
+
+  LightMutexHolder holder(_lock);
+  if (connected == _is_connected) {
+    // Nothing changed.
+    return;
+  }
+  _is_connected = connected;
+
+  if (connected) {
+    init_device(caps, state);
+    mgr->add_device(this);
+  } else {
+    mgr->remove_device(this);
+  }
+}
+
+/**
+ * Static method to initialize the XInput library.
+ */
+bool XInputDevice::
+init_xinput() {
+  if (device_cat.is_debug()) {
+    device_cat.debug() << "Initializing XInput library.\n";
+  }
+
+  _initialized = true;
+  const char *dll_name = "Xinput1_4.dll";
+  HMODULE module = LoadLibraryA(dll_name);
+
+  // If we didn't find XInput 1.4, fall back to the older 1.3 version.
+  if (!module) {
+    if (device_cat.is_debug()) {
+      device_cat.debug()
+        << "Xinput1_4.dll not found, falling back to Xinput1_3.dll\n";
+    }
+
+    dll_name = "Xinput1_3.dll";
+    module = LoadLibraryA(dll_name);
+  }
+
+  if (module) {
+    if (device_cat.is_debug()) {
+      device_cat.debug()
+        << "Successfully loaded " << dll_name << "\n";
+    }
+
+    // Undocumented version (XInputGetStateEx) that includes a
+    // state bit for the guide button.
+    get_state = (pXInputGetState)GetProcAddress(module, MAKEINTRESOURCE(100));
+    if (get_state == nullptr) {
+      get_state = (pXInputGetState)GetProcAddress(module, "XInputGetState");
+      if (get_state == nullptr) {
+        device_cat.error()
+          << "Failed to find function XInputGetState in " << dll_name << ".\n";
+        return false;
+      }
+    }
+
+    set_state = (pXInputSetState)GetProcAddress(module, "XInputSetState");
+    if (set_state == nullptr) {
+      device_cat.error()
+        << "Failed to find function XInputSetState in " << dll_name << ".\n";
+      return false;
+    }
+
+    get_capabilities = (pXInputGetCapabilities)GetProcAddress(module, "XInputGetCapabilities");
+    if (get_capabilities == nullptr) {
+      device_cat.error()
+        << "Failed to find function XInputGetCapabilities in " << dll_name << ".\n";
+      return false;
+    }
+
+    get_battery_information = (pXInputGetBatteryInformation)GetProcAddress(module, "XInputGetBatteryInformation");
+    get_base_bus_information = (pXInputGetBaseBusInformation)GetProcAddress(module, MAKEINTRESOURCE(104));
+    get_capabilities_ex = (pXInputGetCapabilitiesEx)GetProcAddress(module, MAKEINTRESOURCE(108));
+    return true;
+  }
+
+  device_cat.error()
+    << "Failed to load Xinput1_4.dll or Xinput1_3.dll.\n";
+  return false;
+}
+
+/**
+ * Initializes the device.  Called when the device was just connected.
+ * Assumes either the lock is held or this is called from the constructor.
+ */
+void XInputDevice::
+init_device(const XINPUT_CAPABILITIES_EX &caps, const XINPUT_STATE &state) {
+  nassertv(_initialized);
+  // It seems that the Xbox One controller is reported as having a DevType of
+  // zero, at least when I tested in with XInput 1.3 on Windows 7.
+  //if (caps.Type == XINPUT_DEVTYPE_GAMEPAD) {
+
+  // For subtypes and mappings, see this page:
+  // https://msdn.microsoft.com/en-us/library/windows/desktop/hh405050.aspx
+  switch (caps.SubType) {
+  default:
+  case XINPUT_DEVSUBTYPE_GAMEPAD:
+    _device_class = DeviceClass::gamepad;
+    _axes[0].axis = Axis::left_trigger;
+    _axes[1].axis = Axis::right_trigger;
+    _axes[2].axis = Axis::left_x;
+    _axes[3].axis = Axis::left_y;
+    _axes[4].axis = Axis::right_x;
+    _axes[5].axis = Axis::right_y;
+    break;
+
+  case XINPUT_DEVSUBTYPE_WHEEL:
+    _device_class = DeviceClass::steering_wheel;
+    _axes[0].axis = Axis::brake;
+    _axes[1].axis = Axis::accelerator;
+    _axes[2].axis = Axis::wheel;
+    _axes[3].axis = Axis::none;
+    _axes[4].axis = Axis::none;
+    _axes[5].axis = Axis::none;
+    break;
+
+  case XINPUT_DEVSUBTYPE_FLIGHT_STICK:
+    _device_class = DeviceClass::flight_stick;
+    _axes[0].axis = Axis::yaw;
+    _axes[1].axis = Axis::throttle;
+    _axes[2].axis = Axis::roll;
+    _axes[3].axis = Axis::pitch;
+    _axes[4].axis = Axis::none;
+    _axes[5].axis = Axis::none;
+    break;
+
+  case XINPUT_DEVSUBTYPE_DANCE_PAD:
+    _device_class = DeviceClass::dance_pad;
+    _axes[0].axis = Axis::none;
+    _axes[1].axis = Axis::none;
+    _axes[2].axis = Axis::none;
+    _axes[3].axis = Axis::none;
+    _axes[4].axis = Axis::none;
+    _axes[5].axis = Axis::none;
+    break;
+  }
+
+  _axes[0]._scale = 1.0 / 255.0;
+  _axes[1]._scale = 1.0 / 255.0;
+  _axes[2]._scale = 1.0 / 32767.5;
+  _axes[3]._scale = 1.0 / 32767.5;
+  _axes[4]._scale = 1.0 / 32767.5;
+  _axes[5]._scale = 1.0 / 32767.5;
+
+  _axes[2]._bias = 0.5 / 32767.5;
+  _axes[3]._bias = 0.5 / 32767.5;
+  _axes[4]._bias = 0.5 / 32767.5;
+  _axes[5]._bias = 0.5 / 32767.5;
+
+  if (caps.Flags & XINPUT_CAPS_NO_NAVIGATION) {
+    _buttons[0].handle = ButtonHandle::none();
+    _buttons[1].handle = ButtonHandle::none();
+    _buttons[2].handle = ButtonHandle::none();
+    _buttons[3].handle = ButtonHandle::none();
+    _buttons[4].handle = ButtonHandle::none();
+    _buttons[5].handle = ButtonHandle::none();
+  } else {
+    _buttons[0].handle = GamepadButton::dpad_up();
+    _buttons[1].handle = GamepadButton::dpad_down();
+    _buttons[2].handle = GamepadButton::dpad_left();
+    _buttons[3].handle = GamepadButton::dpad_right();
+    _buttons[4].handle = GamepadButton::start();
+    _buttons[5].handle = GamepadButton::back();
+  }
+  _buttons[6].handle = GamepadButton::lstick();
+  _buttons[7].handle = GamepadButton::rstick();
+  _buttons[8].handle = GamepadButton::lshoulder();
+  _buttons[9].handle = GamepadButton::rshoulder();
+  _buttons[10].handle = GamepadButton::guide();
+  _buttons[11].handle = GamepadButton::face_a();
+  _buttons[12].handle = GamepadButton::face_b();
+  _buttons[13].handle = GamepadButton::face_x();
+  _buttons[14].handle = GamepadButton::face_y();
+
+  if (caps.Vibration.wLeftMotorSpeed != 0 ||
+      caps.Vibration.wRightMotorSpeed != 0) {
+    enable_feature(Feature::vibration);
+  }
+
+  if (get_battery_information != nullptr) {
+    XINPUT_BATTERY_INFORMATION batt;
+    if (get_battery_information(_index, BATTERY_DEVTYPE_GAMEPAD, &batt) == ERROR_SUCCESS) {
+      if (batt.BatteryType != BATTERY_TYPE_DISCONNECTED &&
+          batt.BatteryType != BATTERY_TYPE_WIRED) {
+        // This device has a battery.  Report the battery level.
+        enable_feature(Feature::battery);
+        _battery_data.level = batt.BatteryLevel;
+        _battery_data.max_level = BATTERY_LEVEL_FULL;
+      }
+    }
+  }
+
+  WORD buttons = state.Gamepad.wButtons;
+  WORD mask = 1;
+  for (int i = 0; i < 16; ++i) {
+    // Set the state without triggering a button event.
+    _buttons[i]._state = (buttons & mask) ? S_down : S_up;
+    mask <<= 1;
+    if (i == 10) {
+      // XInput skips 0x0800.
+      mask <<= 1;
+    }
+  }
+
+  axis_changed(0, state.Gamepad.bLeftTrigger);
+  axis_changed(1, state.Gamepad.bRightTrigger);
+  axis_changed(2, state.Gamepad.sThumbLX);
+  axis_changed(3, state.Gamepad.sThumbLY);
+  axis_changed(4, state.Gamepad.sThumbRX);
+  axis_changed(5, state.Gamepad.sThumbRY);
+
+  _last_buttons = buttons;
+  _last_packet = state.dwPacketNumber;
+}
+
+/**
+ * Sets the vibration strength.  The first argument controls a low-frequency
+ * motor, if present, and the latter controls a high-frequency motor.
+ * The values are within the 0-1 range.
+ */
+void XInputDevice::
+do_set_vibration(double strong, double weak) {
+  nassertv_always(_is_connected);
+
+  XINPUT_VIBRATION vibration;
+  vibration.wLeftMotorSpeed = strong * 0xffff;
+  vibration.wRightMotorSpeed = weak * 0xffff;
+  set_state(_index, &vibration);
+}
+
+/**
+ * Polls the input device for new activity, to ensure it contains the latest
+ * events.  This will only have any effect for some types of input devices;
+ * others may be updated automatically, and this method will be a no-op.
+ */
+void XInputDevice::
+do_poll() {
+  // Not sure why someone would call this on a disconnected device.
+  if (!_is_connected) {
+    return;
+  }
+
+  XINPUT_STATE state;
+
+  if (get_state(_index, &state) != ERROR_SUCCESS) {
+    // Device was disconnected.
+    if (_is_connected) {
+      _is_connected = false;
+      InputDeviceManager *mgr = InputDeviceManager::get_global_ptr();
+      mgr->remove_device(this);
+    }
+    return;
+  }
+
+  if (state.dwPacketNumber == _last_packet) {
+    // No change since last time we asked.
+    return;
+  }
+
+  // Did any buttons change state?
+  WORD changed_buttons = _last_buttons ^ state.Gamepad.wButtons;
+
+  WORD mask = 1;
+  for (int i = 0; i < 16; ++i) {
+    if (changed_buttons & mask) {
+      button_changed(i, (state.Gamepad.wButtons & mask) != 0);
+    }
+    mask <<= 1;
+    if (i == 10) {
+      // XInput skips 0x0800.
+      mask <<= 1;
+    }
+  }
+
+  axis_changed(0, state.Gamepad.bLeftTrigger);
+  axis_changed(1, state.Gamepad.bRightTrigger);
+  axis_changed(2, state.Gamepad.sThumbLX);
+  axis_changed(3, state.Gamepad.sThumbLY);
+  axis_changed(4, state.Gamepad.sThumbRX);
+  axis_changed(5, state.Gamepad.sThumbRY);
+
+  _last_buttons = state.Gamepad.wButtons;
+  _last_packet = state.dwPacketNumber;
+}
+
+#endif  // _WIN32

+ 60 - 0
panda/src/device/xInputDevice.h

@@ -0,0 +1,60 @@
+/**
+ * PANDA 3D SOFTWARE
+ * Copyright (c) Carnegie Mellon University.  All rights reserved.
+ *
+ * All use of this software is subject to the terms of the revised BSD
+ * license.  You should have received a copy of this license along
+ * with this source code in a file named "LICENSE."
+ *
+ * @file xInputDevice.h
+ * @author rdb
+ * @date 2015-07-15
+ */
+
+#ifndef XINPUTDEVICE_H
+#define XINPUTDEVICE_H
+
+#include "pandabase.h"
+#include "inputDevice.h"
+
+#if defined(_WIN32) && !defined(CPPPARSER)
+
+#include <CfgMgr32.h>
+
+class InputDeviceManager;
+
+typedef struct _XINPUT_CAPABILITIES_EX XINPUT_CAPABILITIES_EX;
+typedef struct _XINPUT_STATE XINPUT_STATE;
+
+typedef struct tagRID_DEVICE_INFO RID_DEVICE_INFO;
+
+/**
+ * This implementation of InputDevice uses Microsoft's XInput library to
+ * interface with an Xbox 360 game controller.
+ */
+class EXPCL_PANDA_DEVICE XInputDevice final : public InputDevice {
+public:
+  XInputDevice(DWORD user_index);
+  ~XInputDevice();
+
+  bool check_arrival(const RID_DEVICE_INFO &info, DEVINST inst,
+                     const std::string &name, const std::string &manufacturer);
+  void detect(InputDeviceManager *mgr);
+  static bool init_xinput();
+
+private:
+  void init_device(const XINPUT_CAPABILITIES_EX &caps, const XINPUT_STATE &state);
+  virtual void do_set_vibration(double strong, double weak);
+  virtual void do_poll();
+
+private:
+  const DWORD _index;
+  DWORD _last_packet;
+  WORD _last_buttons;
+
+  static bool _initialized;
+};
+
+#endif  // _WIN32
+
+#endif

+ 1 - 16
panda/src/display/callbackGraphicsWindow.cxx

@@ -47,28 +47,13 @@ CallbackGraphicsWindow::
 ~CallbackGraphicsWindow() {
 }
 
-
-/**
- * Returns a writable reference to the nth input device (mouse).  This is
- * intended to be used for the window implementation to record mouse and
- * keyboard input information for the Panda system.
- */
-GraphicsWindowInputDevice &CallbackGraphicsWindow::
-get_input_device(int device) {
-  LightMutexHolder holder(_input_lock);
-  nassertr(device >= 0 && device < (int)_input_devices.size(), _input_devices[0]);
-  return _input_devices[device];
-}
-
 /**
  * Adds a new input device (mouse) to the window with the indicated name.
  * Returns the index of the new device.
  */
 int CallbackGraphicsWindow::
 create_input_device(const std::string &name) {
-  GraphicsWindowInputDevice device =
-    GraphicsWindowInputDevice::pointer_and_keyboard(this, name);
-  return add_input_device(device);
+  return add_input_device(GraphicsWindowInputDevice::pointer_and_keyboard(this, name));
 }
 
 /**

+ 0 - 1
panda/src/display/callbackGraphicsWindow.h

@@ -178,7 +178,6 @@ PUBLISHED:
   INLINE void clear_render_callback();
   INLINE CallbackObject *get_render_callback() const;
 
-  GraphicsWindowInputDevice &get_input_device(int device);
   int create_input_device(const std::string &name);
 
 public:

+ 5 - 1
panda/src/display/config_display.cxx

@@ -21,9 +21,11 @@
 #include "graphicsPipe.h"
 #include "graphicsOutput.h"
 #include "graphicsBuffer.h"
-#include "graphicsWindow.h"
 #include "graphicsDevice.h"
+#include "graphicsWindow.h"
+#include "graphicsWindowInputDevice.h"
 #include "graphicsWindowProcCallbackData.h"
+#include "mouseAndKeyboard.h"
 #include "nativeWindowHandle.h"
 #include "parasiteBuffer.h"
 #include "pandaSystem.h"
@@ -510,7 +512,9 @@ init_libdisplay() {
   GraphicsPipe::init_type();
   GraphicsStateGuardian::init_type();
   GraphicsWindow::init_type();
+  GraphicsWindowInputDevice::init_type();
   GraphicsWindowProcCallbackData::init_type();
+  MouseAndKeyboard::init_type();
   NativeWindowHandle::init_type();
   ParasiteBuffer::init_type();
   StandardMunger::init_type();

+ 24 - 76
panda/src/display/graphicsWindow.cxx

@@ -247,6 +247,17 @@ get_num_input_devices() const {
   return result;
 }
 
+/**
+ * Returns the nth input device associated with the window.  Typically, a
+ * window will have exactly one input device: the keyboard/mouse pair.
+ */
+InputDevice *GraphicsWindow::
+get_input_device(int device) const {
+  LightMutexHolder holder(_input_lock);
+  nassertr(device >= 0 && device < (int)_input_devices.size(), NULL);
+  return _input_devices[device];
+}
+
 /**
  * Returns the name of the nth input device.
  */
@@ -256,7 +267,7 @@ get_input_device_name(int device) const {
   {
     LightMutexHolder holder(_input_lock);
     nassertr(device >= 0 && device < (int)_input_devices.size(), "");
-    result = _input_devices[device].get_name();
+    result = _input_devices[device]->get_name();
   }
   return result;
 }
@@ -271,7 +282,7 @@ has_pointer(int device) const {
   {
     LightMutexHolder holder(_input_lock);
     nassertr(device >= 0 && device < (int)_input_devices.size(), false);
-    result = _input_devices[device].has_pointer();
+    result = _input_devices[device]->has_pointer();
   }
   return result;
 }
@@ -285,7 +296,7 @@ has_keyboard(int device) const {
   {
     LightMutexHolder holder(_input_lock);
     nassertr(device >= 0 && device < (int)_input_devices.size(), false);
-    result = _input_devices[device].has_keyboard();
+    result = _input_devices[device]->has_keyboard();
   }
   return result;
 }
@@ -306,7 +317,7 @@ void GraphicsWindow::
 enable_pointer_events(int device) {
   LightMutexHolder holder(_input_lock);
   nassertv(device >= 0 && device < (int)_input_devices.size());
-  _input_devices[device].enable_pointer_events();
+  _input_devices[device]->enable_pointer_events();
 }
 
 /**
@@ -316,28 +327,30 @@ void GraphicsWindow::
 disable_pointer_events(int device) {
   LightMutexHolder holder(_input_lock);
   nassertv(device >= 0 && device < (int)_input_devices.size());
-  _input_devices[device].disable_pointer_events();
+  _input_devices[device]->disable_pointer_events();
 }
 
 /**
  * See GraphicsWindowInputDevice::enable_pointer_mode
  */
+/*
 void GraphicsWindow::
 enable_pointer_mode(int device, double speed) {
   LightMutexHolder holder(_input_lock);
   nassertv(device >= 0 && device < (int)_input_devices.size());
-  _input_devices[device].enable_pointer_mode(speed);
-}
+  _input_devices[device]->enable_pointer_mode(speed);
+}*/
 
 /**
  * See GraphicsWindowInputDevice::disable_pointer_mode
  */
+/*
 void GraphicsWindow::
 disable_pointer_mode(int device) {
   LightMutexHolder holder(_input_lock);
   nassertv(device >= 0 && device < (int)_input_devices.size());
-  _input_devices[device].disable_pointer_mode();
-}
+  _input_devices[device]->disable_pointer_mode();
+}*/
 
 /**
  * Returns the MouseData associated with the nth input device's pointer.  This
@@ -350,7 +363,7 @@ get_pointer(int device) const {
   {
     LightMutexHolder holder(_input_lock);
     nassertr(device >= 0 && device < (int)_input_devices.size(), MouseData());
-    result = _input_devices[device].get_pointer();
+    result = ((const GraphicsWindowInputDevice *)_input_devices[device].p())->get_pointer();
   }
   return result;
 }
@@ -377,70 +390,6 @@ close_ime() {
   return;
 }
 
-/**
- * Returns true if the indicated device has a pending button event (a mouse
- * button or keyboard button down/up), false otherwise.  If this returns true,
- * the particular event may be extracted via get_button_event().
- */
-bool GraphicsWindow::
-has_button_event(int device) const {
-  bool result;
-  {
-    LightMutexHolder holder(_input_lock);
-    nassertr(device >= 0 && device < (int)_input_devices.size(), false);
-    result = _input_devices[device].has_button_event();
-  }
-  return result;
-}
-
-/**
- * Assuming a previous call to has_button_event() returned true, this returns
- * the pending button event.
- */
-ButtonEvent GraphicsWindow::
-get_button_event(int device) {
-  ButtonEvent result;
-  {
-    LightMutexHolder holder(_input_lock);
-    nassertr(device >= 0 && device < (int)_input_devices.size(), ButtonEvent());
-    nassertr(_input_devices[device].has_button_event(), ButtonEvent());
-    result = _input_devices[device].get_button_event();
-  }
-  return result;
-}
-
-/**
- * Returns true if the indicated device has a pending pointer event (a mouse
- * movement).  If this returns true, the particular event may be extracted via
- * get_pointer_events().
- */
-bool GraphicsWindow::
-has_pointer_event(int device) const {
-  bool result;
-  {
-    LightMutexHolder holder(_input_lock);
-    nassertr(device >= 0 && device < (int)_input_devices.size(), false);
-    result = _input_devices[device].has_pointer_event();
-  }
-  return result;
-}
-
-/**
- * Assuming a previous call to has_pointer_event() returned true, this returns
- * the pending pointer event list.
- */
-PT(PointerEventList) GraphicsWindow::
-get_pointer_events(int device) {
-  PT(PointerEventList) result;
-  {
-    LightMutexHolder holder(_input_lock);
-    nassertr(device >= 0 && device < (int)_input_devices.size(), nullptr);
-    nassertr(_input_devices[device].has_pointer_event(), nullptr);
-    result = _input_devices[device].get_pointer_events();
-  }
-  return result;
-}
-
 /**
  * Determines which of the indicated window sizes are supported by available
  * hardware (e.g.  in fullscreen mode).
@@ -739,11 +688,10 @@ system_changed_size(int x_size, int y_size) {
  * new device.
  */
 int GraphicsWindow::
-add_input_device(const GraphicsWindowInputDevice &device) {
+add_input_device(InputDevice *device) {
   LightMutexHolder holder(_input_lock);
   int index = (int)_input_devices.size();
   _input_devices.push_back(device);
-  _input_devices.back().set_device_index(index);
   return index;
 }
 

+ 6 - 10
panda/src/display/graphicsWindow.h

@@ -82,7 +82,9 @@ PUBLISHED:
 
   // Mouse and keyboard routines
   int get_num_input_devices() const;
+  InputDevice *get_input_device(int i) const;
   std::string get_input_device_name(int device) const;
+  MAKE_SEQ(get_input_devices, get_num_input_devices, get_input_device);
   MAKE_SEQ(get_input_device_names, get_num_input_devices, get_input_device_name);
   bool has_pointer(int device) const;
   bool has_keyboard(int device) const;
@@ -90,20 +92,14 @@ PUBLISHED:
 
   void enable_pointer_events(int device);
   void disable_pointer_events(int device);
-  void enable_pointer_mode(int device, double speed);
-  void disable_pointer_mode(int device);
+  /*void enable_pointer_mode(int device, double speed);
+  void disable_pointer_mode(int device);*/
 
   virtual MouseData get_pointer(int device) const;
   virtual bool move_pointer(int device, int x, int y);
   virtual void close_ime();
 
 public:
-  // No need to publish these.
-  bool has_button_event(int device) const;
-  ButtonEvent get_button_event(int device);
-  bool has_pointer_event(int device) const;
-  PT(PointerEventList) get_pointer_events(int device);
-
   virtual void add_window_proc( const GraphicsWindowProc* wnd_proc_object ){};
   virtual void remove_window_proc( const GraphicsWindowProc* wnd_proc_object ){};
   virtual void clear_window_procs(){};
@@ -143,8 +139,8 @@ protected:
   void system_changed_size(int x_size, int y_size);
 
 protected:
-  int add_input_device(const GraphicsWindowInputDevice &device);
-  typedef vector_GraphicsWindowInputDevice InputDevices;
+  int add_input_device(InputDevice *device);
+  typedef pvector<PT(InputDevice)> InputDevices;
   InputDevices _input_devices;
   LightMutex _input_lock;
 

+ 20 - 188
panda/src/display/graphicsWindowInputDevice.I

@@ -12,213 +12,45 @@
  */
 
 /**
- *
- */
-INLINE GraphicsWindowInputDevice::
-GraphicsWindowInputDevice() {
-  LightMutexHolder holder(_lock);
-  _flags = 0;
-}
-
-/**
- *
+ * Returns the PointerData associated with the input device's pointer.  This
+ * only makes sense if has_pointer() also returns true.
  */
-INLINE std::string GraphicsWindowInputDevice::
-get_name() const {
-  LightMutexHolder holder(_lock);
-  return _name;
-}
-
-/**
- *
- */
-INLINE bool GraphicsWindowInputDevice::
-has_pointer() const {
-  LightMutexHolder holder(_lock);
-  return ((_flags & IDF_has_pointer) != 0);
-}
-
-/**
- *
- */
-INLINE bool GraphicsWindowInputDevice::
-has_keyboard() const {
-  LightMutexHolder holder(_lock);
-  return ((_flags & IDF_has_keyboard) != 0);
-}
-
-/**
- * Returns the MouseData associated with the input device's pointer.
- */
-INLINE  MouseData GraphicsWindowInputDevice::
+INLINE PointerData GraphicsWindowInputDevice::
 get_pointer() const {
   LightMutexHolder holder(_lock);
-  return _mouse_data;
-}
-
-/**
- * Returns the MouseData associated with the input device's pointer, in raw
- * form (ie, prior to any pointer_mode interpretation).
- */
-INLINE  MouseData GraphicsWindowInputDevice::
-get_raw_pointer() const {
-  LightMutexHolder holder(_lock);
-  return _true_mouse_data;
-}
-
-/**
- * Set the device index.  This is reported in pointer events.  The device
- * index will be equal to the position of the GraphicsWindowInputDevice in the
- * window's list.
- */
-INLINE void GraphicsWindowInputDevice::
-set_device_index(int index) {
-  LightMutexHolder holder(_lock);
-  _device_index = index;
-}
-
-/**
- * Enables the generation of mouse-movement events.
- */
-INLINE void GraphicsWindowInputDevice::
-enable_pointer_events() {
-  LightMutexHolder holder(_lock);
-  _enable_pointer_events = true;
-}
-
-/**
- * Disables the generation of mouse-movement events.
- */
-INLINE void GraphicsWindowInputDevice::
-disable_pointer_events() {
-  LightMutexHolder holder(_lock);
-  _enable_pointer_events = false;
-  _pointer_events.clear();
-}
-
-/**
- * Records that the indicated button has been depressed.
- */
-INLINE void GraphicsWindowInputDevice::
-button_down(ButtonHandle button) {
-  button_down(button, ClockObject::get_global_clock()->get_frame_time());
-}
-
-/**
- * Records that the indicated button was depressed earlier, and we only just
- * detected the event after the fact.  This is mainly useful for tracking the
- * state of modifier keys.
- */
-INLINE void GraphicsWindowInputDevice::
-button_resume_down(ButtonHandle button) {
-  button_resume_down(button, ClockObject::get_global_clock()->get_frame_time());
-}
-
-/**
- * Records that the indicated button has been released.
- */
-INLINE void GraphicsWindowInputDevice::
-button_up(ButtonHandle button) {
-  button_up(button, ClockObject::get_global_clock()->get_frame_time());
-}
-
-/**
- * Records that the indicated keystroke has been generated.
- */
-INLINE void GraphicsWindowInputDevice::
-keystroke(int keycode) {
-  keystroke(keycode, ClockObject::get_global_clock()->get_frame_time());
-}
-
-/**
- * This should be called when the window focus is lost, so that we may miss
- * upcoming button events (especially "up" events) for the next period of
- * time.  It generates keyboard and mouse "up" events for those buttons that
- * we previously sent unpaired "down" events, so that the Panda application
- * will believe all buttons are now released.
- */
-INLINE void GraphicsWindowInputDevice::
-focus_lost() {
-  focus_lost(ClockObject::get_global_clock()->get_frame_time());
-}
-
-/**
- * Records that the indicated button has been depressed.
- */
-INLINE void GraphicsWindowInputDevice::
-raw_button_down(ButtonHandle button) {
-  raw_button_down(button, ClockObject::get_global_clock()->get_frame_time());
-}
-
-/**
- * Records that the indicated button has been released.
- */
-INLINE void GraphicsWindowInputDevice::
-raw_button_up(ButtonHandle button) {
-  raw_button_up(button, ClockObject::get_global_clock()->get_frame_time());
+  if (!_pointers.empty()) {
+    return _pointers[0];
+  } else {
+    return PointerData();
+  }
 }
 
 /**
  * To be called by a particular kind of GraphicsWindow to indicate that the
- * pointer is within the window, at the given pixel coordinates.
+ * pointer data has changed.
  */
 INLINE void GraphicsWindowInputDevice::
-set_pointer_in_window(double x, double y) {
-  // mutex is handled in set pointer .. convience function
-  set_pointer(true, x, y, ClockObject::get_global_clock()->get_frame_time());
-}
-
-/**
- * To be called by a particular kind of GraphicsWindow to indicate that the
- * pointer is no longer within the window.
- */
-INLINE void GraphicsWindowInputDevice::
-set_pointer_out_of_window() {
- // mutex is handled in set pointer .. convience function
-  set_pointer(false, _mouse_data._xpos, _mouse_data._ypos,
-              ClockObject::get_global_clock()->get_frame_time());
+update_pointer(PointerData data, double time) {
+  LightMutexHolder holder(_lock);
+  InputDevice::update_pointer(std::move(data), time);
 }
 
 /**
  * To be called by a particular kind of GraphicsWindow to indicate that the
- * pointer is within the window, at the given pixel coordinates.
+ * pointer has moved by the given relative amount.
  */
 INLINE void GraphicsWindowInputDevice::
-set_pointer_in_window(double x, double y, double time) {
- // mutex is handled in set pointer .. convience function
-  set_pointer(true, x, y, time);
+pointer_moved(double x, double y, double time) {
+  LightMutexHolder holder(_lock);
+  InputDevice::pointer_moved(0, x, y, time);
 }
 
 /**
  * To be called by a particular kind of GraphicsWindow to indicate that the
- * pointer is no longer within the window.
+ * pointer no longer exists.
  */
 INLINE void GraphicsWindowInputDevice::
-set_pointer_out_of_window(double time) {
- // mutex is handled in set pointer .. convience function
-  set_pointer(false, _mouse_data._xpos, _mouse_data._ypos, time);
-}
-
-/**
- *
- */
-INLINE bool GraphicsWindowInputDevice::
-operator == (const GraphicsWindowInputDevice &) const {
-  return true;
-}
-
-/**
- *
- */
-INLINE bool GraphicsWindowInputDevice::
-operator != (const GraphicsWindowInputDevice &) const {
-  return false;
-}
-
-/**
- *
- */
-INLINE bool GraphicsWindowInputDevice::
-operator < (const GraphicsWindowInputDevice &) const {
-  return false;
+remove_pointer(int id) {
+  LightMutexHolder holder(_lock);
+  InputDevice::remove_pointer(id);
 }

+ 66 - 192
panda/src/display/graphicsWindowInputDevice.cxx

@@ -16,12 +16,7 @@
 #include "mouseButton.h"
 #include "keyboardButton.h"
 
-#define EXPCL EXPCL_PANDA_DISPLAY
-#define EXPTP EXPTP_PANDA_DISPLAY
-#define TYPE GraphicsWindowInputDevice
-#define NAME vector_GraphicsWindowInputDevice
-
-#include "vector_src.cxx"
+TypeHandle GraphicsWindowInputDevice::_type_handle;
 
 using std::string;
 
@@ -34,206 +29,43 @@ using std::string;
  * below.
  */
 GraphicsWindowInputDevice::
-GraphicsWindowInputDevice(GraphicsWindow *host, const string &name, int flags) :
-  _host(host),
-  _name(name),
-  _flags(flags),
-  _device_index(0),
-  _event_sequence(0),
-  _pointer_mode_enable(false),
-  _pointer_speed(1.0),
-  _enable_pointer_events(false)
+GraphicsWindowInputDevice(GraphicsWindow *host, const string &name, bool pointer, bool keyboard) :
+  InputDevice(name, DeviceClass::virtual_device)
 {
+  if (pointer) {
+    enable_feature(Feature::pointer);
+    add_pointer(PointerType::mouse, 0);
+  }
+  if (keyboard) {
+    enable_feature(Feature::keyboard);
+  }
 }
 
 /**
  * This named constructor returns an input device that only has a pointing
  * device, no keyboard.
  */
-GraphicsWindowInputDevice GraphicsWindowInputDevice::
+PT(GraphicsWindowInputDevice) GraphicsWindowInputDevice::
 pointer_only(GraphicsWindow *host, const string &name) {
-  return GraphicsWindowInputDevice(host, name, IDF_has_pointer);
+  return new GraphicsWindowInputDevice(host, name, true, false);
 }
 
 /**
  * This named constructor returns an input device that only has a keyboard, no
  * pointing device.
  */
-GraphicsWindowInputDevice GraphicsWindowInputDevice::
+PT(GraphicsWindowInputDevice) GraphicsWindowInputDevice::
 keyboard_only(GraphicsWindow *host, const string &name) {
-  return GraphicsWindowInputDevice(host, name, IDF_has_keyboard);
+  return new GraphicsWindowInputDevice(host, name, false, true);
 }
 
 /**
  * This named constructor returns an input device that has both a keyboard and
  * pointer.
  */
-GraphicsWindowInputDevice GraphicsWindowInputDevice::
+PT(GraphicsWindowInputDevice) GraphicsWindowInputDevice::
 pointer_and_keyboard(GraphicsWindow *host, const string &name) {
-  return
-    GraphicsWindowInputDevice(host, name, IDF_has_pointer | IDF_has_keyboard);
-}
-
-/**
- *
- */
-GraphicsWindowInputDevice::
-GraphicsWindowInputDevice(const GraphicsWindowInputDevice &copy)
-{
-    *this = copy;
-}
-
-/**
- *
- */
-void GraphicsWindowInputDevice::
-operator = (const GraphicsWindowInputDevice &copy)
-{
-  LightMutexHolder holder(_lock);
-  LightMutexHolder holder1(copy._lock);
-  _host = copy._host;
-  _name = copy._name;
-  _flags = copy._flags;
-  _device_index = copy._device_index;
-  _event_sequence = copy._event_sequence;
-  _pointer_mode_enable = copy._pointer_mode_enable;
-  _pointer_speed = copy._pointer_speed;
-  _enable_pointer_events = copy._enable_pointer_events;
-  _mouse_data = copy._mouse_data;
-  _true_mouse_data = copy._true_mouse_data;
-  _button_events = copy._button_events;
-  _pointer_events = copy._pointer_events;
-}
-
-/**
- *
- */
-GraphicsWindowInputDevice::
-~GraphicsWindowInputDevice() {
-}
-
-/**
- * Returns true if this device has a pending button event (a mouse button or
- * keyboard button down/up), false otherwise.  If this returns true, the
- * particular event may be extracted via get_button_event().
- */
-bool GraphicsWindowInputDevice::
-has_button_event() const {
-  LightMutexHolder holder(_lock);
-  return !_button_events.empty();
-}
-
-/**
- * Assuming a previous call to has_button_event() returned true, this returns
- * the pending button event.
- */
-ButtonEvent GraphicsWindowInputDevice::
-get_button_event() {
-  LightMutexHolder holder(_lock);
-  ButtonEvent be = _button_events.front();
-  _button_events.pop_front();
-  return be;
-}
-
-/**
- * Returns true if this device has a pending pointer event (a mouse movement),
- * or false otherwise.  If this returns true, the particular event may be
- * extracted via get_pointer_event().
- */
-bool GraphicsWindowInputDevice::
-has_pointer_event() const {
-  LightMutexHolder holder(_lock);
-  return (_pointer_events != nullptr);
-}
-
-/**
- * Returns a PointerEventList containing all the recent pointer events.
- */
-PT(PointerEventList) GraphicsWindowInputDevice::
-get_pointer_events() {
-  LightMutexHolder holder(_lock);
-  PT(PointerEventList) result = _pointer_events;
-  _pointer_events = nullptr;
-  return result;
-}
-
-/**
- * There are two modes: raw mode, and pointer mode.  In pointer mode, the
- * mouse stops when it reaches the edges of the window.  In raw mode, the
- * mouse ignores the screen boundaries and can continue indefinitely, even
- * into negative coordinates.  In raw mode, each "blip" from the mouse
- * hardware corresponds to a change of 1 unit in the mouse's (x,y) coordinate.
- * In pointer mode, a variety of speed adjustment factors and concepts like
- * "mouse acceleration" may be applied.
- *
- * Mouse zero represents the system mouse pointer.  This is by definition a
- * pointer, not a raw mouse.  It is an error to try to enable or disable
- * pointer mode on mouse zero.
- */
-void GraphicsWindowInputDevice::
-enable_pointer_mode(double speed) {
-  LightMutexHolder holder(_lock);
-  nassertv(_device_index != 0);
-  _pointer_mode_enable = true;
-  _pointer_speed = speed;
-  _mouse_data._xpos = _host->get_x_size() * 0.5;
-  _mouse_data._ypos = _host->get_y_size() * 0.5;
-  _mouse_data._in_window = true;
-}
-
-/**
- * see enable_pointer_mode.
- */
-void GraphicsWindowInputDevice::
-disable_pointer_mode() {
-  LightMutexHolder holder(_lock);
-  nassertv(_device_index != 0);
-  _pointer_mode_enable = false;
-  _pointer_speed = 1.0;
-  _mouse_data = _true_mouse_data;
-}
-
-/**
- * Records that a mouse movement has taken place.
- */
-void GraphicsWindowInputDevice::
-set_pointer(bool inwin, double x, double y, double time) {
-  LightMutexHolder holder(_lock);
-
-  double delta_x = x - _true_mouse_data._xpos;
-  double delta_y = y - _true_mouse_data._ypos;
-  _true_mouse_data._in_window = inwin;
-  _true_mouse_data._xpos = x;
-  _true_mouse_data._ypos = y;
-
-  if (_pointer_mode_enable) {
-    double pointer_x = _mouse_data._xpos;
-    double pointer_y = _mouse_data._ypos;
-    pointer_x += (delta_x * _pointer_speed);
-    pointer_y += (delta_y * _pointer_speed);
-    double xhi = _host->get_x_size();
-    double yhi = _host->get_y_size();
-    if (pointer_x < 0.0) pointer_x = 0.0;
-    if (pointer_y < 0.0) pointer_y = 0.0;
-    if (pointer_x > xhi) pointer_x = xhi;
-    if (pointer_y > yhi) pointer_y = yhi;
-    _mouse_data._in_window = true;
-    _mouse_data._xpos = pointer_x;
-    _mouse_data._ypos = pointer_y;
-  } else {
-    _mouse_data = _true_mouse_data;
-  }
-
-  if (_enable_pointer_events) {
-    int seq = _event_sequence++;
-    if (_pointer_events == nullptr) {
-      _pointer_events = new PointerEventList();
-    }
-    _pointer_events->add_event(_mouse_data._in_window,
-                               _mouse_data._xpos,
-                               _mouse_data._ypos,
-                               seq, time);
-  }
+  return new GraphicsWindowInputDevice(host, name, true, true);
 }
 
 /**
@@ -242,7 +74,7 @@ set_pointer(bool inwin, double x, double y, double time) {
 void GraphicsWindowInputDevice::
 button_down(ButtonHandle button, double time) {
   LightMutexHolder holder(_lock);
-  _button_events.push_back(ButtonEvent(button, ButtonEvent::T_down, time));
+  _button_events->add_event(ButtonEvent(button, ButtonEvent::T_down, time));
   _buttons_held.insert(button);
 }
 
@@ -254,7 +86,7 @@ button_down(ButtonHandle button, double time) {
 void GraphicsWindowInputDevice::
 button_resume_down(ButtonHandle button, double time) {
   LightMutexHolder holder(_lock);
-  _button_events.push_back(ButtonEvent(button, ButtonEvent::T_resume_down, time));
+  _button_events->add_event(ButtonEvent(button, ButtonEvent::T_resume_down, time));
   _buttons_held.insert(button);
 }
 
@@ -264,7 +96,7 @@ button_resume_down(ButtonHandle button, double time) {
 void GraphicsWindowInputDevice::
 button_up(ButtonHandle button, double time) {
   LightMutexHolder holder(_lock);
-  _button_events.push_back(ButtonEvent(button, ButtonEvent::T_up, time));
+  _button_events->add_event(ButtonEvent(button, ButtonEvent::T_up, time));
   _buttons_held.erase(button);
 }
 
@@ -274,7 +106,7 @@ button_up(ButtonHandle button, double time) {
 void GraphicsWindowInputDevice::
 keystroke(int keycode, double time) {
   LightMutexHolder holder(_lock);
-  _button_events.push_back(ButtonEvent(keycode, time));
+  _button_events->add_event(ButtonEvent(keycode, time));
 }
 
 /**
@@ -286,7 +118,7 @@ void GraphicsWindowInputDevice::
 candidate(const std::wstring &candidate_string, size_t highlight_start,
           size_t highlight_end, size_t cursor_pos) {
   LightMutexHolder holder(_lock);
-  _button_events.push_back(ButtonEvent(candidate_string,
+  _button_events->add_event(ButtonEvent(candidate_string,
                                        highlight_start, highlight_end,
                                        cursor_pos));
 }
@@ -303,7 +135,7 @@ focus_lost(double time) {
   LightMutexHolder holder(_lock);
   ButtonsHeld::iterator bi;
   for (bi = _buttons_held.begin(); bi != _buttons_held.end(); ++bi) {
-    _button_events.push_back(ButtonEvent(*bi, ButtonEvent::T_up, time));
+    _button_events->add_event(ButtonEvent(*bi, ButtonEvent::T_up, time));
   }
   _buttons_held.clear();
 }
@@ -314,7 +146,7 @@ focus_lost(double time) {
 void GraphicsWindowInputDevice::
 raw_button_down(ButtonHandle button, double time) {
   LightMutexHolder holder(_lock);
-  _button_events.push_back(ButtonEvent(button, ButtonEvent::T_raw_down, time));
+  _button_events->add_event(ButtonEvent(button, ButtonEvent::T_raw_down, time));
 }
 
 /**
@@ -323,5 +155,47 @@ raw_button_down(ButtonHandle button, double time) {
 void GraphicsWindowInputDevice::
 raw_button_up(ButtonHandle button, double time) {
   LightMutexHolder holder(_lock);
-  _button_events.push_back(ButtonEvent(button, ButtonEvent::T_raw_up, time));
+  _button_events->add_event(ButtonEvent(button, ButtonEvent::T_raw_up, time));
+}
+
+/**
+ * To be called by a particular kind of GraphicsWindow to indicate that the
+ * pointer is within the window, at the given pixel coordinates.
+ */
+void GraphicsWindowInputDevice::
+set_pointer_in_window(double x, double y, double time) {
+  LightMutexHolder holder(_lock);
+  PointerData data = _pointers[0];
+  data._id = 0;
+  data._type = PointerType::mouse;
+  data._xpos = x;
+  data._ypos = y;
+  data._in_window = true;
+  InputDevice::update_pointer(data, time);
+}
+
+/**
+ * To be called by a particular kind of GraphicsWindow to indicate that the
+ * pointer is no longer within the window.
+ */
+void GraphicsWindowInputDevice::
+set_pointer_out_of_window(double time) {
+  LightMutexHolder holder(_lock);
+  if (_pointers.empty()) {
+    return;
+  }
+
+  _pointers[0]._in_window = false;
+  _pointers[0]._pressure = 0.0;
+
+  if (_enable_pointer_events) {
+    int seq = _event_sequence++;
+    if (_pointer_events.is_null()) {
+      _pointer_events = new PointerEventList();
+    }
+    _pointer_events->add_event(_pointers[0]._in_window,
+                               _pointers[0]._xpos,
+                               _pointers[0]._ypos,
+                               seq, time);
+  }
 }

+ 43 - 101
panda/src/display/graphicsWindowInputDevice.h

@@ -15,131 +15,73 @@
 #define GRAPHICSWINDOWINPUTDEVICE_H
 
 #include "pandabase.h"
-
-#include "buttonEvent.h"
-#include "pointerEvent.h"
-#include "pointerEventList.h"
-#include "mouseData.h"
-#include "clockObject.h"
-
-#include "pdeque.h"
-#include "pvector.h"
-#include "lightMutex.h"
-#include "lightMutexHolder.h"
+#include "inputDevice.h"
 
 // Forward declarations
 class GraphicsWindow;
 
 /**
- * This is a structure representing a single input device that may be
- * associated with a window.  Typically this will be a keyboard/mouse pair,
- * and there will be exactly one of these associated with each window, but
- * other variants are possible.
+ * This is a virtual input device that represents the keyboard and mouse pair
+ * that is associated with a particular window.  It collects mouse and
+ * keyboard events from the windowing system while the window is in focus.
  */
-class EXPCL_PANDA_DISPLAY GraphicsWindowInputDevice {
+class EXPCL_PANDA_DISPLAY GraphicsWindowInputDevice : public InputDevice {
 private:
-  GraphicsWindowInputDevice(GraphicsWindow *host, const std::string &name, int flags);
+  GraphicsWindowInputDevice(GraphicsWindow *host, const std::string &name,
+                            bool pointer, bool keyboard);
 
 public:
-  static GraphicsWindowInputDevice pointer_only(GraphicsWindow *host, const std::string &name);
-  static GraphicsWindowInputDevice keyboard_only(GraphicsWindow *host, const std::string &name);
-  static GraphicsWindowInputDevice pointer_and_keyboard(GraphicsWindow *host, const std::string &name);
-
-  INLINE GraphicsWindowInputDevice();
-  GraphicsWindowInputDevice(const GraphicsWindowInputDevice &copy);
-  void operator = (const GraphicsWindowInputDevice &copy);
-  ~GraphicsWindowInputDevice();
-
-  INLINE std::string get_name() const;
-  INLINE bool has_pointer() const;
-  INLINE bool has_keyboard() const;
-
-  INLINE void set_device_index(int index);
-
-  INLINE MouseData get_pointer() const;
-  INLINE MouseData get_raw_pointer() const;
+  static PT(GraphicsWindowInputDevice) pointer_only(GraphicsWindow *host, const std::string &name);
+  static PT(GraphicsWindowInputDevice) keyboard_only(GraphicsWindow *host, const std::string &name);
+  static PT(GraphicsWindowInputDevice) pointer_and_keyboard(GraphicsWindow *host, const std::string &name);
 
-  INLINE void enable_pointer_events();
-  INLINE void disable_pointer_events();
-
-  void enable_pointer_mode(double speed);
-  void disable_pointer_mode();
-
-  bool has_button_event() const;
-  ButtonEvent get_button_event();
-  bool has_pointer_event() const;
-  PT(PointerEventList) get_pointer_events();
+  GraphicsWindowInputDevice() = default;
 
 PUBLISHED:
   // The following interface is for the various kinds of GraphicsWindows to
   // record the data incoming on the device.
-  INLINE void button_down(ButtonHandle button);
-  INLINE void button_resume_down(ButtonHandle button);
-  INLINE void button_up(ButtonHandle button);
-  INLINE void keystroke(int keycode);
-  INLINE void focus_lost();
-  INLINE void raw_button_down(ButtonHandle button);
-  INLINE void raw_button_up(ButtonHandle button);
-  INLINE void set_pointer_in_window(double x, double y);
-  INLINE void set_pointer_out_of_window();
-
-  void button_down(ButtonHandle button, double time);
-  void button_resume_down(ButtonHandle button, double time);
-  void button_up(ButtonHandle button, double time);
-  void keystroke(int keycode, double time);
+  void button_down(ButtonHandle button, double time = ClockObject::get_global_clock()->get_frame_time());
+  void button_resume_down(ButtonHandle button, double time = ClockObject::get_global_clock()->get_frame_time());
+  void button_up(ButtonHandle button, double time = ClockObject::get_global_clock()->get_frame_time());
+
+  void keystroke(int keycode, double time = ClockObject::get_global_clock()->get_frame_time());
   void candidate(const std::wstring &candidate_string, size_t highlight_start,
                  size_t highlight_end, size_t cursor_pos);
-  void focus_lost(double time);
-  void raw_button_down(ButtonHandle button, double time);
-  void raw_button_up(ButtonHandle button, double time);
 
-  INLINE void set_pointer_in_window(double x, double y, double time);
-  INLINE void set_pointer_out_of_window(double time);
-  void set_pointer(bool inwin, double x, double y, double time);
+  void focus_lost(double time = ClockObject::get_global_clock()->get_frame_time());
 
-public:
-  // We need these methods to make VC++ happy when we try to instantiate a
-  // pvector<GraphicsWindowInputDevice>.  They don't do anything useful.
-  INLINE bool operator == (const GraphicsWindowInputDevice &other) const;
-  INLINE bool operator != (const GraphicsWindowInputDevice &other) const;
-  INLINE bool operator < (const GraphicsWindowInputDevice &other) const;
-
-private:
-  enum InputDeviceFlags {
-    IDF_has_pointer    = 0x01,
-    IDF_has_keyboard   = 0x02
-  };
-  typedef pdeque<ButtonEvent> ButtonEvents;
+  void raw_button_down(ButtonHandle button, double time = ClockObject::get_global_clock()->get_frame_time());
+  void raw_button_up(ButtonHandle button, double time = ClockObject::get_global_clock()->get_frame_time());
 
-  LightMutex _lock;
-
-  GraphicsWindow *_host;
-
-  std::string _name;
-  int _flags;
-  int _device_index;
-  int _event_sequence;
-
-  bool   _pointer_mode_enable;
-  double _pointer_speed;
-
-  bool _enable_pointer_events;
-  MouseData _mouse_data;
-  MouseData _true_mouse_data;
-  ButtonEvents _button_events;
-  PT(PointerEventList) _pointer_events;
+  INLINE PointerData get_pointer() const;
+  void set_pointer_in_window(double x, double y, double time = ClockObject::get_global_clock()->get_frame_time());
+  void set_pointer_out_of_window(double time = ClockObject::get_global_clock()->get_frame_time());
+  INLINE void update_pointer(PointerData data, double time = ClockObject::get_global_clock()->get_frame_time());
+  INLINE void pointer_moved(double x, double y, double time = ClockObject::get_global_clock()->get_frame_time());
+  INLINE void remove_pointer(int id);
 
+private:
   typedef pset<ButtonHandle> ButtonsHeld;
   ButtonsHeld _buttons_held;
-};
 
-#include "graphicsWindowInputDevice.I"
+public:
+  static TypeHandle get_class_type() {
+    return _type_handle;
+  }
+  static void init_type() {
+    InputDevice::init_type();
+    register_type(_type_handle, "GraphicsWindowInputDevice",
+                  InputDevice::get_class_type());
+  }
+  virtual TypeHandle get_type() const {
+    return get_class_type();
+  }
+  virtual TypeHandle force_init_type() {init_type(); return get_class_type();}
 
-#define EXPCL EXPCL_PANDA_DISPLAY
-#define EXPTP EXPTP_PANDA_DISPLAY
-#define TYPE GraphicsWindowInputDevice
-#define NAME vector_GraphicsWindowInputDevice
+private:
+  static TypeHandle _type_handle;
+};
 
-#include "vector_src.h"
+#include "graphicsWindowInputDevice.I"
 
 #endif

+ 15 - 30
panda/src/device/mouseAndKeyboard.cxx → panda/src/display/mouseAndKeyboard.cxx

@@ -27,7 +27,7 @@ MouseAndKeyboard::
 MouseAndKeyboard(GraphicsWindow *window, int device, const std::string &name) :
   DataNode(name),
   _window(window),
-  _device(device)
+  _device(window->get_input_device(device))
 {
   _pixel_xy_output = define_output("pixel_xy", EventStoreVec2::get_class_type());
   _pixel_size_output = define_output("pixel_size", EventStoreVec2::get_class_type());
@@ -38,7 +38,8 @@ MouseAndKeyboard(GraphicsWindow *window, int device, const std::string &name) :
   _pixel_xy = new EventStoreVec2(LPoint2(0.0f, 0.0f));
   _pixel_size = new EventStoreVec2(LPoint2(0.0f, 0.0f));
   _xy = new EventStoreVec2(LPoint2(0.0f, 0.0f));
-  _button_events = new ButtonEventList;
+
+  _device->enable_pointer_events();
 }
 
 /**
@@ -48,23 +49,9 @@ MouseAndKeyboard(GraphicsWindow *window, int device, const std::string &name) :
 void MouseAndKeyboard::
 set_source(GraphicsWindow *window, int device) {
   _window = window;
-  _device = device;
-}
+  _device = window->get_input_device(device);
 
-/**
- * Returns the associated source window.
- */
-PT(GraphicsWindow) MouseAndKeyboard::
-get_source_window() const {
-  return _window;
-}
-
-/**
- * Returns the associated source device.
- */
-int MouseAndKeyboard::
-get_source_device() const {
-  return _device;
+  //_device->enable_pointer_events();
 }
 
 /**
@@ -78,17 +65,15 @@ get_source_device() const {
 void MouseAndKeyboard::
 do_transmit_data(DataGraphTraverser *, const DataNodeTransmit &,
                  DataNodeTransmit &output) {
-  if (_window->has_button_event(_device)) {
-    // Fill up the button events.
-    _button_events->clear();
-    while (_window->has_button_event(_device)) {
-      ButtonEvent be = _window->get_button_event(_device);
-      _button_events->add_event(be);
-    }
-    output.set_data(_button_events_output, EventParameter(_button_events));
+
+  GraphicsWindowInputDevice *device = (GraphicsWindowInputDevice *)_device.p();
+
+  if (device->has_button_event()) {
+    PT(ButtonEventList) bel = device->get_button_events();
+    output.set_data(_button_events_output, EventParameter(bel));
   }
-  if (_window->has_pointer_event(_device)) {
-    PT(PointerEventList) pel = _window->get_pointer_events(_device);
+  if (device->has_pointer_event()) {
+    PT(PointerEventList) pel = device->get_pointer_events();
     output.set_data(_pointer_events_output, EventParameter(pel));
   }
 
@@ -101,8 +86,8 @@ do_transmit_data(DataGraphTraverser *, const DataNodeTransmit &,
     _pixel_size->set_value(LPoint2(w, h));
     output.set_data(_pixel_size_output, EventParameter(_pixel_size));
 
-    if (_window->has_pointer(_device)) {
-      const MouseData &mdata = _window->get_pointer(_device);
+    if (device->has_pointer()) {
+      PointerData mdata = device->get_pointer();
 
       if (mdata._in_window) {
         // Get mouse motion in pixels.

+ 1 - 5
panda/src/device/mouseAndKeyboard.h → panda/src/display/mouseAndKeyboard.h

@@ -43,9 +43,6 @@ PUBLISHED:
   explicit MouseAndKeyboard(GraphicsWindow *window, int device, const std::string &name);
   void set_source(GraphicsWindow *window, int device);
 
-  PT(GraphicsWindow) get_source_window() const;
-  int                get_source_device() const;
-
 protected:
   // Inherited from DataNode
   virtual void do_transmit_data(DataGraphTraverser *trav,
@@ -63,10 +60,9 @@ private:
   PT(EventStoreVec2) _pixel_xy;
   PT(EventStoreVec2) _pixel_size;
   PT(EventStoreVec2) _xy;
-  PT(ButtonEventList) _button_events;
 
   PT(GraphicsWindow) _window;
-  int _device;
+  PT(InputDevice) _device;
 
 public:
   static TypeHandle get_class_type() {

+ 1 - 0
panda/src/display/p3display_composite2.cxx

@@ -4,6 +4,7 @@
 #include "graphicsWindowProc.cxx"
 #include "graphicsWindowProcCallbackData.cxx"
 #include "graphicsWindowInputDevice.cxx"
+#include "mouseAndKeyboard.cxx"
 #include "nativeWindowHandle.cxx"
 #include "parasiteBuffer.cxx"
 #include "standardMunger.cxx"

+ 9 - 10
panda/src/display/subprocessWindow.cxx

@@ -39,9 +39,8 @@ SubprocessWindow(GraphicsEngine *engine, GraphicsPipe *pipe,
                  GraphicsOutput *host) :
   GraphicsWindow(engine, pipe, name, fb_prop, win_prop, flags, gsg, host)
 {
-  GraphicsWindowInputDevice device =
-    GraphicsWindowInputDevice::pointer_and_keyboard(this, "keyboard/mouse");
-  _input_devices.push_back(device);
+  _input = GraphicsWindowInputDevice::pointer_and_keyboard(this, "keyboard/mouse");
+  _input_devices.push_back(_input.p());
 
   // This will be an offscreen buffer that we use to render the actual
   // contents.
@@ -83,9 +82,9 @@ process_events() {
     while (_swbuffer->get_event(swb_event)) {
       // Deal with this event.
       if (swb_event._flags & SubprocessWindowBuffer::EF_mouse_position) {
-        _input_devices[0].set_pointer_in_window(swb_event._x, swb_event._y);
+        _input->set_pointer_in_window(swb_event._x, swb_event._y);
       } else if ((swb_event._flags & SubprocessWindowBuffer::EF_has_mouse) == 0) {
-        _input_devices[0].set_pointer_out_of_window();
+        _input->set_pointer_out_of_window();
       }
 
       unsigned int diff = swb_event._flags ^ _last_event_flags;
@@ -112,14 +111,14 @@ process_events() {
         int keycode;
         button = translate_key(keycode, swb_event._code, swb_event._flags);
         if (keycode != 0 && swb_event._type != SubprocessWindowBuffer::ET_button_up) {
-          _input_devices[0].keystroke(keycode);
+          _input->keystroke(keycode);
         }
       }
 
       if (swb_event._type == SubprocessWindowBuffer::ET_button_up) {
-        _input_devices[0].button_up(button);
+        _input->button_up(button);
       } else if (swb_event._type == SubprocessWindowBuffer::ET_button_down) {
-        _input_devices[0].button_down(button);
+        _input->button_down(button);
       }
     }
   }
@@ -556,9 +555,9 @@ translate_key(int &keycode, int os_code, unsigned int flags) const {
 void SubprocessWindow::
 transition_button(unsigned int flags, ButtonHandle button) {
   if (flags) {
-    _input_devices[0].button_down(button);
+    _input->button_down(button);
   } else {
-    _input_devices[0].button_up(button);
+    _input->button_up(button);
   }
 }
 

+ 1 - 0
panda/src/display/subprocessWindow.h

@@ -75,6 +75,7 @@ private:
 private:
   PT(GraphicsBuffer) _buffer;
   PT(Texture) _texture;
+  PT(GraphicsWindowInputDevice) _input;
 
   int _fd;
   size_t _mmap_size;

+ 0 - 38
panda/src/egldisplay/eglGraphicsWindow.cxx

@@ -55,44 +55,6 @@ eglGraphicsWindow::
 ~eglGraphicsWindow() {
 }
 
-/**
- * Forces the pointer to the indicated position within the window, if
- * possible.
- *
- * Returns true if successful, false on failure.  This may fail if the mouse
- * is not currently within the window, or if the API doesn't support this
- * operation.
- */
-bool eglGraphicsWindow::
-move_pointer(int device, int x, int y) {
-  // Note: this is not thread-safe; it should be called only from App.
-  // Probably not an issue.
-  if (device == 0) {
-    // Move the system mouse pointer.
-    if (!_properties.get_foreground() ||
-        !_input_devices[0].get_pointer().get_in_window()) {
-      // If the window doesn't have input focus, or the mouse isn't currently
-      // within the window, forget it.
-      return false;
-    }
-
-    const MouseData &md = _input_devices[0].get_pointer();
-    if (!md.get_in_window() || md.get_x() != x || md.get_y() != y) {
-      XWarpPointer(_display, None, _xwindow, 0, 0, 0, 0, x, y);
-      _input_devices[0].set_pointer_in_window(x, y);
-    }
-    return true;
-  } else {
-    // Move a raw mouse.
-    if (device < 1 || (size_t)device >= _input_devices.size()) {
-      return false;
-    }
-    _input_devices[device].set_pointer_in_window(x, y);
-    return true;
-  }
-}
-
-
 /**
  * This function will be called within the draw thread before beginning
  * rendering for a given frame.  It should do whatever setup is required, and

+ 0 - 1
panda/src/egldisplay/eglGraphicsWindow.h

@@ -33,7 +33,6 @@ public:
                     GraphicsOutput *host);
   virtual ~eglGraphicsWindow();
 
-  virtual bool move_pointer(int device, int x, int y);
   virtual bool begin_frame(FrameMode mode, Thread *current_thread);
   virtual void end_frame(FrameMode mode, Thread *current_thread);
   virtual void end_flip();

+ 1 - 1
panda/src/event/buttonEventList.h

@@ -31,7 +31,7 @@ class DatagramIterator;
  * but it may be used anywhere a list of ButtonEvents is desired.
  */
 class EXPCL_PANDA_EVENT ButtonEventList : public ParamValueBase {
-public:
+PUBLISHED:
   INLINE ButtonEventList();
   INLINE ButtonEventList(const ButtonEventList &copy);
   INLINE void operator = (const ButtonEventList &copy);

+ 0 - 53
panda/src/event/pointerEvent.I

@@ -11,59 +11,6 @@
  * @date 2007-09-20
  */
 
-/**
- *
- */
-INLINE PointerEvent::
-PointerEvent() :
-  _in_window(false),
-  _xpos(0),
-  _ypos(0),
-  _dx(0),
-  _dy(0),
-  _length(0.0),
-  _direction(0.0),
-  _rotation(0.0),
-  _sequence(0),
-  _time(0.0)
-{
-}
-
-/**
- *
- */
-INLINE PointerEvent::
-PointerEvent(const PointerEvent &copy) :
-  _in_window(copy._in_window),
-  _xpos(copy._xpos),
-  _ypos(copy._ypos),
-  _dx(copy._dx),
-  _dy(copy._dy),
-  _length(copy._length),
-  _direction(copy._direction),
-  _rotation(copy._rotation),
-  _sequence(copy._sequence),
-  _time(copy._time)
-{
-}
-
-/**
- *
- */
-INLINE void PointerEvent::
-operator = (const PointerEvent &copy) {
-  _in_window = copy._in_window;
-  _xpos = copy._xpos;
-  _ypos = copy._ypos;
-  _dx = copy._dx;
-  _dy = copy._dy;
-  _sequence = copy._sequence;
-  _length = copy._length;
-  _direction = copy._direction;
-  _rotation = copy._rotation;
-  _time = copy._time;
-}
-
 /**
  * The equality operator does not consider time significant.
  */

+ 15 - 15
panda/src/event/pointerEvent.h

@@ -15,7 +15,7 @@
 #define POINTEREVENT_H
 
 #include "pandabase.h"
-#include "mouseData.h"
+#include "pointerData.h"
 
 class Datagram;
 class DatagramIterator;
@@ -25,10 +25,7 @@ class DatagramIterator;
  */
 class EXPCL_PANDA_EVENT PointerEvent {
 public:
-
-  INLINE PointerEvent();
-  INLINE PointerEvent(const PointerEvent &copy);
-  INLINE void operator = (const PointerEvent &copy);
+  PointerEvent() = default;
 
   INLINE bool operator == (const PointerEvent &other) const;
   INLINE bool operator != (const PointerEvent &other) const;
@@ -40,16 +37,19 @@ public:
   void read_datagram(DatagramIterator &scan);
 
 public:
-  bool      _in_window;
-  int       _xpos;
-  int       _ypos;
-  int       _dx;
-  int       _dy;
-  double    _length;
-  double    _direction;
-  double    _rotation;
-  int       _sequence;
-  double    _time;
+  bool _in_window = false;
+  int _id = 0;
+  PointerType _type = PointerType::unknown;
+  double _xpos = 0.0;
+  double _ypos = 0.0;
+  double _dx = 0.0;
+  double _dy = 0.0;
+  double _length = 0.0;
+  double _direction = 0.0;
+  double _pressure = 0.0;
+  double _rotation = 0.0;
+  int _sequence = 0;
+  double _time = 0.0;
 };
 
 INLINE std::ostream &operator << (std::ostream &out, const PointerEvent &pe) {

+ 51 - 35
panda/src/event/pointerEventList.I

@@ -38,99 +38,115 @@ operator = (const PointerEventList &copy) {
 /**
  * Returns the number of events in the list.
  */
-INLINE int PointerEventList::
+INLINE size_t PointerEventList::
 get_num_events() const {
   return _events.size();
 }
 
+/**
+ * Returns true if the list is empty.
+ */
+INLINE bool PointerEventList::
+empty() const {
+  return _events.empty();
+}
+
+/**
+ * Returns the nth event in the list.
+ */
+INLINE const PointerEvent &PointerEventList::
+get_event(size_t n) const {
+  return _events[n];
+}
+
 /**
  * Get the in-window flag of the nth event.
  */
 INLINE bool PointerEventList::
-get_in_window(int evt) const {
-  nassertr((evt >= 0) && (evt < (int)_events.size()), 0);
-  return _events[evt]._in_window;
+get_in_window(size_t n) const {
+  nassertr(n < _events.size(), 0);
+  return _events[n]._in_window;
 }
 
 /**
  * Get the x-coordinate of the nth event.
  */
 INLINE int PointerEventList::
-get_xpos(int evt) const {
-  nassertr((evt >= 0) && (evt < (int)_events.size()), 0);
-  return _events[evt]._xpos;
+get_xpos(size_t n) const {
+  nassertr(n < _events.size(), 0);
+  return _events[n]._xpos;
 }
 
 /**
  * Get the y-coordinate of the nth event.
  */
 INLINE int PointerEventList::
-get_ypos(int evt) const {
-  nassertr((evt >= 0) && (evt < (int)_events.size()), 0);
-  return _events[evt]._ypos;
+get_ypos(size_t n) const {
+  nassertr(n < _events.size(), 0);
+  return _events[n]._ypos;
 }
 
 /**
- * Get the x-coordinate of the nth event.
+ * Get the x-delta of the nth event.
  */
-INLINE int PointerEventList::
-get_dx(int evt) const {
-  nassertr((evt >= 0) && (evt < (int)_events.size()), 0);
-  return _events[evt]._dx;
+INLINE double PointerEventList::
+get_dx(size_t n) const {
+  nassertr(n < _events.size(), 0);
+  return _events[n]._dx;
 }
 
 /**
- * Get the y-coordinate of the nth event.
+ * Get the y-delta of the nth event.
  */
-INLINE int PointerEventList::
-get_dy(int evt) const {
-  nassertr((evt >= 0) && (evt < (int)_events.size()), 0);
-  return _events[evt]._dy;
+INLINE double PointerEventList::
+get_dy(size_t n) const {
+  nassertr(n < _events.size(), 0);
+  return _events[n]._dy;
 }
 
 /**
  * Get the length of the nth event.
  */
 INLINE double PointerEventList::
-get_length(int evt) const {
-  nassertr((evt >= 0) && (evt < (int)_events.size()), 0);
-  return _events[evt]._length;
+get_length(size_t n) const {
+  nassertr(n < _events.size(), 0);
+  return _events[n]._length;
 }
 
 /**
  * Get the direction of the nth event.
  */
 INLINE double PointerEventList::
-get_direction(int evt) const {
-  nassertr((evt >= 0) && (evt < (int)_events.size()), 0);
-  return _events[evt]._direction;
+get_direction(size_t n) const {
+  nassertr(n < _events.size(), 0);
+  return _events[n]._direction;
 }
 
 /**
  * Get the rotation of the nth event.
  */
 INLINE double PointerEventList::
-get_rotation(int evt) const {
-  nassertr((evt >= 0) && (evt < (int)_events.size()), 0);
-  return _events[evt]._rotation;
+get_rotation(size_t n) const {
+  nassertr(n < _events.size(), 0);
+  return _events[n]._rotation;
 }
 
 /**
  * Get the sequence number of the nth event.
  */
 INLINE int PointerEventList::
-get_sequence(int evt) const {
-  nassertr((evt >= 0) && (evt < (int)_events.size()), 0);
-  return _events[evt]._sequence;
+get_sequence(size_t n) const {
+  nassertr(n < _events.size(), 0);
+  return _events[n]._sequence;
 }
 
 /**
  * Get the timestamp of the nth event.
  */
 INLINE double PointerEventList::
-get_time(int evt) const {
-  nassertr((evt >= 0) && (evt < (int)_events.size()), 0);
-  return _events[evt]._time;
+get_time(size_t n) const {
+  nassertr(n < _events.size(), 0);
+  return _events[n]._time;
 }
 
 /**

+ 65 - 0
panda/src/event/pointerEventList.cxx

@@ -76,6 +76,42 @@ write(std::ostream &out, int indent_level) const {
   }
 }
 
+/**
+ * Adds a new event from the given PointerData object.
+ */
+void PointerEventList::
+add_event(const PointerData &data, int seq, double time) {
+  PointerEvent pe;
+  pe._in_window = data._in_window;
+  pe._type = data._type;
+  pe._id = data._id;
+  pe._xpos = data._xpos;
+  pe._ypos = data._ypos;
+  pe._pressure = data._pressure;
+  pe._sequence = seq;
+  pe._time = time;
+  if (_events.size() > 0) {
+    pe._dx = data._xpos - _events.back()._xpos;
+    pe._dy = data._ypos - _events.back()._ypos;
+    double ddx = pe._dx;
+    double ddy = pe._dy;
+    pe._length = csqrt(ddx*ddx + ddy*ddy);
+    if (pe._length > 0.0) {
+      pe._direction = normalize_angle(rad_2_deg(catan2(-ddy,ddx)));
+    } else {
+      pe._direction = _events.back()._direction;
+    }
+    pe._rotation = delta_angle(_events.back()._direction, pe._direction);
+  } else {
+    pe._dx = 0;
+    pe._dy = 0;
+    pe._length = 0.0;
+    pe._direction = 0.0;
+    pe._rotation = 0.0;
+  }
+  _events.push_back(pe);
+}
+
 /**
  * Adds a new event to the end of the list.  Automatically calculates the dx,
  * dy, length, direction, and rotation for all but the first event.
@@ -110,6 +146,35 @@ add_event(bool in_win, int xpos, int ypos, int seq, double time) {
   _events.push_back(pe);
 }
 
+/**
+ * Adds a new event to the end of the list based on the given mouse movement.
+ */
+void PointerEventList::
+add_event(bool in_win, int xpos, int ypos, double xdelta, double ydelta, int seq, double time) {
+  PointerEvent pe;
+  pe._in_window = in_win;
+  pe._xpos = xpos;
+  pe._ypos = ypos;
+  pe._dx = xdelta;
+  pe._dy = ydelta;
+  pe._sequence = seq;
+  pe._time = time;
+  pe._length = csqrt(xdelta*xdelta + ydelta*ydelta);
+  if (pe._length > 0.0) {
+    pe._direction = normalize_angle(rad_2_deg(catan2(-ydelta,xdelta)));
+  } else if (!_events.empty()) {
+    pe._direction = _events.back()._direction;
+  } else {
+    pe._direction = 0.0;
+  }
+  if (!_events.empty()) {
+    pe._rotation = delta_angle(_events.back()._direction, pe._direction);
+  } else {
+    pe._rotation = 0.0;
+  }
+  _events.push_back(pe);
+}
+
 /**
  * Returns true if the trail loops around the specified point.
  */

+ 18 - 12
panda/src/event/pointerEventList.h

@@ -34,21 +34,24 @@ class EXPCL_PANDA_EVENT PointerEventList : public ParamValueBase {
 PUBLISHED:
   INLINE PointerEventList();
 
-  INLINE int get_num_events() const;
-  INLINE bool   get_in_window(int n) const;
-  INLINE int    get_xpos(int n) const;
-  INLINE int    get_ypos(int n) const;
-  INLINE int    get_dx(int n) const;
-  INLINE int    get_dy(int n) const;
-  INLINE int    get_sequence(int n) const;
-  INLINE double get_length(int n) const;
-  INLINE double get_direction(int n) const;
-  INLINE double get_rotation(int n) const;
-  INLINE double get_time(int n) const;
+  INLINE size_t get_num_events() const;
+  INLINE bool   get_in_window(size_t n) const;
+  INLINE int    get_xpos(size_t n) const;
+  INLINE int    get_ypos(size_t n) const;
+  INLINE double get_dx(size_t n) const;
+  INLINE double get_dy(size_t n) const;
+  INLINE int    get_sequence(size_t n) const;
+  INLINE double get_length(size_t n) const;
+  INLINE double get_direction(size_t n) const;
+  INLINE double get_rotation(size_t n) const;
+  INLINE double get_time(size_t n) const;
 
   INLINE void   clear();
   INLINE void   pop_front();
-  void   add_event(bool in_win, int xpos, int ypos, int seq, double time);
+  void add_event(const PointerData &data, int seq, double time);
+  void add_event(bool in_win, int xpos, int ypos, int seq, double time);
+  void add_event(bool in_win, int xpos, int ypos, double xdelta, double ydelta,
+                 int seq, double time);
 
   bool   encircles(int x, int y) const;
   double total_turns(double sec) const;
@@ -58,6 +61,9 @@ public:
   INLINE PointerEventList(const PointerEventList &copy);
   INLINE void operator = (const PointerEventList &copy);
 
+  INLINE bool empty() const;
+  INLINE const PointerEvent &get_event(size_t n) const;
+
   virtual void output(std::ostream &out) const;
   void write(std::ostream &out, int indent_level = 0) const;
 

+ 2 - 0
panda/src/putil/config_putil.cxx

@@ -31,6 +31,7 @@
 #include "datagram.h"
 #include "doubleBitMask.h"
 #include "factoryParam.h"
+#include "gamepadButton.h"
 #include "namable.h"
 #include "nodeCachedReferenceCount.h"
 #include "paramValue.h"
@@ -238,6 +239,7 @@ init_libputil() {
   WritableConfigurable::init_type();
   WritableParam::init_type();
 
+  GamepadButton::init_gamepad_buttons();
   KeyboardButton::init_keyboard_buttons();
   MouseButton::init_mouse_buttons();
 

+ 118 - 0
panda/src/putil/gamepadButton.cxx

@@ -0,0 +1,118 @@
+/**
+ * PANDA 3D SOFTWARE
+ * Copyright (c) Carnegie Mellon University.  All rights reserved.
+ *
+ * All use of this software is subject to the terms of the revised BSD
+ * license.  You should have received a copy of this license along
+ * with this source code in a file named "LICENSE."
+ *
+ * @file gamepadButton.cxx
+ * @author rdb
+ * @date 2015-08-21
+ */
+
+#include "gamepadButton.h"
+#include "buttonRegistry.h"
+
+#define DEFINE_GAMEPAD_BUTTON_HANDLE(KeyName)     \
+                  static ButtonHandle _##KeyName; \
+                  ButtonHandle GamepadButton::KeyName() { return _##KeyName; }
+
+DEFINE_GAMEPAD_BUTTON_HANDLE(lstick)
+DEFINE_GAMEPAD_BUTTON_HANDLE(rstick)
+DEFINE_GAMEPAD_BUTTON_HANDLE(lshoulder)
+DEFINE_GAMEPAD_BUTTON_HANDLE(rshoulder)
+DEFINE_GAMEPAD_BUTTON_HANDLE(ltrigger)
+DEFINE_GAMEPAD_BUTTON_HANDLE(rtrigger)
+
+DEFINE_GAMEPAD_BUTTON_HANDLE(dpad_left)
+DEFINE_GAMEPAD_BUTTON_HANDLE(dpad_right)
+DEFINE_GAMEPAD_BUTTON_HANDLE(dpad_up)
+DEFINE_GAMEPAD_BUTTON_HANDLE(dpad_down)
+
+DEFINE_GAMEPAD_BUTTON_HANDLE(back)
+DEFINE_GAMEPAD_BUTTON_HANDLE(guide)
+DEFINE_GAMEPAD_BUTTON_HANDLE(start)
+
+DEFINE_GAMEPAD_BUTTON_HANDLE(next)
+DEFINE_GAMEPAD_BUTTON_HANDLE(previous)
+
+DEFINE_GAMEPAD_BUTTON_HANDLE(face_a)
+DEFINE_GAMEPAD_BUTTON_HANDLE(face_b)
+DEFINE_GAMEPAD_BUTTON_HANDLE(face_c)
+DEFINE_GAMEPAD_BUTTON_HANDLE(face_x)
+DEFINE_GAMEPAD_BUTTON_HANDLE(face_y)
+DEFINE_GAMEPAD_BUTTON_HANDLE(face_z)
+
+DEFINE_GAMEPAD_BUTTON_HANDLE(face_1)
+DEFINE_GAMEPAD_BUTTON_HANDLE(face_2)
+
+DEFINE_GAMEPAD_BUTTON_HANDLE(trigger)
+DEFINE_GAMEPAD_BUTTON_HANDLE(hat_up)
+DEFINE_GAMEPAD_BUTTON_HANDLE(hat_down)
+DEFINE_GAMEPAD_BUTTON_HANDLE(hat_left)
+DEFINE_GAMEPAD_BUTTON_HANDLE(hat_right)
+
+/**
+ * Returns the ButtonHandle associated with the particular numbered joystick
+ * button (zero-based), if there is one, or ButtonHandle::none() if there is
+ * not.
+ */
+ButtonHandle GamepadButton::
+joystick(int button_number) {
+  if (button_number >= 0) {
+    // "button1" does not exist, it is called "trigger" instead
+    static pvector<ButtonHandle> buttons(1, _trigger);
+    while ((size_t)button_number >= buttons.size()) {
+      char numstr[20];
+      sprintf(numstr, "joystick%d", (int)buttons.size() + 1);
+      ButtonHandle handle;
+      ButtonRegistry::ptr()->register_button(handle, numstr);
+      buttons.push_back(handle);
+    }
+    return buttons[button_number];
+  }
+  return ButtonHandle::none();
+}
+
+/**
+ * This is intended to be called only once, by the static initialization
+ * performed in config_util.cxx.
+ */
+void GamepadButton::
+init_gamepad_buttons() {
+  ButtonRegistry::ptr()->register_button(_lstick, "lstick");
+  ButtonRegistry::ptr()->register_button(_rstick, "rstick");
+  ButtonRegistry::ptr()->register_button(_lshoulder, "lshoulder");
+  ButtonRegistry::ptr()->register_button(_rshoulder, "rshoulder");
+  ButtonRegistry::ptr()->register_button(_ltrigger, "ltrigger");
+  ButtonRegistry::ptr()->register_button(_rtrigger, "rtrigger");
+
+  ButtonRegistry::ptr()->register_button(_dpad_left, "dpad_left");
+  ButtonRegistry::ptr()->register_button(_dpad_right, "dpad_right");
+  ButtonRegistry::ptr()->register_button(_dpad_up, "dpad_up");
+  ButtonRegistry::ptr()->register_button(_dpad_down, "dpad_down");
+
+  ButtonRegistry::ptr()->register_button(_back, "back");
+  ButtonRegistry::ptr()->register_button(_guide, "guide");
+  ButtonRegistry::ptr()->register_button(_start, "start");
+
+  ButtonRegistry::ptr()->register_button(_next, "next");
+  ButtonRegistry::ptr()->register_button(_previous, "previous");
+
+  ButtonRegistry::ptr()->register_button(_face_a, "face_a");
+  ButtonRegistry::ptr()->register_button(_face_b, "face_b");
+  ButtonRegistry::ptr()->register_button(_face_c, "face_c");
+  ButtonRegistry::ptr()->register_button(_face_x, "face_x");
+  ButtonRegistry::ptr()->register_button(_face_y, "face_y");
+  ButtonRegistry::ptr()->register_button(_face_z, "face_z");
+
+  ButtonRegistry::ptr()->register_button(_face_1, "face_1");
+  ButtonRegistry::ptr()->register_button(_face_2, "face_2");
+
+  ButtonRegistry::ptr()->register_button(_trigger, "trigger");
+  ButtonRegistry::ptr()->register_button(_hat_up, "hat_up");
+  ButtonRegistry::ptr()->register_button(_hat_down, "hat_down");
+  ButtonRegistry::ptr()->register_button(_hat_left, "hat_left");
+  ButtonRegistry::ptr()->register_button(_hat_right, "hat_right");
+}

+ 68 - 0
panda/src/putil/gamepadButton.h

@@ -0,0 +1,68 @@
+/**
+ * PANDA 3D SOFTWARE
+ * Copyright (c) Carnegie Mellon University.  All rights reserved.
+ *
+ * All use of this software is subject to the terms of the revised BSD
+ * license.  You should have received a copy of this license along
+ * with this source code in a file named "LICENSE."
+ *
+ * @file gamepadButton.h
+ * @author rdb
+ * @date 2015-08-21
+ */
+
+#ifndef GAMEPADBUTTON_H
+#define GAMEPADBUTTON_H
+
+#include "pandabase.h"
+
+#include "buttonHandle.h"
+
+/**
+ * This class is just used as a convenient namespace for grouping all of these
+ * handy functions that return buttons which map to gamepad buttons.
+ */
+class EXPCL_PANDA_PUTIL GamepadButton {
+PUBLISHED:
+  static ButtonHandle lstick();
+  static ButtonHandle rstick();
+  static ButtonHandle lshoulder();
+  static ButtonHandle rshoulder();
+  static ButtonHandle ltrigger();
+  static ButtonHandle rtrigger();
+
+  static ButtonHandle dpad_left();
+  static ButtonHandle dpad_right();
+  static ButtonHandle dpad_up();
+  static ButtonHandle dpad_down();
+
+  static ButtonHandle back();
+  static ButtonHandle guide();
+  static ButtonHandle start();
+
+  static ButtonHandle next();
+  static ButtonHandle previous();
+
+  static ButtonHandle face_a();
+  static ButtonHandle face_b();
+  static ButtonHandle face_c();
+  static ButtonHandle face_x();
+  static ButtonHandle face_y();
+  static ButtonHandle face_z();
+
+  static ButtonHandle face_1();
+  static ButtonHandle face_2();
+
+  // Flight stick buttons, takes zero-based index.  First is always trigger.
+  static ButtonHandle trigger();
+  static ButtonHandle joystick(int button_number);
+  static ButtonHandle hat_up();
+  static ButtonHandle hat_down();
+  static ButtonHandle hat_left();
+  static ButtonHandle hat_right();
+
+public:
+  static void init_gamepad_buttons();
+};
+
+#endif

+ 0 - 73
panda/src/putil/mouseData.I

@@ -1,73 +0,0 @@
-/**
- * PANDA 3D SOFTWARE
- * Copyright (c) Carnegie Mellon University.  All rights reserved.
- *
- * All use of this software is subject to the terms of the revised BSD
- * license.  You should have received a copy of this license along
- * with this source code in a file named "LICENSE."
- *
- * @file mouseData.I
- * @author drose
- * @date 2002-07-15
- */
-
-/**
- *
- */
-INLINE MouseData::
-MouseData() {
-  _in_window = false;
-  _xpos = 0;
-  _ypos = 0;
-}
-
-/**
- *
- */
-INLINE MouseData::
-MouseData(const MouseData &copy) :
-  _in_window(copy._in_window),
-  _xpos(copy._xpos),
-  _ypos(copy._ypos)
-{
-}
-
-/**
- *
- */
-INLINE void MouseData::
-operator = (const MouseData &copy) {
-  _in_window = copy._in_window;
-  _xpos = copy._xpos;
-  _ypos = copy._ypos;
-}
-
-/**
- *
- */
-INLINE double MouseData::
-get_x() const {
-  return _xpos;
-}
-
-/**
- *
- */
-INLINE double MouseData::
-get_y() const {
-  return _ypos;
-}
-
-/**
- *
- */
-INLINE bool MouseData::
-get_in_window() const {
-  return _in_window;
-}
-
-
-INLINE std::ostream &operator << (std::ostream &out, const MouseData &md) {
-  md.output(out);
-  return out;
-}

+ 7 - 31
panda/src/putil/mouseData.h

@@ -7,45 +7,21 @@
  * with this source code in a file named "LICENSE."
  *
  * @file mouseData.h
- * @author drose
- * @date 1999-02-08
+ * @author rdb
+ * @date 2018-09-24
  */
 
 #ifndef MOUSEDATA_H
 #define MOUSEDATA_H
 
-#include "pandabase.h"
-
-#include "modifierButtons.h"
+#include "pointerData.h"
 
+BEGIN_PUBLISH
 /**
- * Holds the data that might be generated by a 2-d pointer input device, such
- * as the mouse in the GraphicsWindow.
+ * Deprecated alias for PointerData.
  */
-class EXPCL_PANDA_PUTIL MouseData {
-PUBLISHED:
-  INLINE MouseData();
-  INLINE MouseData(const MouseData &copy);
-  INLINE void operator = (const MouseData &copy);
-
-  INLINE double get_x() const;
-  INLINE double get_y() const;
-  INLINE bool get_in_window() const;
-
-  void output(std::ostream &out) const;
-
-  MAKE_PROPERTY(x, get_x);
-  MAKE_PROPERTY(y, get_y);
-  MAKE_PROPERTY(in_window, get_in_window);
-
-public:
-  bool _in_window;
-  double _xpos;
-  double _ypos;
-};
-
-INLINE std::ostream &operator << (std::ostream &out, const MouseData &md);
+typedef PointerData MouseData;
 
-#include "mouseData.I"
+END_PUBLISH
 
 #endif

+ 0 - 1
panda/src/putil/p3putil_composite1.cxx

@@ -29,4 +29,3 @@
 #include "factoryBase.cxx"
 #include "factoryParam.cxx"
 #include "factoryParams.cxx"
-#include "globalPointerRegistry.cxx"

+ 3 - 1
panda/src/putil/p3putil_composite2.cxx

@@ -1,3 +1,5 @@
+#include "gamepadButton.cxx"
+#include "globalPointerRegistry.cxx"
 #include "ioPtaDatagramFloat.cxx"
 #include "ioPtaDatagramInt.cxx"
 #include "ioPtaDatagramShort.cxx"
@@ -7,11 +9,11 @@
 #include "loaderOptions.cxx"
 #include "modifierButtons.cxx"
 #include "mouseButton.cxx"
-#include "mouseData.cxx"
 #include "nameUniquifier.cxx"
 #include "nodeCachedReferenceCount.cxx"
 #include "paramValue.cxx"
 #include "pbitops.cxx"
+#include "pointerData.cxx"
 #include "pta_ushort.cxx"
 #include "simpleHashMap.cxx"
 #include "sparseArray.cxx"

+ 71 - 0
panda/src/putil/pointerData.I

@@ -0,0 +1,71 @@
+/**
+ * PANDA 3D SOFTWARE
+ * Copyright (c) Carnegie Mellon University.  All rights reserved.
+ *
+ * All use of this software is subject to the terms of the revised BSD
+ * license.  You should have received a copy of this license along
+ * with this source code in a file named "LICENSE."
+ *
+ * @file pointerData.I
+ * @author drose
+ * @date 2002-07-15
+ */
+
+/**
+ *
+ */
+INLINE double PointerData::
+get_x() const {
+  return _xpos;
+}
+
+/**
+ *
+ */
+INLINE double PointerData::
+get_y() const {
+  return _ypos;
+}
+
+/**
+ * If this returns false, the pointer is not currently present in the window
+ * and the values returned by get_x() and get_y() may not be meaningful.
+ */
+INLINE bool PointerData::
+get_in_window() const {
+  return _in_window;
+}
+
+/**
+ * Returns a unique identifier for this pointer.  This is for tracking
+ * individual fingers.  This value should not be assumed to have a specific
+ * meaning other than that there will not be two different pointers active
+ * simultaneously with the same identifier.
+ */
+INLINE int PointerData::
+get_id() const {
+  return _id;
+}
+
+/**
+ * Returns the type of pointing device.
+ */
+INLINE PointerType PointerData::
+get_type() const {
+  return _type;
+}
+
+/**
+ * Returns the pressure of the pointer.  For mice, this will be 1.0 if any
+ * button is pressed, 0.0 otherwise.
+ */
+INLINE double PointerData::
+get_pressure() const {
+  return _pressure;
+}
+
+
+INLINE std::ostream &operator << (std::ostream &out, const PointerData &md) {
+  md.output(out);
+  return out;
+}

+ 5 - 5
panda/src/putil/mouseData.cxx → panda/src/putil/pointerData.cxx

@@ -6,21 +6,21 @@
  * license.  You should have received a copy of this license along
  * with this source code in a file named "LICENSE."
  *
- * @file mouseData.cxx
+ * @file pointerData.cxx
  * @author drose
  * @date 1999-02-08
  */
 
-#include "mouseData.h"
+#include "pointerData.h"
 
 /**
  *
  */
-void MouseData::
+void PointerData::
 output(std::ostream &out) const {
   if (!_in_window) {
-    out << "MouseData: Not in window";
+    out << "PointerData: Not in window";
   } else {
-    out << "MouseData: (" << _xpos << ", " << _ypos << ")";
+    out << "PointerData: (" << _xpos << ", " << _ypos << ")";
   }
 }

+ 72 - 0
panda/src/putil/pointerData.h

@@ -0,0 +1,72 @@
+/**
+ * PANDA 3D SOFTWARE
+ * Copyright (c) Carnegie Mellon University.  All rights reserved.
+ *
+ * All use of this software is subject to the terms of the revised BSD
+ * license.  You should have received a copy of this license along
+ * with this source code in a file named "LICENSE."
+ *
+ * @file pointerData.h
+ * @author drose
+ * @date 1999-02-08
+ */
+
+#ifndef POINTERDATA_H
+#define POINTERDATA_H
+
+#include "pandabase.h"
+
+#include "modifierButtons.h"
+
+BEGIN_PUBLISH
+/**
+ * Contains the types of pointer device.
+ */
+enum class PointerType {
+  unknown,
+  mouse,
+  finger,
+  stylus,
+  eraser,
+};
+END_PUBLISH
+
+/**
+ * Holds the data that might be generated by a 2-d pointer input device, such
+ * as the mouse in the GraphicsWindow.
+ */
+class EXPCL_PANDA_PUTIL PointerData {
+PUBLISHED:
+  INLINE double get_x() const;
+  INLINE double get_y() const;
+  INLINE bool get_in_window() const;
+
+public:
+  INLINE int get_id() const;
+  INLINE PointerType get_type() const;
+  INLINE double get_pressure() const;
+
+  void output(std::ostream &out) const;
+
+PUBLISHED:
+  MAKE_PROPERTY(x, get_x);
+  MAKE_PROPERTY(y, get_y);
+  MAKE_PROPERTY(type, get_type);
+  MAKE_PROPERTY(id, get_id);
+  MAKE_PROPERTY(in_window, get_in_window);
+  MAKE_PROPERTY(pressure, get_pressure);
+
+public:
+  bool _in_window = false;
+  double _xpos = 0.0;
+  double _ypos = 0.0;
+  double _pressure = 0.0;
+  PointerType _type = PointerType::unknown;
+  int _id = 0;
+};
+
+INLINE std::ostream &operator << (std::ostream &out, const PointerData &md);
+
+#include "pointerData.I"
+
+#endif

+ 1 - 1
panda/src/tform/mouseWatcher.I

@@ -542,7 +542,7 @@ get_trail_log() const {
  * frame.  The trail log is updated once per frame, during the process_events
  * operation.
  */
-INLINE int MouseWatcher::
+INLINE size_t MouseWatcher::
 num_trail_recent() const {
   return _num_trail_recent;
 }

+ 1 - 1
panda/src/tform/mouseWatcher.cxx

@@ -1326,7 +1326,7 @@ do_transmit_data(DataGraphTraverser *trav, const DataNodeTransmit &input,
     const PointerEventList *this_pointer_events;
     DCAST_INTO_V(this_pointer_events, input.get_data(_pointer_events_input).get_ptr());
     _num_trail_recent = this_pointer_events->get_num_events();
-    for (int i = 0; i < _num_trail_recent; i++) {
+    for (size_t i = 0; i < _num_trail_recent; i++) {
       bool in_win = this_pointer_events->get_in_window(i);
       int xpos = this_pointer_events->get_xpos(i);
       int ypos = this_pointer_events->get_ypos(i);

+ 2 - 2
panda/src/tform/mouseWatcher.h

@@ -138,7 +138,7 @@ PUBLISHED:
   INLINE const std::string &get_inactivity_timeout_event() const;
 
   INLINE CPT(PointerEventList) get_trail_log() const;
-  INLINE int   num_trail_recent() const;
+  INLINE size_t num_trail_recent() const;
   void         set_trail_log_duration(double duration);
   PT(GeomNode) get_trail_node();
   void         clear_trail_node();
@@ -220,7 +220,7 @@ private:
   LVecBase4 _frame;
 
   PT(PointerEventList) _trail_log;
-  int _num_trail_recent;
+  size_t _num_trail_recent;
   double _trail_log_duration;
   PT(GeomNode) _trail_node;
 

+ 25 - 25
panda/src/tinydisplay/tinyOsxGraphicsWindow.mm

@@ -397,7 +397,7 @@ OSStatus TinyOsxGraphicsWindow::handleTextInput (EventHandlerCallRef myHandler,
     }
 
     for (unsigned int x = 0; x < actualSize/sizeof(UniChar); ++x) {
-      _input_devices[0].keystroke(text[x]);
+      _input_devices[0]->keystroke(text[x]);
     }
     DisposePtr((char *)text);
   }
@@ -466,10 +466,10 @@ TinyOsxGraphicsWindow::TinyOsxGraphicsWindow(GraphicsEngine *engine, GraphicsPip
   _current_icon(NULL),
   _ID(id_seed++),
   _originalMode(NULL) {
- GraphicsWindowInputDevice device =
+  PT(InputDevice) device =
     GraphicsWindowInputDevice::pointer_and_keyboard(this, "keyboard/mouse");
   _input_devices.push_back(device);
-  _input_devices[0].set_pointer_in_window(0, 0);
+  device->set_pointer_in_window(0, 0);
   _last_key_modifiers = 0;
   _last_buttons = 0;
 
@@ -545,7 +545,7 @@ bool TinyOsxGraphicsWindow::set_icon_filename(const Filename &icon_filename) {
  */
 void TinyOsxGraphicsWindow::
 set_pointer_in_window(int x, int y) {
-  _input_devices[0].set_pointer_in_window(x, y);
+  _input_devices[0]->set_pointer_in_window(x, y);
 
   if (_cursor_hidden != _display_hide_cursor) {
     if (_cursor_hidden) {
@@ -563,7 +563,7 @@ set_pointer_in_window(int x, int y) {
  */
 void TinyOsxGraphicsWindow::
 set_pointer_out_of_window() {
-  _input_devices[0].set_pointer_out_of_window();
+  _input_devices[0]->set_pointer_out_of_window();
 
   if (_display_hide_cursor) {
     CGDisplayShowCursor(kCGDirectMainDisplay);
@@ -1253,19 +1253,19 @@ void TinyOsxGraphicsWindow::SystemPointToLocalPoint(Point &qdGlobalPoint) {
       SystemPointToLocalPoint(qdGlobalPoint);
 
       if (wheelAxis == kEventMouseWheelAxisY) {
-  set_pointer_in_window((int)qdGlobalPoint.h, (int)qdGlobalPoint.v);
-  _wheel_delta += this_wheel_delta;
-  SInt32 wheel_scale = osx_mouse_wheel_scale;
-  while (_wheel_delta > wheel_scale) {
-    _input_devices[0].button_down(MouseButton::wheel_up());
-    _input_devices[0].button_up(MouseButton::wheel_up());
-    _wheel_delta -= wheel_scale;
-  }
-  while (_wheel_delta < -wheel_scale) {
-    _input_devices[0].button_down(MouseButton::wheel_down());
-    _input_devices[0].button_up(MouseButton::wheel_down());
-    _wheel_delta += wheel_scale;
-  }
+        set_pointer_in_window((int)qdGlobalPoint.h, (int)qdGlobalPoint.v);
+        _wheel_delta += this_wheel_delta;
+        SInt32 wheel_scale = osx_mouse_wheel_scale;
+        while (_wheel_delta > wheel_scale) {
+          _input_devices[0]->button_down(MouseButton::wheel_up());
+          _input_devices[0]->button_up(MouseButton::wheel_up());
+          _wheel_delta -= wheel_scale;
+        }
+        while (_wheel_delta < -wheel_scale) {
+          _input_devices[0]->button_down(MouseButton::wheel_down());
+          _input_devices[0]->button_up(MouseButton::wheel_down());
+          _wheel_delta += wheel_scale;
+        }
       }
       result = noErr;
       break;
@@ -1438,25 +1438,25 @@ HandleButtonDelta(UInt32 new_buttons) {
 
   if (changed & 0x01) {
     if (new_buttons & 0x01) {
-      _input_devices[0].button_down(MouseButton::one());
+      _input_devices[0]->button_down(MouseButton::one());
     } else {
-      _input_devices[0].button_up(MouseButton::one());
+      _input_devices[0]->button_up(MouseButton::one());
     }
   }
 
   if (changed & 0x04) {
     if (new_buttons & 0x04) {
-      _input_devices[0].button_down(MouseButton::two());
+      _input_devices[0]->button_down(MouseButton::two());
     } else {
-      _input_devices[0].button_up(MouseButton::two());
+      _input_devices[0]->button_up(MouseButton::two());
     }
   }
 
   if (changed & 0x02) {
     if (new_buttons & 0x02) {
-      _input_devices[0].button_down(MouseButton::three());
+      _input_devices[0]->button_down(MouseButton::three());
     } else {
-      _input_devices[0].button_up(MouseButton::three());
+      _input_devices[0]->button_up(MouseButton::three());
     }
   }
 
@@ -1657,7 +1657,7 @@ void TinyOsxGraphicsWindow::set_properties_now(WindowProperties &properties) {
   if (properties.has_cursor_hidden()) {
     _properties.set_cursor_hidden(properties.get_cursor_hidden());
     _cursor_hidden = properties.get_cursor_hidden();
-    if (_cursor_hidden && _input_devices[0].has_pointer()) {
+    if (_cursor_hidden && _input_devices[0]->has_pointer()) {
       if (!_display_hide_cursor) {
         CGDisplayHideCursor(kCGDirectMainDisplay);
         _display_hide_cursor = true;

+ 9 - 11
panda/src/tinydisplay/tinySDLGraphicsWindow.cxx

@@ -43,9 +43,7 @@ TinySDLGraphicsWindow(GraphicsEngine *engine, GraphicsPipe *pipe,
   _pitch = 0;
   update_pixel_factor();
 
-  GraphicsWindowInputDevice device =
-    GraphicsWindowInputDevice::pointer_and_keyboard(this, "keyboard_mouse");
-  add_input_device(device);
+  add_input_device(GraphicsWindowInputDevice::pointer_and_keyboard(this, "keyboard_mouse"));
 }
 
 /**
@@ -168,35 +166,35 @@ process_events() {
     switch(evt.type) {
     case SDL_KEYDOWN:
       if (evt.key.keysym.unicode) {
-        _input_devices[0].keystroke(evt.key.keysym.unicode);
+        _input_devices[0]->keystroke(evt.key.keysym.unicode);
       }
       button = get_keyboard_button(evt.key.keysym.sym);
       if (button != ButtonHandle::none()) {
-        _input_devices[0].button_down(button);
+        _input_devices[0]->button_down(button);
       }
       break;
 
     case SDL_KEYUP:
       button = get_keyboard_button(evt.key.keysym.sym);
       if (button != ButtonHandle::none()) {
-        _input_devices[0].button_up(button);
+        _input_devices[0]->button_up(button);
       }
       break;
 
     case SDL_MOUSEBUTTONDOWN:
       button = get_mouse_button(evt.button.button);
-      _input_devices[0].set_pointer_in_window(evt.button.x, evt.button.y);
-      _input_devices[0].button_down(button);
+      _input_devices[0]->set_pointer_in_window(evt.button.x, evt.button.y);
+      _input_devices[0]->button_down(button);
       break;
 
     case SDL_MOUSEBUTTONUP:
       button = get_mouse_button(evt.button.button);
-      _input_devices[0].set_pointer_in_window(evt.button.x, evt.button.y);
-      _input_devices[0].button_up(button);
+      _input_devices[0]->set_pointer_in_window(evt.button.x, evt.button.y);
+      _input_devices[0]->button_up(button);
       break;
 
     case SDL_MOUSEMOTION:
-      _input_devices[0].set_pointer_in_window(evt.motion.x, evt.motion.y);
+      _input_devices[0]->set_pointer_in_window(evt.motion.x, evt.motion.y);
       break;
 
     case SDL_VIDEORESIZE:

+ 8 - 10
panda/src/tinydisplay/tinyXGraphicsWindow.cxx

@@ -195,8 +195,6 @@ process_events() {
     return;
   }
 
-  poll_raw_mice();
-
   XEvent event;
   XKeyEvent keyrelease_event;
   bool got_keyrelease_event = false;
@@ -271,18 +269,18 @@ process_events() {
     case ButtonPress:
       // This refers to the mouse buttons.
       button = get_mouse_button(event.xbutton);
-      _input_devices[0].set_pointer_in_window(event.xbutton.x, event.xbutton.y);
-      _input_devices[0].button_down(button);
+      _input->set_pointer_in_window(event.xbutton.x, event.xbutton.y);
+      _input->button_down(button);
       break;
 
     case ButtonRelease:
       button = get_mouse_button(event.xbutton);
-      _input_devices[0].set_pointer_in_window(event.xbutton.x, event.xbutton.y);
-      _input_devices[0].button_up(button);
+      _input->set_pointer_in_window(event.xbutton.x, event.xbutton.y);
+      _input->button_up(button);
       break;
 
     case MotionNotify:
-      _input_devices[0].set_pointer_in_window(event.xmotion.x, event.xmotion.y);
+      _input->set_pointer_in_window(event.xmotion.x, event.xmotion.y);
       break;
 
     case KeyPress:
@@ -299,11 +297,11 @@ process_events() {
       break;
 
     case EnterNotify:
-      _input_devices[0].set_pointer_in_window(event.xcrossing.x, event.xcrossing.y);
+      _input->set_pointer_in_window(event.xcrossing.x, event.xcrossing.y);
       break;
 
     case LeaveNotify:
-      _input_devices[0].set_pointer_out_of_window();
+      _input->set_pointer_out_of_window();
       break;
 
     case FocusIn:
@@ -312,7 +310,7 @@ process_events() {
       break;
 
     case FocusOut:
-      _input_devices[0].focus_lost();
+      _input->focus_lost();
       properties.set_foreground(false);
       system_changed_properties(properties);
       break;

+ 1 - 9
panda/src/vrpn/vrpnAnalog.cxx

@@ -99,16 +99,8 @@ vrpn_analog_callback(void *userdata, const vrpn_ANALOGCB info) {
   Devices::iterator di;
   for (di = self->_devices.begin(); di != self->_devices.end(); ++di) {
     VrpnAnalogDevice *device = (*di);
-    device->acquire();
     for (int i = 0; i < info.num_channel; i++) {
-      if (vrpn_cat.is_debug()) {
-        if (device->get_control_state(i) != info.channel[i]) {
-          vrpn_cat.debug()
-            << *self << " got analog " << i << " = " << info.channel[i] << "\n";
-        }
-      }
-      device->set_control_state(i, info.channel[i]);
+      device->set_axis_value(i, info.channel[i]);
     }
-    device->unlock();
   }
 }

+ 1 - 3
panda/src/vrpn/vrpnButton.cxx

@@ -103,8 +103,6 @@ vrpn_button_callback(void *userdata, const vrpn_BUTTONCB info) {
   Devices::iterator di;
   for (di = self->_devices.begin(); di != self->_devices.end(); ++di) {
     VrpnButtonDevice *device = (*di);
-    device->acquire();
-    device->set_button_state(info.button, info.state != 0);
-    device->unlock();
+    device->button_changed(info.button, info.state != 0);
   }
 }

Some files were not shown because too many files changed in this diff