#!/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): if device in self.devices: return 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()