Moved into a sub-dir, so that a svn checkout has the same structure as
[rox-lib/lack.git] / ROX-Lib2 / python / rox / Menu.py
blobea24dc2a0dac63b79e65b7ae701e494266d86efb
1 """The Menu widget provides an easy way to create menus that allow the user to
2 define keyboard shortcuts, and saves the shortcuts automatically. You only define
3 each Menu once, and attach it to windows as required.
5 Example:
7 from rox.Menu import Menu, set_save_name, Action, Separator, SubMenu
9 set_save_name('Edit')
11 menu = Menu('main', [
12 SubMenu('File', Menu([
13 Action('Save', 'save'),
14 Action('Open Parent', 'up'),
15 Action('Close', 'close'),
16 Separator(),
17 Action('New', 'new')
18 ])),
19 SubMenu('/Edit', Menu([
20 Action('Undo', 'undo'),
21 Action('Redo', 'redo'),
22 Separator(),
23 Action('Search...', 'search'),
24 Action('Goto line...', 'goto'),
25 Separator(),
26 Action('Process...', 'process'),
27 ])),
28 Action('Options', 'show_options'),
29 Action('Quit', 'show_options', 'F1', stock=g.STOCK_HELP),
32 There is also an older syntax, where you pass tuples of strings
33 to the Menu constructor. This has not been required since 1.9.13.
34 """
36 from __future__ import generators
38 import os
39 import rox
40 from rox import g
41 import choices, basedir
43 import warnings
44 warnings.filterwarnings('ignore', 'use gtk.UIManager', DeprecationWarning,
45 'rox')
47 _save_name = None
48 def set_save_name(prog, leaf = 'menus', site = None):
49 """Set the directory/leafname (see choices) used to save the menu keys.
50 Call this before creating any menus.
51 If 'site' is given, the basedir module is used for saving bindings (the
52 new system). Otherwise, the deprecated choices module is used."""
53 global _save_name
54 _save_name = (site, prog, leaf)
56 class MenuItem:
57 """Base class for menu items. You should normally use one of the subclasses..."""
58 def __init__(self, label, callback_name, type = '', key = None, stock = None):
59 if label and label[0] == '/':
60 self.label = label[1:]
61 else:
62 self.label = label
63 self.fn = callback_name
64 self.type = type
65 self.key = key
66 self.stock = stock
68 def activate(self, caller):
69 getattr(caller, self.fn)()
71 class Action(MenuItem):
72 """A leaf menu item, possibly with a stock icon, which calls a method when clicked."""
73 def __init__(self, label, callback_name, key = None, stock = None, values = ()):
74 """object.callback(*values) is called when the item is activated."""
75 if stock:
76 MenuItem.__init__(self, label, callback_name, '<StockItem>', key, stock)
77 else:
78 MenuItem.__init__(self, label, callback_name, '', key)
79 self.values = values
81 def activate(self, caller):
82 getattr(caller, self.fn)(*self.values)
84 class ToggleItem(MenuItem):
85 """A menu item that has a check icon and toggles state each time it is activated."""
86 def __init__(self, label, property_name):
87 """property_name is a boolean property on the caller object. You can use
88 the built-in Python class property() if you want to perform calculations when
89 getting or setting the value."""
90 MenuItem.__init__(self, label, property_name, '<ToggleItem>')
91 self.updating = False
93 def update(self, menu, widget):
94 """Called when then menu is opened."""
95 self.updating = True
96 state = getattr(menu.caller, self.fn)
97 widget.set_active(state)
98 self.updating = False
100 def activate(self, caller):
101 if not self.updating:
102 setattr(caller, self.fn, not getattr(caller, self.fn))
104 class SubMenu(MenuItem):
105 """A branch menu item leading to a submenu."""
106 def __init__(self, label, submenu):
107 MenuItem.__init__(self, label, None, '<Branch>')
108 self.submenu = submenu
110 class Separator(MenuItem):
111 """A line dividing two parts of the menu."""
112 def __init__(self):
113 MenuItem.__init__(self, '', None, '<Separator>')
115 def _walk(items):
116 for x in items:
117 yield "/" + x.label, x
118 if isinstance(x, SubMenu):
119 for l, y in _walk(x.submenu):
120 yield "/" + x.label + l, y
122 class Menu:
123 """A popup menu. This wraps GtkMenu. It handles setting, loading and saving of
124 keyboard-shortcuts, applies translations, and has a simpler API."""
125 fns = None # List of MenuItem objects which can be activated
126 update_callbacks = None # List of functions to call just before popping up the menu
127 accel_group = None
128 menu = None # The actual GtkMenu
130 def __init__(self, name, items):
131 """names should be unique (eg, 'popup', 'main', etc).
132 items is a list of menu items:
133 [(name, callback_name, type, key), ...].
134 'name' is the item's path.
135 'callback_name' is the NAME of a method to call.
136 'type' is as for g.ItemFactory.
137 'key' is only used if no bindings are in Choices."""
138 if not _save_name:
139 raise Exception('Call rox.Menu.set_save_name() first!')
141 ag = g.AccelGroup()
142 self.accel_group = ag
143 factory = g.ItemFactory(g.Menu, '<%s>' % name, ag)
145 site, program, save_leaf = _save_name
146 if site:
147 accel_path = basedir.load_first_config(site, program, save_leaf)
148 else:
149 accel_path = choices.load(program, save_leaf)
151 out = []
152 self.fns = []
154 # Convert old-style list of tuples to new classes
155 if items and not isinstance(items[0], MenuItem):
156 items = [MenuItem(*t) for t in items]
158 items_with_update = []
159 for path, item in _walk(items):
160 if item.fn:
161 self.fns.append(item)
162 cb = self._activate
163 else:
164 cb = None
165 if item.stock:
166 out.append((path, item.key, cb, len(self.fns) - 1, item.type, item.stock))
167 else:
168 out.append((path, item.key, cb, len(self.fns) - 1, item.type))
169 if hasattr(item, 'update'):
170 items_with_update.append((path, item))
172 factory.create_items(out)
173 self.factory = factory
175 self.update_callbacks = []
176 for path, item in items_with_update:
177 widget = factory.get_widget(path)
178 fn = item.update
179 self.update_callbacks.append(lambda f = fn, w = widget: f(self, w))
181 if accel_path:
182 g.accel_map_load(accel_path)
184 self.caller = None # Caller of currently open menu
185 self.menu = factory.get_widget('<%s>' % name)
187 def keys_changed(*unused):
188 site, program, name = _save_name
189 if site:
190 d = basedir.save_config_path(site, program)
191 path = os.path.join(d, name)
192 else:
193 path = choices.save(program, name)
194 if path:
195 try:
196 g.accel_map_save(path)
197 except AttributeError:
198 print "Error saving keybindings to", path
199 # GtkAccelGroup has its own (unrelated) connect method,
200 # so the obvious approach doesn't work.
201 #ag.connect('accel_changed', keys_changed)
202 import gobject
203 gobject.GObject.connect(ag, 'accel_changed', keys_changed)
205 def attach(self, window, object):
206 """Keypresses on this window will be treated as menu shortcuts
207 for this object, calling 'object.<callback_name>' when used."""
208 def kev(w, k):
209 self.caller = object
210 return 0
211 window.connect('key-press-event', kev)
212 window.add_accel_group(self.accel_group)
214 def _position(self, menu):
215 x, y, mods = g.gdk.get_default_root_window().get_pointer()
216 width, height = menu.size_request()
217 return (x - width * 3 / 4, y - 16, True)
219 def popup(self, caller, event, position_fn = None):
220 """Display the menu. Call 'caller.<callback_name>' when an item is chosen.
221 For applets, position_fn should be my_applet.position_menu)."""
222 self.caller = caller
223 map(apply, self.update_callbacks) # Update toggles, etc
224 if event:
225 self.menu.popup(None, None, position_fn or self._position, event.button, event.time)
226 else:
227 self.menu.popup(None, None, position_fn or self._position, 0, 0)
229 def _activate(self, action, widget):
230 if self.caller:
231 try:
232 self.fns[action].activate(self.caller)
233 except:
234 rox.report_exception()
235 else:
236 raise Exception("No caller for menu!")