comparison src/browser/sat_browser/base_menu.py @ 498:60be99de3808

browser_side: menus refactorization + handle levels > 2
author souliane <souliane@mailoo.org>
date Fri, 25 Jul 2014 02:38:30 +0200
parents 5d8632a7bfde
children 4aa627b059df
comparison
equal deleted inserted replaced
497:516b06787c1a 498:60be99de3808
28 from sat.core.log import getLogger 28 from sat.core.log import getLogger
29 log = getLogger(__name__) 29 log = getLogger(__name__)
30 30
31 from pyjamas.ui.MenuBar import MenuBar 31 from pyjamas.ui.MenuBar import MenuBar
32 from pyjamas.ui.MenuItem import MenuItem 32 from pyjamas.ui.MenuItem import MenuItem
33 from pyjamas import Window
33 34
34 35
35 class MenuCmd: 36 class MenuCmd:
36 """Return an object with an "execute" method that can be set to a menu item callback""" 37 """Return an object with an "execute" method that can be set to a menu item callback"""
37 38
52 self.action_id = action_id 53 self.action_id = action_id
53 self.menu_data = menu_data 54 self.menu_data = menu_data
54 55
55 def execute(self): 56 def execute(self):
56 self.host.launchAction(self.action_id, self.menu_data) 57 self.host.launchAction(self.action_id, self.menu_data)
57
58
59 class CategoryMenuBar(MenuBar):
60 """A menu bar for a category (sub menu)"""
61 def __init__(self):
62 MenuBar.__init__(self, vertical=True)
63 58
64 59
65 class CategoryItem(MenuItem): 60 class CategoryItem(MenuItem):
66 """A category item with a non-internationalized name""" 61 """A category item with a non-internationalized name"""
67 def __init__(self, name, *args, **kwargs): 62 def __init__(self, name, *args, **kwargs):
68 MenuItem.__init__(self, *args, **kwargs) 63 MenuItem.__init__(self, *args, **kwargs)
69 self.name = name 64 self.name = name
70 65
71 66
72 class GenericMenuBar(MenuBar): 67 class GenericMenuBar(MenuBar):
73 68 """A menu bar with sub-categories and items"""
74 def __init__(self, host, vertical=False, **kwargs): 69
70 def __init__(self, host, vertical=False, styles=None, **kwargs):
71 """
72 @param host (SatWebFrontend): host instance
73 @param vertical (bool): True to display the popup menu vertically
74 @param styles (dict): specific styles to be applied:
75 - key: a value in ('moved_popup', 'menu_bar')
76 - value: a CSS class
77 the popup that are not displayed at the position computed by pyjamas.
78 """
75 MenuBar.__init__(self, vertical, **kwargs) 79 MenuBar.__init__(self, vertical, **kwargs)
76 self.host = host 80 self.host = host
77 self.moved_popup_style = None 81 self.styles = styles or {}
82 if 'menu_bar' in self.styles:
83 # XXX: pyjamas set the style to object string representation!
84 # FIXME: fix the bug upstream
85 first = 'gwt-MenuBar'
86 second = first + '-' + ('vertical' if self.vertical else 'horizontal')
87 self.setStyleName(' '.join([first, second, self.styles['menu_bar']]))
78 88
79 @classmethod 89 @classmethod
80 def getCategoryHTML(cls, type_, menu_name_i18n): 90 def getCategoryHTML(cls, type_, menu_name_i18n):
81 """Build from the given parameters the html to be displayed for a category item. 91 """Build the html to be used for displaying a category item.
82 92
83 Inheriting classes may overwrite this method. 93 Inheriting classes may overwrite this method.
84 @param type_ (str): category type 94 @param type_ (str): category type
85 @param menu_name_i18n (str): internationalized category name 95 @param menu_name_i18n (str): internationalized category name
86 @return: str 96 @return: str
88 return menu_name_i18n 98 return menu_name_i18n
89 99
90 def doItemAction(self, item, fireCommand): 100 def doItemAction(self, item, fireCommand):
91 """Overwrites the default behavior for the popup menu to fit in the screen""" 101 """Overwrites the default behavior for the popup menu to fit in the screen"""
92 MenuBar.doItemAction(self, item, fireCommand) 102 MenuBar.doItemAction(self, item, fireCommand)
93 if not self.vertical and self.popup: 103 if not self.popup:
94 # we not only move the last popup, but any which would go over the menu right extremity 104 return
95 most_left = self.getAbsoluteLeft() + self.getOffsetWidth() - self.popup.getOffsetWidth() 105 if self.vertical:
96 if item.getAbsoluteLeft() > most_left: 106 # move the popup if it would go over the screen's viewport
97 self.popup.setPopupPosition(most_left, 107 max_left = Window.getClientWidth() - self.getOffsetWidth() + 1 - self.popup.getOffsetWidth()
98 self.getAbsoluteTop() + 108 new_left = self.getAbsoluteLeft() - self.popup.getOffsetWidth() + 1
99 self.getOffsetHeight() - 1) 109 top = item.getAbsoluteTop()
100 # eventually smooth the popup edges to fit the menu own style 110 else:
101 if self.moved_popup_style: 111 # move the popup if it would go over the menu bar right extremity
102 self.popup.addStyleName(self.moved_popup_style) 112 max_left = self.getAbsoluteLeft() + self.getOffsetWidth() - self.popup.getOffsetWidth()
113 new_left = max_left
114 top = self.getAbsoluteTop() + self.getOffsetHeight() - 1
115 if item.getAbsoluteLeft() > max_left:
116 self.popup.setPopupPosition(new_left, top)
117 # eventually smooth the popup edges to fit the menu own style
118 if 'moved_popup' in self.styles:
119 self.popup.addStyleName(self.styles['moved_popup'])
103 120
104 def getCategories(self): 121 def getCategories(self):
105 """Return the categories items. 122 """Return all the categories items.
106 123
107 @return: list[CategoryItem] 124 @return: list[CategoryItem]
108 """ 125 """
109 return [item for item in self.items if isinstance(item, CategoryItem)] 126 return [item for item in self.items if isinstance(item, CategoryItem)]
110 127
111 def getSubMenu(self, category): 128 def getCategoryItem(self, path):
129 """Return the requested category item
130
131 @param path (list[str]): path to the category
132 @return: CategoryInstance or None
133 """
134 assert(len(path) > 0)
135 if len(path) > 1:
136 menu = self.getCategoryMenu(path[:1])
137 return menu.getCategoryItem(path[1:]) if menu else None
138 items = [item for item in self.items if isinstance(item, CategoryItem) and item.name == path[0]]
139 if len(items) == 1:
140 return items[0]
141 assert(items == []) # there should not be more than 1 category with the same name
142 return None
143
144 def getCategoryMenu(self, path):
112 """Return the popup menu for the given category 145 """Return the popup menu for the given category
113 146
114 @param category (str): category name 147 @param path (list[str]): path to the category
115 @return: CategoryMenuBar instance or None 148 @return: CategoryMenuBar instance or None
116 """ 149 """
117 try: 150 item = self.getCategoryItem(path)
118 return [item for item in self.items if isinstance(item, CategoryItem) and item.name == category][0].getSubMenu() 151 return item.getSubMenu() if item else None
119 except IndexError:
120 return None
121 152
122 def addSeparator(self): 153 def addSeparator(self):
123 """Add a separator between the categories""" 154 """Add a separator between the categories"""
124 self.addItem(CategoryItem(None, text='', asHTML=None, StyleName='menuSeparator')) 155 self.addItem(CategoryItem(None, text='', asHTML=None, StyleName='menuSeparator'))
125 156
126 def addCategory(self, menu_name, menu_name_i18n, type_, sub_menu): 157 def addCategory(self, path, path_i18n, type_, sub_menu=None):
127 """Add a category 158 """Add a category item and its associated sub-menu.
128 159
129 @param menu_name (str): category name 160 If the category already exists, do not overwrite the current sub-menu.
130 @param menu_name_i18n (str): internationalized category name 161 @param path (list[str], str): path to the category. Passing a string for
162 the category name is also accepted if there's no sub-category.
163 @param path_i18n (list[str], str): internationalized path to the category.
164 Passing a string for the internationalized category name is also accepted
165 if there's no sub-category.
131 @param type_ (str): category type 166 @param type_ (str): category type
132 @param sub_menu (CategoryMenuBar): category sub-menu 167 @param sub_menu (CategoryMenuBar): category sub-menu
133 """ 168 """
134 html = self.getCategoryHTML(type_, menu_name_i18n) 169 if isinstance(path, str):
135 self.addItem(CategoryItem(menu_name, text=html, asHTML=True, subMenu=sub_menu)) 170 path = [path]
136 171 if isinstance(path_i18n, str):
137 def addMenu(self, menu_name, menu_name_i18n, item_name_i18n, type_, menu_cmd): 172 path_i18n = [path_i18n]
173 assert(len(path) > 0 and len(path) == len(path_i18n))
174 current = self
175 count = len(path)
176 for menu_name, menu_name_i18n in zip(path, path_i18n):
177 tmp = current.getCategoryMenu([menu_name])
178 if not tmp:
179 html = self.getCategoryHTML(type_, menu_name_i18n)
180 tmp = CategoryMenuBar(self.host) if (count > 1 or not sub_menu) else sub_menu
181 current.addItem(CategoryItem(menu_name, text=html, asHTML=True, subMenu=tmp))
182 current = tmp
183 count -= 1
184
185 def addMenuItem(self, path, path_i18n, type_, menu_cmd):
138 """Add a new menu item 186 """Add a new menu item
139 @param menu_name (str): category name 187 @param path (list[str], str): path to the category, completed by a dummy
140 @param menu_name_i18n (str): internationalized menu name 188 value for the item in last position. Passing a string for the category
141 @param item_name_i18n (str): internationalized item name 189 name is also accepted if there's no sub-category.
190 @param path_i18n (list[str]): internationalized path to the item
142 @param type_ (str): category type in ('games', 'help', 'home', 'photos', 'plugins', 'settings', 'social') 191 @param type_ (str): category type in ('games', 'help', 'home', 'photos', 'plugins', 'settings', 'social')
143 @param menu_cmd (MenuCmd or PluginMenuCmd): instance to execute as the item callback 192 @param menu_cmd (MenuCmd or PluginMenuCmd): instance to execute as the item callback
144 """ 193 """
145 log.info("addMenu: %s %s %s %s %s" % (menu_name, menu_name_i18n, item_name_i18n, type_, menu_cmd)) 194 if isinstance(path, str):
146 sub_menu = self.getSubMenu(menu_name) 195 assert(len(path_i18n) == 2)
196 path = [path, None]
197 assert(len(path) > 1 and len(path) == len(path_i18n))
198 log.info("addMenuItem: %s %s %s %s" % (path, path_i18n, type_, menu_cmd))
199 sub_menu = self.getCategoryMenu(path[:-1])
147 if not sub_menu: 200 if not sub_menu:
148 sub_menu = CategoryMenuBar() 201 sub_menu = CategoryMenuBar(self.host)
149 self.addCategory(menu_name, menu_name_i18n, type_, sub_menu) 202 self.addCategory(path[:-1], path_i18n[:-1], type_, sub_menu)
150 if item_name_i18n and menu_cmd: 203 if menu_cmd:
151 sub_menu.addItem(item_name_i18n, menu_cmd) 204 sub_menu.addItem(path_i18n[-1], menu_cmd)
152 205
153 def addCachedMenus(self, type_, menu_data=None): 206 def addCachedMenus(self, type_, menu_data=None):
154 """Add cached menus to instance 207 """Add cached menus to instance
155 @param type_: menu type like is sat.core.sat_main.importMenu 208 @param type_: menu type like is sat.core.sat_main.importMenu
156 @param menu_data: data to send with these menus 209 @param menu_data: data to send with these menus
157 """ 210 """
158 menus = self.host.menus.get(type_, []) 211 menus = self.host.menus.get(type_, [])
159 for action_id, path, path_i18n in menus: 212 for action_id, path, path_i18n in menus:
160 if len(path) != 2:
161 raise NotImplementedError("Menu with a path != 2 are not implemented yet")
162 if len(path) != len(path_i18n): 213 if len(path) != len(path_i18n):
163 log.error("inconsistency between menu paths") 214 log.error("inconsistency between menu paths")
164 continue 215 continue
165 callback = PluginMenuCmd(self.host, action_id, menu_data) 216 callback = PluginMenuCmd(self.host, action_id, menu_data)
166 self.addMenu(path[0], path_i18n[0], path_i18n[1], 'plugins', callback) 217 self.addMenuItem(path, path_i18n, 'plugins', callback)
218
219
220 class CategoryMenuBar(GenericMenuBar):
221 """A menu bar for a category (sub-menu)"""
222 def __init__(self, host):
223 GenericMenuBar.__init__(self, host, vertical=True)