/menu.py

https://bitbucket.org/hyperion/simplemenu · Python · 224 lines · 150 code · 16 blank · 58 comment · 22 complexity · b1f32ced18e876cff398ea0eea14761f MD5 · raw file

  1. #! /usr/bin/python
  2. # -*- coding: utf-8 -*-
  3. import pygame
  4. from codecs import open
  5. import json
  6. def import_string(import_name):
  7. """
  8. Inspired by a function from the infamous werkzeug framework.
  9. the name is given by a dotted notation as shown here:
  10. - modul.funktion
  11. or:
  12. - modul.funktion:parameter
  13. or:
  14. - modul.modul.modul.funktion:parameter
  15. :param import_name: name of the function in dotted style
  16. :return: imported object
  17. """
  18. param = None
  19. try:
  20. module, obj = import_name.rsplit('.', 1)
  21. if ":" in obj:
  22. obj, param = obj.split(":", 1)
  23. return getattr(__import__(module, None, None, [obj]), obj), param
  24. except (ImportError, AttributeError):
  25. raise
  26. class Menu(object):
  27. """
  28. this represents a menu object.
  29. The core idea is to generate a separate surface for each given option
  30. and possible state.
  31. The states are selected or not selected, depending on the itemindex.
  32. Later depending on the actual state a item can be drawn in the
  33. approriate color just by its state.
  34. """
  35. def __init__(self, displayname, items, key, font, default_color,
  36. selected_color, itemindex=0, itemheight=30, menuwidth=150):
  37. self.displayname = displayname,
  38. self.items = self.generate_menu_surfaces(items, font,
  39. default_color, selected_color)
  40. self.key = key
  41. self.itemindex = itemindex
  42. self.itemheight = itemheight
  43. self.menuwidth = menuwidth
  44. def __repr__(self):
  45. return "Menu({0})".format(self.displayname)
  46. def generate_menu_surfaces(self, options, font, default_color,
  47. selected_color):
  48. items = []
  49. for option in options:
  50. item = {
  51. False: font.render(option["name"], True, default_color),
  52. True: font.render(option["name"], True, selected_color),
  53. "action": option["action"],
  54. # eigentlich unnötig, aber so zur Not noch einmal Textausgabe
  55. # möglich
  56. "text": option["name"],
  57. }
  58. items.append(item)
  59. return items
  60. def move_down(self):
  61. self.itemindex += 1
  62. if self.itemindex == len(self.items):
  63. self.itemindex = 0
  64. def move_up(self):
  65. self.itemindex -= 1
  66. if self.itemindex < 0:
  67. self.itemindex = len(self.items)-1
  68. def load_menu(fname, menuname=None):
  69. """
  70. Parses all Menus (or one special) out of a JSON file and generates
  71. a dictioary of Menu objects from that data.
  72. It has the following format:
  73. {
  74. "default_color": [R, G , B],
  75. "selected_color": [R, G, B],
  76. "fontname": string,
  77. "menus": {
  78. "main": {
  79. "display": string,
  80. "itemheight": int,
  81. "menuwidth": int,
  82. "itemindex": int,
  83. "key": string,
  84. "items": [
  85. {
  86. "name": string,
  87. "action": "module.function:parameter"
  88. },
  89. ...
  90. ]
  91. },
  92. "another_menu": {
  93. ...
  94. }
  95. }
  96. }
  97. Also a key_binding map ist built by the given keys for each menu. It maps
  98. one key-string to a Menu object, so one can use that later on to fire up
  99. the approriate menu for a hitten key.
  100. If a menuname is given, it only generates the Menu object for that special
  101. menu. No key_binding will be generated as the user seems to handle all by
  102. himself.
  103. :return: dict of Menus, dict of key_bindings
  104. """
  105. menus = {}
  106. key_binding = {}
  107. with open(fname, "r", encoding="utf-8") as infile:
  108. rawdata = json.load(infile)
  109. default_color = rawdata.get("default_color", (255, 255, 255))
  110. selected_color = rawdata.get("selected_color", (255, 0, 0))
  111. font = pygame.font.Font(rawdata.get("fontname"),
  112. rawdata.get("fontsize", 30)
  113. )
  114. if menuname:
  115. menunames = [menuname]
  116. else:
  117. menunames = rawdata["menus"].keys()
  118. try:
  119. for menuid in menunames:
  120. menu_data = rawdata["menus"][menuid]
  121. menus[menuid] = Menu(
  122. displayname = menu_data["display"],
  123. items = menu_data["items"],
  124. key = menu_data["key"],
  125. font = font,
  126. default_color = default_color,
  127. selected_color = selected_color,
  128. itemindex = menu_data.get("itemindex", 0),
  129. itemheight = menu_data.get("itemsize", 30),
  130. menuwidth = menu_data.get("menuwidth", 150)
  131. )
  132. key_binding[menu_data["key"]] = menus[menuid]
  133. except KeyError, e:
  134. print "Menü '{0}' existiert nicht in der Datei '{1}'".format(
  135. menuname, fname
  136. )
  137. return
  138. else:
  139. return menus[menuname] if menuname else menus, key_binding
  140. def render_menu(menu, surface, position):
  141. """
  142. Paints a given Menu object onto a given surface at a given position.
  143. :return: a rect of the painted area (usefull to make a backup of that)
  144. """
  145. x, y = position
  146. for index, item in enumerate(menu.items):
  147. surface.blit(
  148. item[menu.itemindex==index],
  149. (x, y + (index*menu.itemheight))
  150. )
  151. return pygame.Rect(x, y, menu.menuwidth, len(menu.items)*menu.itemheight)
  152. def restore_screen(backup_surface, screen):
  153. """
  154. Redraw the original screen after the menu was closed.
  155. """
  156. screen.blit(backup_surface, (0, 0))
  157. pygame.display.update(backup_surface.get_rect())
  158. def menu_loop(screen, menu):
  159. """
  160. a simple endless looping function to take control over user input as long
  161. as no menuitem was chosen (ENTER) or the user decided to quit (ESCAPE).
  162. One can use KEY_UP and KEY_DOWN to navigate though the menu.
  163. :return: the chosen menuitem, the action-function object,
  164. a given parameter
  165. TODO: make left-upper corner variable (now it is (100, 100))
  166. """
  167. menu_action = {
  168. pygame.K_DOWN: menu.move_down,
  169. pygame.K_UP: menu.move_up
  170. }
  171. # keep the actual screen in a copy
  172. backup_surface = screen.copy()
  173. menu_rect = render_menu(menu, screen, (100, 100))
  174. pygame.display.update(menu_rect)
  175. while True:
  176. for event in pygame.event.get():
  177. if event.type == pygame.KEYDOWN:
  178. if event.key == pygame.K_ESCAPE:
  179. # mit 'Escape'-Taste raus
  180. restore_screen(backup_surface, screen)
  181. return None, None, None
  182. elif event.key in (pygame.K_UP, pygame.K_DOWN):
  183. # hier auf- und abbewegen der Auswahl
  184. menu_action[event.key]()
  185. menu_rect = render_menu(menu, screen, (100, 100))
  186. pygame.display.update(menu_rect)
  187. elif event.key == pygame.K_RETURN:
  188. # Item & Action hinter einem Menüpunkt zurückgeben
  189. action, param = import_string(
  190. menu.items[menu.itemindex]["action"]
  191. )
  192. restore_screen(backup_surface, screen)
  193. return menu.items[menu.itemindex], action, param