#!/usr/bin/env python ''' Demonstrate how a simple button mapping gui can be written ''' from direct.showbase.ShowBase import ShowBase from direct.gui.DirectGui import ( DGG, DirectFrame, DirectButton, DirectLabel, OkCancelDialog, DirectScrolledFrame) from panda3d.core import ( VBase4, TextNode, Vec2, InputDevice, loadPrcFileData, GamepadButton, KeyboardButton) # Make sure the textures look crisp on every device that supports # non-power-2 textures loadPrcFileData("", "textures-auto-power-2 #t") # How much an axis should have moved for it to register as a movement. DEAD_ZONE = 0.33 class InputMapping(object): """A container class for storing a mapping from a string action to either an axis or a button. You could extend this with additional methods to load the default mappings from a configuration file. """ # Define all the possible actions. actions = ( "Move forward", "Move backward", "Move left", "Move right", "Jump", "Buy", "Use", "Break", "Fix", "Trash", "Change", "Mail", "Upgrade", ) def __init__(self): self.__map = dict.fromkeys(self.actions) def mapButton(self, action, button): self.__map[action] = ("button", str(button)) def mapAxis(self, action, axis): self.__map[action] = ("axis", axis.name) def unmap(self): self.__map[action] = None def formatMapping(self, action): """Returns a string label describing the mapping for a given action, for displaying in a GUI. """ mapping = self.__map.get(action) if not mapping: return "Unmapped" # Format the symbolic string from Panda nicely. In a real-world game, # you might want to look these up in a translation table, or so. label = mapping[1].replace('_', ' ').title() if mapping[0] == "axis": return "Axis: " + label else: return "Button: " + label class ChangeActionDialog(object): """Encapsulates the UI dialog that opens up when changing a mapping. It holds the state of which action is being set and which button is pressed and invokes a callback function when the dialog is exited.""" def __init__(self, action, button_geom, command): # This stores which action we are remapping. self.action = action # This will store the key/axis that we want to asign to an action self.newInputType = "" self.newInput = "" self.setKeyCalled = False self.__command = command self.attachedDevices = [] # Initialize the DirectGUI stuff. self.dialog = OkCancelDialog( dialogName="dlg_device_input", pos=(0, 0, 0.25), text="Hit desired key:", text_fg=VBase4(0.898, 0.839, 0.730, 1.0), text_shadow=VBase4(0, 0, 0, 0.75), text_shadowOffset=Vec2(0.05, 0.05), text_scale=0.05, text_align=TextNode.ACenter, fadeScreen=0.65, frameColor=VBase4(0.3, 0.3, 0.3, 1), button_geom=button_geom, button_scale=0.15, button_text_scale=0.35, button_text_align=TextNode.ALeft, button_text_fg=VBase4(0.898, 0.839, 0.730, 1.0), button_text_pos=Vec2(-0.9, -0.125), button_relief=1, button_pad=Vec2(0.01, 0.01), button_frameColor=VBase4(0, 0, 0, 0), button_frameSize=VBase4(-1.0, 1.0, -0.25, 0.25), button_pressEffect=False, command=self.onClose) self.dialog.setTransparency(True) self.dialog.configureDialog() scale = self.dialog["image_scale"] self.dialog["image_scale"] = (scale[0]/2.0, scale[1], scale[2]/2.0) self.dialog["text_pos"] = (self.dialog["text_pos"][0], self.dialog["text_pos"][1] + 0.06) def buttonPressed(self, button): if any(button.guiItem.getState() == 1 for button in self.dialog.buttonList): # Ignore events while any of the dialog buttons are active, because # otherwise we register mouse clicks when the user is trying to # exit the dialog. return text = str(button).replace('_', ' ').title() self.dialog["text"] = "New event will be:\n\nButton: " + text self.newInputType = "button" self.newInput = button def axisMoved(self, axis): text = axis.name.replace('_', ' ').title() self.dialog["text"] = "New event will be:\n\nAxis: " + text self.newInputType = "axis" self.newInput = axis def onClose(self, result): """Called when the OK or Cancel button is pressed.""" self.dialog.cleanup() # Call the constructor-supplied callback with our new setting, if any. if self.newInput and result == DGG.DIALOG_OK: self.__command(self.action, self.newInputType, self.newInput) else: # Cancel (or no input was pressed) self.__command(self.action, None, None) class MappingGUIDemo(ShowBase): def __init__(self): ShowBase.__init__(self) self.setBackgroundColor(0, 0, 0) # make the font look nice at a big scale DGG.getDefaultFont().setPixelsPerUnit(100) # Store our mapping, with some sensible defaults. In a real game, you # will want to load these from a configuration file. self.mapping = InputMapping() self.mapping.mapAxis("Move forward", InputDevice.Axis.left_y) self.mapping.mapAxis("Move backward", InputDevice.Axis.left_y) self.mapping.mapAxis("Move left", InputDevice.Axis.left_x) self.mapping.mapAxis("Move right", InputDevice.Axis.left_x) self.mapping.mapButton("Jump", GamepadButton.face_a()) self.mapping.mapButton("Use", GamepadButton.face_b()) self.mapping.mapButton("Break", GamepadButton.face_x()) self.mapping.mapButton("Fix", GamepadButton.face_y()) # The geometry for our basic buttons maps = loader.loadModel("models/button_map") self.buttonGeom = ( maps.find("**/ready"), maps.find("**/click"), maps.find("**/hover"), maps.find("**/disabled")) # Change the default dialog skin. DGG.setDefaultDialogGeom("models/dialog.png") # create a sample title self.textscale = 0.1 self.title = DirectLabel( scale=self.textscale, pos=(base.a2dLeft + 0.05, 0.0, base.a2dTop - (self.textscale + 0.05)), frameColor=VBase4(0, 0, 0, 0), text="Button Mapping", text_align=TextNode.ALeft, text_fg=VBase4(1, 1, 1, 1), text_shadow=VBase4(0, 0, 0, 0.75), text_shadowOffset=Vec2(0.05, 0.05)) self.title.setTransparency(1) # Set up the list of actions that we can map keys to # create a frame that will create the scrollbars for us # Load the models for the scrollbar elements thumbMaps = loader.loadModel("models/thumb_map") thumbGeom = ( thumbMaps.find("**/thumb_ready"), thumbMaps.find("**/thumb_click"), thumbMaps.find("**/thumb_hover"), thumbMaps.find("**/thumb_disabled")) incMaps = loader.loadModel("models/inc_map") incGeom = ( incMaps.find("**/inc_ready"), incMaps.find("**/inc_click"), incMaps.find("**/inc_hover"), incMaps.find("**/inc_disabled")) decMaps = loader.loadModel("models/dec_map") decGeom = ( decMaps.find("**/dec_ready"), decMaps.find("**/dec_click"), decMaps.find("**/dec_hover"), decMaps.find("**/dec_disabled")) # create the scrolled frame that will hold our list self.lstActionMap = DirectScrolledFrame( # make the frame occupy the whole window frameSize=VBase4(base.a2dLeft, base.a2dRight, 0.0, 1.55), # make the canvas as big as the frame canvasSize=VBase4(base.a2dLeft, base.a2dRight, 0.0, 0.0), # set the frames color to white frameColor=VBase4(0, 0, 0.25, 0.75), pos=(0, 0, -0.8), verticalScroll_scrollSize=0.2, verticalScroll_frameColor=VBase4(0.02, 0.02, 0.02, 1), verticalScroll_thumb_relief=1, verticalScroll_thumb_geom=thumbGeom, verticalScroll_thumb_pressEffect=False, verticalScroll_thumb_frameColor=VBase4(0, 0, 0, 0), verticalScroll_incButton_relief=1, verticalScroll_incButton_geom=incGeom, verticalScroll_incButton_pressEffect=False, verticalScroll_incButton_frameColor=VBase4(0, 0, 0, 0), verticalScroll_decButton_relief=1, verticalScroll_decButton_geom=decGeom, verticalScroll_decButton_pressEffect=False, verticalScroll_decButton_frameColor=VBase4(0, 0, 0, 0),) # creat the list items idx = 0 self.listBGEven = base.loader.loadModel("models/list_item_even") self.listBGOdd = base.loader.loadModel("models/list_item_odd") self.actionLabels = {} for action in self.mapping.actions: mapped = self.mapping.formatMapping(action) item = self.__makeListItem(action, mapped, idx) item.reparentTo(self.lstActionMap.getCanvas()) idx += 1 # recalculate the canvas size to set scrollbars if necesary self.lstActionMap["canvasSize"] = ( base.a2dLeft+0.05, base.a2dRight-0.05, -(len(self.mapping.actions)*0.1), 0.09) self.lstActionMap.setCanvasSize() def closeDialog(self, action, newInputType, newInput): """Called in callback when the dialog is closed. newInputType will be "button" or "axis", or None if the remapping was cancelled.""" self.dlgInput = None if newInputType is not None: # map the event to the given action if newInputType == "axis": self.mapping.mapAxis(action, newInput) else: self.mapping.mapButton(action, newInput) # actualize the label in the list that shows the current # event for the action self.actionLabels[action]["text"] = self.mapping.formatMapping(action) # cleanup for bt in base.buttonThrowers: bt.node().setSpecificFlag(True) bt.node().setButtonDownEvent("") for bt in base.deviceButtonThrowers: bt.node().setSpecificFlag(True) bt.node().setButtonDownEvent("") taskMgr.remove("checkControls") # Now detach all the input devices. for device in self.attachedDevices: base.detachInputDevice(device) self.attachedDevices.clear() def changeMapping(self, action): # Create the dialog window self.dlgInput = ChangeActionDialog(action, button_geom=self.buttonGeom, command=self.closeDialog) # Attach all input devices. devices = base.devices.getDevices() for device in devices: base.attachInputDevice(device) self.attachedDevices = devices # Disable regular button events on all button event throwers, and # instead broadcast a generic event. for bt in base.buttonThrowers: bt.node().setSpecificFlag(False) bt.node().setButtonDownEvent("keyListenEvent") for bt in base.deviceButtonThrowers: bt.node().setSpecificFlag(False) bt.node().setButtonDownEvent("deviceListenEvent") self.accept("keyListenEvent", self.dlgInput.buttonPressed) self.accept("deviceListenEvent", self.dlgInput.buttonPressed) # As there are no events thrown for control changes, we set up a task # to check if the controls were moved # This list will help us for checking which controls were moved self.axisStates = {None: {}} # fill it with all available controls for device in devices: for axis in device.axes: if device not in self.axisStates.keys(): self.axisStates.update({device: {axis.axis: axis.value}}) else: self.axisStates[device].update({axis.axis: axis.value}) # start the task taskMgr.add(self.watchControls, "checkControls") def watchControls(self, task): # move through all devices and all it's controls for device in self.attachedDevices: if device.device_class == InputDevice.DeviceClass.mouse: # Ignore mouse axis movement, or the user can't even navigate # to the OK/Cancel buttons! continue for axis in device.axes: # if a control got changed more than the given dead zone if self.axisStates[device][axis.axis] + DEAD_ZONE < axis.value or \ self.axisStates[device][axis.axis] - DEAD_ZONE > axis.value: # set the current state in the dict self.axisStates[device][axis.axis] = axis.value # Format the axis for being displayed. if axis.axis != InputDevice.Axis.none: #label = axis.axis.name.replace('_', ' ').title() self.dlgInput.axisMoved(axis.axis) return task.cont def __makeListItem(self, action, event, index): def dummy(): pass if index % 2 == 0: bg = self.listBGEven else: bg = self.listBGOdd item = DirectFrame( text=action, geom=bg, geom_scale=(base.a2dRight-0.05, 1, 0.1), frameSize=VBase4(base.a2dLeft+0.05, base.a2dRight-0.05, -0.05, 0.05), frameColor=VBase4(1,0,0,0), text_align=TextNode.ALeft, text_scale=0.05, text_fg=VBase4(1,1,1,1), text_pos=(base.a2dLeft + 0.3, -0.015), text_shadow=VBase4(0, 0, 0, 0.35), text_shadowOffset=Vec2(-0.05, -0.05), pos=(0.05, 0, -(0.10 * index))) item.setTransparency(True) lbl = DirectLabel( text=event, text_fg=VBase4(1, 1, 1, 1), text_scale=0.05, text_pos=Vec2(0, -0.015), frameColor=VBase4(0, 0, 0, 0), ) lbl.reparentTo(item) lbl.setTransparency(True) self.actionLabels[action] = lbl buttonScale = 0.15 btn = DirectButton( text="Change", geom=self.buttonGeom, scale=buttonScale, text_scale=0.25, text_align=TextNode.ALeft, text_fg=VBase4(0.898, 0.839, 0.730, 1.0), text_pos=Vec2(-0.9, -0.085), relief=1, pad=Vec2(0.01, 0.01), frameColor=VBase4(0, 0, 0, 0), frameSize=VBase4(-1.0, 1.0, -0.25, 0.25), pos=(base.a2dRight-(0.898*buttonScale+0.3), 0, 0), pressEffect=False, command=self.changeMapping, extraArgs=[action]) btn.setTransparency(True) btn.reparentTo(item) return item app = MappingGUIDemo() app.run()