From f8520773e94efb4ccd63d334a4ae7f3e1718017f Mon Sep 17 00:00:00 2001 From: Sebastian Hoffmann Date: Fri, 30 Nov 2018 21:02:02 +0100 Subject: [PATCH] samples: add input device tester program Closes #234 --- samples/gamepad/device_tester.py | 456 +++++++++++++++++++++++++++++++ 1 file changed, 456 insertions(+) create mode 100644 samples/gamepad/device_tester.py diff --git a/samples/gamepad/device_tester.py b/samples/gamepad/device_tester.py new file mode 100644 index 0000000000..a3f70b96b4 --- /dev/null +++ b/samples/gamepad/device_tester.py @@ -0,0 +1,456 @@ +#!/usr/bin/env python + +import sys + +from direct.showbase.ShowBase import ShowBase +from direct.showbase.DirectObject import DirectObject +from panda3d.core import InputDeviceManager, InputDevice +from panda3d.core import VBase4, Vec2 +from panda3d.core import TextNode +from direct.gui.DirectGui import ( + DGG, + DirectFrame, + DirectButton, + DirectLabel, + DirectScrolledFrame, + DirectSlider, +) + + +class Main(ShowBase): + def __init__(self): + super().__init__() + base.disableMouse() + self.accept("escape", sys.exit) + self.device_connectivity_monitor = DeviceConnectivityMonitor() + + +class DeviceConnectivityMonitor(DirectObject): + def __init__(self): + super().__init__() + self.mgr = InputDeviceManager.get_global_ptr() + self.create_device_menu() + + self.devices = {} + for device in self.mgr.get_devices(): + self.connect_device(device) + + self.accept("connect-device", self.connect_device) + self.accept("disconnect-device", self.disconnect_device) + + def create_device_menu(self): + self.current_panel = None + self.buttons = {} + self.devices_frame = DirectScrolledFrame( + frameSize=VBase4( + 0, + base.a2dLeft*-0.75, + base.a2dBottom - base.a2dTop, + 0, + ), + frameColor=VBase4(0, 0, 0.25, 1.0), + canvasSize=VBase4( + 0, + base.a2dLeft*-0.75, + 0, + 0, + ), + scrollBarWidth=0.08, + manageScrollBars=True, + autoHideScrollBars=True, + pos=(base.a2dLeft, 0, base.a2dTop), + parent=base.aspect2d, + ) + + self.devices_frame.setCanvasSize() + + def create_menu_button(self, device): + button = DirectButton( + command=self.switch_to_panel, + extraArgs=[device], + text=device.name, + text_scale=0.05, + text_align=TextNode.ALeft, + text_fg=VBase4(0.0, 0.0, 0.0, 1.0), + text_pos=Vec2(0.01, base.a2dBottom / 10.0), + relief=1, + pad=Vec2(0.01, 0.01), + frameColor=VBase4(0.8, 0.8, 0.8, 1.0), + frameSize=VBase4( + 0.0, + base.a2dLeft*-0.75 - 0.081, # 0.08=Scrollbar, 0.001=inaccuracy + base.a2dBottom / 5.0, + 0.0, + ), + parent=self.devices_frame.getCanvas(), + ) + self.buttons[device] = button + + def destroy_menu_button(self, device): + self.buttons[device].detach_node() + del self.buttons[device] + + def refresh_device_menu(self): + self.devices_frame['canvasSize'] = VBase4( + 0, + base.a2dLeft*-0.75, + base.a2dBottom / 5.0 * len(self.buttons), + 0, + ) + self.devices_frame.setCanvasSize() + sorted_buttons = sorted(self.buttons.items(), key=lambda i: i[0].name) + for idx, (dev, button) in enumerate(sorted_buttons): + button.set_pos( + 0, + 0, + (base.a2dBottom / 5.0) * idx, + ) + + def switch_to_panel(self, device): + if self.current_panel is not None: + self.devices[self.current_panel].hide() + self.current_panel = device + self.devices[self.current_panel].show() + + def connect_device(self, device): + self.devices[device] = DeviceMonitor(device) + self.switch_to_panel(device) + self.create_menu_button(device) + self.refresh_device_menu() + + def disconnect_device(self, device): + self.devices[device].deactivate() + del self.devices[device] + if self.current_panel == device: + self.current_panel = None + if len(self.devices) > 0: + active_device = sorted( + self.devices.keys(), + key=lambda d: d.name, + )[0] + self.switch_to_panel(active_device) + + self.destroy_menu_button(device) + self.refresh_device_menu() + + +class DeviceMonitor(DirectObject): + def __init__(self, device): + super().__init__() + self.device = device + self.create_panel() + self.activate() + self.hide() + + def activate(self): + print("Device connected") + print(" Name : {}".format(self.device.name)) + print(" Type : {}".format(self.device.device_class.name)) + print(" Manufacturer: {}".format(self.device.manufacturer)) + print(" ID : {:04x}:{:04x}".format(self.device.vendor_id, + self.device.product_id)) + axis_names = [axis.axis.name for axis in self.device.axes] + print(" Axes : {} ({})".format(len(self.device.axes), + ', '.join(axis_names))) + button_names = [button.handle.name for button in self.device.buttons] + print(" Buttons : {} ({})".format(len(self.device.buttons), + ', '.join(button_names))) + + base.attachInputDevice(self.device) + + self.task = base.taskMgr.add( + self.update, + "Monitor for {}".format(self.device.name), + sort=10, + ) + + def deactivate(self): + print("\"{}\" disconnected".format(self.device.name)) + base.taskMgr.remove(self.task) + self.panel.detach_node() + + def create_panel(self): + panel_width = base.a2dLeft * -0.25 + base.a2dRight + scroll_bar_width = 0.08 + # NOTE: -0.001 because thanks to inaccuracy the vertical bar appears... + canvas_width = panel_width - scroll_bar_width - 0.001 + canvas_height = base.a2dBottom - base.a2dTop + + self.panel = DirectScrolledFrame( + frameSize=VBase4( + 0, + panel_width, + canvas_height, + 0, + ), + frameColor=VBase4(0.8, 0.8, 0.8, 1), + canvasSize=VBase4( + 0, + canvas_width, + canvas_height, + 0, + ), + scrollBarWidth=scroll_bar_width, + manageScrollBars=True, + autoHideScrollBars=True, + pos=(base.a2dLeft * 0.25, 0, base.a2dTop), + parent=base.aspect2d, + ) + panel_canvas = self.panel.getCanvas() + offset = -0.0 + + # Style sheets + + half_width_entry = dict( + frameSize=VBase4( + 0, + canvas_width / 2, + -0.1, + 0, + ), + parent=panel_canvas, + frameColor=VBase4(0.8, 0.8, 0.8, 1), + ) + left_aligned_small_text = dict( + text_align=TextNode.ALeft, + text_scale=0.05, + text_fg=VBase4(0,0,0,1), + text_pos=(0.05, -0.06), + ) + half_width_text_frame = dict( + **half_width_entry, + **left_aligned_small_text, + ) + + header = dict( + frameSize=VBase4( + 0, + canvas_width, + -0.1, + 0, + ), + parent=panel_canvas, + frameColor=VBase4(0.6, 0.6, 0.6, 1), + text_align=TextNode.ALeft, + text_scale=0.1, + text_fg=VBase4(0,0,0,1), + text_pos=(0.05, -0.075), + ) + + # Basic device data (name, device class, manufacturer, USB ID) + + self.device_header = DirectLabel( + text="Device data", + pos=(0, 0, offset), + **header, + ) + offset -= 0.1 + + def add_data_entry(offset, label, text): + self.name = DirectLabel( + text=label, + pos=(0, 0, offset), + **half_width_text_frame, + ) + self.name = DirectLabel( + text=text, + pos=(canvas_width / 2, 0, offset), + **half_width_text_frame, + ) + + metadata = [ + ('Name', self.device.name), + ('Device class', self.device.device_class.name), + ('Manufacturer', self.device.manufacturer), + ('USB ID', + "{:04x}:{:04x}".format( + self.device.vendor_id, + self.device.product_id, + ), + ), + ] + for label, text in metadata: + add_data_entry(offset, label, text) + offset -= 0.1 + + # Axes + + self.axis_sliders = [] + if len(self.device.axes) > 0: + offset -= 0.1 + self.axes_header = DirectLabel( + text="Axes", + pos=(0, 0, offset), + **header, + ) + offset -= 0.1 + + def add_axis(offset, axis_name): + slider_width = canvas_width / 2 + label = DirectLabel( + text=axis_name, + **left_aligned_small_text, + pos=(0.05, 0, offset), + parent=panel_canvas, + ) + slider = DirectSlider( + value=0.0, + range=(-1.0, 1.0), + state=DGG.DISABLED, + frameSize=VBase4( + 0, + slider_width, + -0.1, + 0, + ), + thumb_frameSize=VBase4( + 0.0, + 0.04, + -0.04, + 0.04), + frameColor=VBase4(0.3, 0.3, 0.3, 1), + pos=(canvas_width - slider_width, 0, offset), + parent=panel_canvas, + ) + return slider + + for axis in self.device.axes: + axis_slider = add_axis(offset, axis.axis.name) + self.axis_sliders.append(axis_slider) + offset -= 0.1 + + # Buttons + + self.button_buttons = [] + if len(self.device.buttons) > 0: + offset -= 0.1 + self.buttons_header = DirectLabel( + text="Buttons", + pos=(0, 0, offset), + **header, + ) + offset -= 0.1 + + def add_button(offset, button_name): + button_width = canvas_width / 2 + label = DirectLabel( + text=button_name, + **left_aligned_small_text, + pos=(0.05, 0, offset), + parent=panel_canvas, + ) + button = DirectFrame( + frameSize=VBase4( + 0, + button_width, + -0.1, + 0, + ), + text="", + text_align=TextNode.ACenter, + text_scale=0.05, + text_fg=VBase4(0,0,0,1), + text_pos=(button_width / 2, -0.06), + frameColor=VBase4(0.3, 0.3, 0.3, 1), + pos=(canvas_width - button_width, 0, offset), + parent=panel_canvas, + ) + return button + + for i in range(len(self.device.buttons)): + button_name = self.device.buttons[i].handle.name + button_button = add_button(offset, button_name) + self.button_buttons.append(button_button) + offset -= 0.1 + + # Vibration + + self.vibration = [] + if self.device.has_feature(InputDevice.Feature.vibration): + offset -= 0.1 + self.vibration_header = DirectLabel( + text="Vibration", + pos=(0, 0, offset), + **header, + ) + offset -= 0.1 + + def add_vibration(offset, axis_name, index): + slider_width = canvas_width / 2 + label = DirectLabel( + text=axis_name, + **left_aligned_small_text, + pos=(0.05, 0, offset), + parent=panel_canvas, + ) + slider = DirectSlider( + value=0.0, + range=(0.0, 1.0), + command=self.update_vibration, + frameSize=VBase4( + 0, + slider_width, + -0.1, + 0, + ), + thumb_frameSize=VBase4( + 0.0, + 0.04, + -0.04, + 0.04), + frameColor=VBase4(0.3, 0.3, 0.3, 1), + pos=(canvas_width - slider_width, 0, offset), + parent=panel_canvas, + ) + return slider + + for index, name in enumerate(["low frequency", "high frequency"]): + self.vibration.append(add_vibration(offset, name, index)) + offset -= 0.1 + + # Resize the panel's canvas to the widgets actually in it. + if -offset > -canvas_height: + self.panel['canvasSize'] = VBase4( + 0, + canvas_width, + offset, + 0, + ) + self.panel.setCanvasSize() + + def show(self): + # FIXME: Activate update task here, and deactivate it in hide()? + self.panel.show() + + def hide(self): + self.panel.hide() + + def update_vibration(self): + low = self.vibration[0]['value'] + high = self.vibration[1]['value'] + self.device.set_vibration(low, high) + + def update(self, task): + # FIXME: There needs to be a demo of events here, too. + for idx, slider in enumerate(self.axis_sliders): + slider["value"] = self.device.axes[idx].value + for idx, button in enumerate(self.button_buttons): + if self.device.buttons[idx].known: + if self.device.buttons[idx].pressed: + button['frameColor'] = VBase4(0.0, 0.8, 0.0, 1) + button['text'] = "down" + else: + button['frameColor'] = VBase4(0.3, 0.3, 0.3, 1) + button['text'] = "up" + else: + # State is InputDevice.S_unknown. This happens if the device + # manager hasn't polled yet, and in some cases before a button + # has been pressed after the program's start. + button['frameColor'] = VBase4(0.8, 0.8, 0.0, 1) + button['text'] = "unknown" + return task.cont + + +if __name__ == '__main__': + main = Main() + main.run()