mappingGUI.py 15 KB

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