336 lines
10 KiB
Python
336 lines
10 KiB
Python
#----------------------------------------------------------------------
|
|
#
|
|
# 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("<app>", 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 "<SingleMenuItem %r %r Sh:%s Op:%s En:%s>" % (
|
|
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
|