mappingGUI.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399
  1. #!/usr/bin/env python
  2. '''
  3. Demonstrate how a simple button mapping gui can be written
  4. '''
  5. from direct.showbase.ShowBase import ShowBase
  6. from direct.gui.DirectGui import (
  7. DGG,
  8. DirectFrame,
  9. DirectButton,
  10. DirectLabel,
  11. OkCancelDialog,
  12. DirectScrolledFrame)
  13. from panda3d.core import (
  14. VBase4,
  15. TextNode,
  16. Vec2,
  17. InputDevice,
  18. loadPrcFileData,
  19. GamepadButton,
  20. KeyboardButton)
  21. # Make sure the textures look crisp on every device that supports
  22. # non-power-2 textures
  23. loadPrcFileData("", "textures-auto-power-2 #t")
  24. # How much an axis should have moved for it to register as a movement.
  25. DEAD_ZONE = 0.33
  26. class InputMapping(object):
  27. """A container class for storing a mapping from a string action to either
  28. an axis or a button. You could extend this with additional methods to load
  29. the default mappings from a configuration file. """
  30. # Define all the possible actions.
  31. actions = (
  32. "Move forward", "Move backward", "Move left", "Move right", "Jump",
  33. "Buy", "Use", "Break", "Fix", "Trash", "Change", "Mail", "Upgrade",
  34. )
  35. def __init__(self):
  36. self.__map = dict.fromkeys(self.actions)
  37. def mapButton(self, action, button):
  38. self.__map[action] = ("button", str(button))
  39. def mapAxis(self, action, axis):
  40. self.__map[action] = ("axis", axis.name)
  41. def unmap(self):
  42. self.__map[action] = None
  43. def formatMapping(self, action):
  44. """Returns a string label describing the mapping for a given action,
  45. for displaying in a GUI. """
  46. mapping = self.__map.get(action)
  47. if not mapping:
  48. return "Unmapped"
  49. # Format the symbolic string from Panda nicely. In a real-world game,
  50. # you might want to look these up in a translation table, or so.
  51. label = mapping[1].replace('_', ' ').title()
  52. if mapping[0] == "axis":
  53. return "Axis: " + label
  54. else:
  55. return "Button: " + label
  56. class ChangeActionDialog(object):
  57. """Encapsulates the UI dialog that opens up when changing a mapping. It
  58. holds the state of which action is being set and which button is pressed
  59. and invokes a callback function when the dialog is exited."""
  60. def __init__(self, action, button_geom, command):
  61. # This stores which action we are remapping.
  62. self.action = action
  63. # This will store the key/axis that we want to asign to an action
  64. self.newInputType = ""
  65. self.newInput = ""
  66. self.setKeyCalled = False
  67. self.__command = command
  68. # Initialize the DirectGUI stuff.
  69. self.dialog = OkCancelDialog(
  70. dialogName="dlg_device_input",
  71. pos=(0, 0, 0.25),
  72. text="Hit desired key:",
  73. text_fg=VBase4(0.898, 0.839, 0.730, 1.0),
  74. text_shadow=VBase4(0, 0, 0, 0.75),
  75. text_shadowOffset=Vec2(0.05, 0.05),
  76. text_scale=0.05,
  77. text_align=TextNode.ACenter,
  78. fadeScreen=0.65,
  79. frameColor=VBase4(0.3, 0.3, 0.3, 1),
  80. button_geom=button_geom,
  81. button_scale=0.15,
  82. button_text_scale=0.35,
  83. button_text_align=TextNode.ALeft,
  84. button_text_fg=VBase4(0.898, 0.839, 0.730, 1.0),
  85. button_text_pos=Vec2(-0.9, -0.125),
  86. button_relief=1,
  87. button_pad=Vec2(0.01, 0.01),
  88. button_frameColor=VBase4(0, 0, 0, 0),
  89. button_frameSize=VBase4(-1.0, 1.0, -0.25, 0.25),
  90. button_pressEffect=False,
  91. command=self.onClose)
  92. self.dialog.setTransparency(True)
  93. self.dialog.configureDialog()
  94. scale = self.dialog["image_scale"]
  95. self.dialog["image_scale"] = (scale[0]/2.0, scale[1], scale[2]/2.0)
  96. self.dialog["text_pos"] = (self.dialog["text_pos"][0], self.dialog["text_pos"][1] + 0.06)
  97. def buttonPressed(self, button):
  98. if any(button.guiItem.getState() == 1 for button in self.dialog.buttonList):
  99. # Ignore events while any of the dialog buttons are active, because
  100. # otherwise we register mouse clicks when the user is trying to
  101. # exit the dialog.
  102. return
  103. text = str(button).replace('_', ' ').title()
  104. self.dialog["text"] = "New event will be:\n\nButton: " + text
  105. self.newInputType = "button"
  106. self.newInput = button
  107. def axisMoved(self, axis):
  108. text = axis.name.replace('_', ' ').title()
  109. self.dialog["text"] = "New event will be:\n\nAxis: " + text
  110. self.newInputType = "axis"
  111. self.newInput = axis
  112. def onClose(self, result):
  113. """Called when the OK or Cancel button is pressed."""
  114. self.dialog.cleanup()
  115. # Call the constructor-supplied callback with our new setting, if any.
  116. if self.newInput and result == DGG.DIALOG_OK:
  117. self.__command(self.action, self.newInputType, self.newInput)
  118. else:
  119. # Cancel (or no input was pressed)
  120. self.__command(self.action, None, None)
  121. class MappingGUIDemo(ShowBase):
  122. def __init__(self):
  123. ShowBase.__init__(self)
  124. self.setBackgroundColor(0, 0, 0)
  125. # make the font look nice at a big scale
  126. DGG.getDefaultFont().setPixelsPerUnit(100)
  127. # Store our mapping, with some sensible defaults. In a real game, you
  128. # will want to load these from a configuration file.
  129. self.mapping = InputMapping()
  130. self.mapping.mapAxis("Move forward", InputDevice.Axis.left_y)
  131. self.mapping.mapAxis("Move backward", InputDevice.Axis.left_y)
  132. self.mapping.mapAxis("Move left", InputDevice.Axis.left_x)
  133. self.mapping.mapAxis("Move right", InputDevice.Axis.left_x)
  134. self.mapping.mapButton("Jump", GamepadButton.face_a())
  135. self.mapping.mapButton("Use", GamepadButton.face_b())
  136. self.mapping.mapButton("Break", GamepadButton.face_x())
  137. self.mapping.mapButton("Fix", GamepadButton.face_y())
  138. # The geometry for our basic buttons
  139. maps = loader.loadModel("models/button_map")
  140. self.buttonGeom = (
  141. maps.find("**/ready"),
  142. maps.find("**/click"),
  143. maps.find("**/hover"),
  144. maps.find("**/disabled"))
  145. # Change the default dialog skin.
  146. DGG.setDefaultDialogGeom("models/dialog.png")
  147. # create a sample title
  148. self.textscale = 0.1
  149. self.title = DirectLabel(
  150. scale=self.textscale,
  151. pos=(base.a2dLeft + 0.05, 0.0, base.a2dTop - (self.textscale + 0.05)),
  152. frameColor=VBase4(0, 0, 0, 0),
  153. text="Button Mapping",
  154. text_align=TextNode.ALeft,
  155. text_fg=VBase4(1, 1, 1, 1),
  156. text_shadow=VBase4(0, 0, 0, 0.75),
  157. text_shadowOffset=Vec2(0.05, 0.05))
  158. self.title.setTransparency(1)
  159. # Set up the list of actions that we can map keys to
  160. # create a frame that will create the scrollbars for us
  161. # Load the models for the scrollbar elements
  162. thumbMaps = loader.loadModel("models/thumb_map")
  163. thumbGeom = (
  164. thumbMaps.find("**/thumb_ready"),
  165. thumbMaps.find("**/thumb_click"),
  166. thumbMaps.find("**/thumb_hover"),
  167. thumbMaps.find("**/thumb_disabled"))
  168. incMaps = loader.loadModel("models/inc_map")
  169. incGeom = (
  170. incMaps.find("**/inc_ready"),
  171. incMaps.find("**/inc_click"),
  172. incMaps.find("**/inc_hover"),
  173. incMaps.find("**/inc_disabled"))
  174. decMaps = loader.loadModel("models/dec_map")
  175. decGeom = (
  176. decMaps.find("**/dec_ready"),
  177. decMaps.find("**/dec_click"),
  178. decMaps.find("**/dec_hover"),
  179. decMaps.find("**/dec_disabled"))
  180. # create the scrolled frame that will hold our list
  181. self.lstActionMap = DirectScrolledFrame(
  182. # make the frame occupy the whole window
  183. frameSize=VBase4(base.a2dLeft, base.a2dRight, 0.0, 1.55),
  184. # make the canvas as big as the frame
  185. canvasSize=VBase4(base.a2dLeft, base.a2dRight, 0.0, 0.0),
  186. # set the frames color to white
  187. frameColor=VBase4(0, 0, 0.25, 0.75),
  188. pos=(0, 0, -0.8),
  189. verticalScroll_scrollSize=0.2,
  190. verticalScroll_frameColor=VBase4(0.02, 0.02, 0.02, 1),
  191. verticalScroll_thumb_relief=1,
  192. verticalScroll_thumb_geom=thumbGeom,
  193. verticalScroll_thumb_pressEffect=False,
  194. verticalScroll_thumb_frameColor=VBase4(0, 0, 0, 0),
  195. verticalScroll_incButton_relief=1,
  196. verticalScroll_incButton_geom=incGeom,
  197. verticalScroll_incButton_pressEffect=False,
  198. verticalScroll_incButton_frameColor=VBase4(0, 0, 0, 0),
  199. verticalScroll_decButton_relief=1,
  200. verticalScroll_decButton_geom=decGeom,
  201. verticalScroll_decButton_pressEffect=False,
  202. verticalScroll_decButton_frameColor=VBase4(0, 0, 0, 0),)
  203. # creat the list items
  204. idx = 0
  205. self.listBGEven = base.loader.loadModel("models/list_item_even")
  206. self.listBGOdd = base.loader.loadModel("models/list_item_odd")
  207. self.actionLabels = {}
  208. for action in self.mapping.actions:
  209. mapped = self.mapping.formatMapping(action)
  210. item = self.__makeListItem(action, mapped, idx)
  211. item.reparentTo(self.lstActionMap.getCanvas())
  212. idx += 1
  213. # recalculate the canvas size to set scrollbars if necesary
  214. self.lstActionMap["canvasSize"] = (
  215. base.a2dLeft+0.05, base.a2dRight-0.05,
  216. -(len(self.mapping.actions)*0.1), 0.09)
  217. self.lstActionMap.setCanvasSize()
  218. def closeDialog(self, action, newInputType, newInput):
  219. """Called in callback when the dialog is closed. newInputType will be
  220. "button" or "axis", or None if the remapping was cancelled."""
  221. self.dlgInput = None
  222. if newInputType is not None:
  223. # map the event to the given action
  224. if newInputType == "axis":
  225. self.mapping.mapAxis(action, newInput)
  226. else:
  227. self.mapping.mapButton(action, newInput)
  228. # actualize the label in the list that shows the current
  229. # event for the action
  230. self.actionLabels[action]["text"] = self.mapping.formatMapping(action)
  231. # cleanup
  232. for bt in base.buttonThrowers:
  233. bt.node().setSpecificFlag(True)
  234. bt.node().setButtonDownEvent("")
  235. for bt in base.deviceButtonThrowers:
  236. bt.node().setSpecificFlag(True)
  237. bt.node().setButtonDownEvent("")
  238. taskMgr.remove("checkControls")
  239. # Now detach all the input devices.
  240. for device in self.attachedDevices:
  241. base.detachInputDevice(device)
  242. self.attachedDevices.clear()
  243. def changeMapping(self, action):
  244. # Create the dialog window
  245. self.dlgInput = ChangeActionDialog(action, button_geom=self.buttonGeom, command=self.closeDialog)
  246. # Attach all input devices.
  247. devices = base.devices.getDevices()
  248. for device in devices:
  249. base.attachInputDevice(device)
  250. self.attachedDevices = devices
  251. # Disable regular button events on all button event throwers, and
  252. # instead broadcast a generic event.
  253. for bt in base.buttonThrowers:
  254. bt.node().setSpecificFlag(False)
  255. bt.node().setButtonDownEvent("keyListenEvent")
  256. for bt in base.deviceButtonThrowers:
  257. bt.node().setSpecificFlag(False)
  258. bt.node().setButtonDownEvent("deviceListenEvent")
  259. self.accept("keyListenEvent", self.dlgInput.buttonPressed)
  260. self.accept("deviceListenEvent", self.dlgInput.buttonPressed)
  261. # As there are no events thrown for control changes, we set up a task
  262. # to check if the controls were moved
  263. # This list will help us for checking which controls were moved
  264. self.axisStates = {None: {}}
  265. # fill it with all available controls
  266. for device in devices:
  267. for axis in device.axes:
  268. if device not in self.axisStates.keys():
  269. self.axisStates.update({device: {axis.axis: axis.value}})
  270. else:
  271. self.axisStates[device].update({axis.axis: axis.value})
  272. # start the task
  273. taskMgr.add(self.watchControls, "checkControls")
  274. def watchControls(self, task):
  275. # move through all devices and all it's controls
  276. for device in self.attachedDevices:
  277. if device.device_class == InputDevice.DC_mouse:
  278. # Ignore mouse axis movement, or the user can't even navigate
  279. # to the OK/Cancel buttons!
  280. continue
  281. for axis in device.axes:
  282. # if a control got changed more than the given dead zone
  283. if self.axisStates[device][axis.axis] + DEAD_ZONE < axis.value or \
  284. self.axisStates[device][axis.axis] - DEAD_ZONE > axis.value:
  285. # set the current state in the dict
  286. self.axisStates[device][axis.axis] = axis.value
  287. # Format the axis for being displayed.
  288. if axis.axis != InputDevice.Axis.none:
  289. #label = axis.axis.name.replace('_', ' ').title()
  290. self.dlgInput.axisMoved(axis.axis)
  291. return task.cont
  292. def __makeListItem(self, action, event, index):
  293. def dummy(): pass
  294. if index % 2 == 0:
  295. bg = self.listBGEven
  296. else:
  297. bg = self.listBGOdd
  298. item = DirectFrame(
  299. text=action,
  300. geom=bg,
  301. geom_scale=(base.a2dRight-0.05, 1, 0.1),
  302. frameSize=VBase4(base.a2dLeft+0.05, base.a2dRight-0.05, -0.05, 0.05),
  303. frameColor=VBase4(1,0,0,0),
  304. text_align=TextNode.ALeft,
  305. text_scale=0.05,
  306. text_fg=VBase4(1,1,1,1),
  307. text_pos=(base.a2dLeft + 0.3, -0.015),
  308. text_shadow=VBase4(0, 0, 0, 0.35),
  309. text_shadowOffset=Vec2(-0.05, -0.05),
  310. pos=(0.05, 0, -(0.10 * index)))
  311. item.setTransparency(True)
  312. lbl = DirectLabel(
  313. text=event,
  314. text_fg=VBase4(1, 1, 1, 1),
  315. text_scale=0.05,
  316. text_pos=Vec2(0, -0.015),
  317. frameColor=VBase4(0, 0, 0, 0),
  318. )
  319. lbl.reparentTo(item)
  320. lbl.setTransparency(True)
  321. self.actionLabels[action] = lbl
  322. buttonScale = 0.15
  323. btn = DirectButton(
  324. text="Change",
  325. geom=self.buttonGeom,
  326. scale=buttonScale,
  327. text_scale=0.25,
  328. text_align=TextNode.ALeft,
  329. text_fg=VBase4(0.898, 0.839, 0.730, 1.0),
  330. text_pos=Vec2(-0.9, -0.085),
  331. relief=1,
  332. pad=Vec2(0.01, 0.01),
  333. frameColor=VBase4(0, 0, 0, 0),
  334. frameSize=VBase4(-1.0, 1.0, -0.25, 0.25),
  335. pos=(base.a2dRight-(0.898*buttonScale+0.3), 0, 0),
  336. pressEffect=False,
  337. command=self.changeMapping,
  338. extraArgs=[action])
  339. btn.setTransparency(True)
  340. btn.reparentTo(item)
  341. return item
  342. app = MappingGUIDemo()
  343. app.run()