| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401 |
- #!/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()
|