#---------------------------------------------------------------------- # # Python GUI - Menus - Generic # #---------------------------------------------------------------------- from GUI import Globals from GUI.Properties import Properties, overridable_property #---------------------------------------------------------------------- def search_list_for_key(items, char, shift, option): for i in xrange(len(items)-1, -1, -1): result = items[i]._search_for_key(char, shift, option) if result: return result #---------------------------------------------------------------------- class Menu(Properties): """Pull-down or pop-up menu class. Menu(title, item_descriptors) constructs a menu with the given title and items. Each item_descriptor is of the form "-" for a separator, ("text/key", 'command_name') for a single menu item, or (["text/key", ...], 'command_name') for an indexed item group. An indexed group is a group of items sharing the same command name and distinguished by an integer index. Items can be added to and removed from the group dynamically, to implement e.g. a font menu or windows menu. The "key" part of the item descriptor (which is optional) specifies the keyboard equivalent. It should consist of a single character together with the following optional modifiers: ^ representing the Shift key @ representing the Alt or Option key """ title = overridable_property('title', "Title string appearing in menu bar") special = overridable_property('special', "Menu appears at right end of menu bar") _flat_items = None def __init__(self, title, items, special = False, substitutions = {}, **kwds): self._title = title self._items = [] self._special = special Properties.__init__(self, **kwds) self.extend(items, substitutions) def get_title(self): return self._title def get_special(self): return self._special def item_with_command(self, cmd): for item in self._items: if item._command_name == cmd: return item return None def append(self, item, substitutions = {}): items = self._items item = self._make_item(item, substitutions) if not (items and isinstance(item, MenuSeparator) and isinstance(items[-1], MenuSeparator)): items.append(item) def extend(self, items, substitutions = {}): for item in items: self.append(item, substitutions) def _make_item(self, item, substitutions): if isinstance(item, MenuItem): return item elif item == "-": return _menu_separator else: (text, cmd) = item if isinstance(text, basestring): return SingleMenuItem(text, cmd, substitutions) else: return MenuItemGroup(text, cmd) def _command_and_args_for_item(self, item_num): i = 1 for item in self._items: n = item._num_subitems() if item_num < i + n: return item._command_and_args_for_subitem(item_num - i) i += n return '', () def _update_platform_menu(self): # Called during menu setup after items have been enabled/checked. # Generic implementation rebuilds the whole menu from scratch. # Implementations may override this to be more elegant. self._rebuild_platform_menu() def _rebuild_platform_menu(self): self._clear_platform_menu() for item in self._items: item._add_to_platform_menu(self) def _search_for_key(self, char, shift, option): return search_list_for_key(self._items, char, shift, option) def _get_flat_items(self): flat = self._flat_items if flat is None: flat = [] for item in self._items: item._collect_flat_items(flat) self._flat_items = flat return flat def _get_flat_item(self, i): return self._get_flat_items()[i] #---------------------------------------------------------------------- class MenuItem(Properties): # Internal class representing a menu item, group or separator. # # _command_name string Internal command name def _num_subitems(self): return 1 def _split_text(self, text): # Split menu text into label and key combination. if "/" in text: return text.split("/") else: return text, "" def _name(self): return self._label.replace("", Globals.application_name) def _collect_flat_items(self, result): result.append(self) #---------------------------------------------------------------------- class MenuSeparator(MenuItem): # Internal class representing a menu separator. _command_name = '' def _add_to_platform_menu(self, menu): menu._add_separator_to_platform_menu() def _search_for_key(self, char, shift, option): pass #---------------------------------------------------------------------- class SingleMenuItem(MenuItem): """Class representing a menu item. Properties: enabled boolean checked boolean """ enabled = 0 checked = 0 _key = None _shift = 0 _option = 0 #_index = None def __init__(self, text, cmd, substitutions = {}): label1, keycomb1 = self._split_text(text) label2, keycomb2 = self._split_text(substitutions.get(cmd, "")) self._label = label2 or label1 keycomb = keycomb2 or keycomb1 for c in keycomb: if c == '^': self._shift = 1 elif c == '@': self._option = 1 else: self._key = c.upper() self._command_name = cmd def __str__(self): return "" % ( self._label, self._key, self._shift, self._option, self.enabled) def _add_to_platform_menu(self, menu): menu._add_item_to_platform_menu(self, self._name(), self._command_name) def _command_and_args_for_subitem(self, i): return self._command_name, () def _search_for_key(self, char, shift, option): if self._matches_key(char, shift, option): return self._command_name def _matches_key(self, char, shift, option): return self._key == char and self._shift == shift \ and self._option == option and self.enabled #---------------------------------------------------------------------- class MenuItemGroup(MenuItem): """Class representing a menu item group. Properties: enabled <- boolean Assigning to these changes the corresponding checked <- boolean property of all the group's items. Operators: group[index] -> MenuItem Methods: set_items(["text/key", ...]) Replaces all the items in the group by the specified items. """ enabled = overridable_property('enabled') checked = overridable_property('checked') def __init__(self, text_list, cmd): self.set_items(text_list) self._command_name = cmd def _num_subitems(self): return len(self._items) def _command_and_args_for_subitem(self, i): return self._command_name, (i,) def get_enabled(self): raise AttributeError("'enabled' property of MenuItemGroup is write-only") def set_enabled(self, state): for item in self._items: item.enabled = state def get_checked(self): raise AttributeError("'checked' property of MenuItemGroup is write-only") def set_checked(self, state): for item in self._items: item.checked = state def __getitem__(self, index): return self._items[index] def set_items(self, text_list): self._items = [SingleMenuItem(text, '') for text in text_list] def _add_to_platform_menu(self, menu): #for item in self._items: # item._add_to_platform_menu(menu) cmd = self._command_name for index, item in enumerate(self._items): menu._add_item_to_platform_menu(item, item._name(), cmd, index) def _search_for_key(self, char, shift, option): items = self._items for i in xrange(len(items)-1, -1, -1): if items[i]._matches_key(char, shift, option): return (self._command_name, i) def _collect_flat_items(self, result): for item in self._items: item._collect_flat_items(result) #---------------------------------------------------------------------- _menu_separator = MenuSeparator() _dummy_menu_item = SingleMenuItem("", '') #---------------------------------------------------------------------- class MenuState: """A MenuState object is used to enable/disable and check/uncheck menu items, and to add or remove items of indexed groups, during the menu setup phase of menu command handling. Each single menu item or item group appears as an attribute of the MenuState object, with the command name as the attribute name, allowing operations such as menu_state.copy_cmd.enabled = 1 menu_state.font_cmd[current_font].checked = 1 The command name may also be used as a mapping index. Operators: menu_state.command_name -> MenuItem menu_state['command_name'] -> MenuItem """ def __init__(self, menu_list): mapping = {} for menu in menu_list: for item in menu._items: cmd = item._command_name if cmd: mapping[cmd] = item self._mapping = mapping def __getattr__(self, name): try: return self._mapping[name] except KeyError: if name.startswith("__"): raise AttributeError, name return _dummy_menu_item __getitem__ = __getattr__ def reset(self): """Disable and uncheck all items.""" for item in self._mapping.values(): item.enabled = 0 item.checked = None