Allow "" as a valid value in a OptionsBox menu (reported by Guido Schimmels).
[rox-lib.git] / python / rox / Menu.py
blob4e7a1a29e9cb33ead1dae7bf6b8831fb09ad4372
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
9 set_save_name('Edit')
11 menu = Menu('main', [
12 ('/File', '', '<Branch>'),
13 ('/File/Save', 'save', ''),
14 ('/File/Open Parent', 'up', ''),
15 ('/File/Close', 'close', ''),
16 ('/File/', '', '<Separator>'),
17 ('/File/New', 'new', ''),
18 ('/Edit', '', '<Branch>'),
19 ('/Edit/Undo', 'undo', ''),
20 ('/Edit/Redo', 'redo', ''),
21 ('/Edit/', '', '<Separator>'),
22 ('/Edit/Search...', 'search', ''),
23 ('/Edit/Goto line...', 'goto', ''),
24 ('/Edit/', '', '<Separator>'),
25 ('/Edit/Process...', 'process', ''),
26 ('/Options', 'show_options', ''),
27 ('/Help', 'help', '<StockItem>', 'F1', g.STOCK_HELP),
30 There is also a new syntax, supported from 1.9.13, where you pass instances of MenuItem
31 instead of tuples to the Menu constructor. Be sure to require version 1.9.13 if
32 using this feature.
33 """
35 from __future__ import generators
37 import rox
38 from rox import g
39 import choices
41 _save_name = None
42 def set_save_name(prog, leaf = 'menus'):
43 """Set the directory/leafname (see choices) used to save the menu keys.
44 Call this before creating any menus."""
45 global _save_name
46 _save_name = (prog, leaf)
48 class MenuItem:
49 """Base class for menu items. You should normally use one of the subclasses..."""
50 def __init__(self, label, callback_name, type = '', key = None, stock = None):
51 if label and label[0] == '/':
52 self.label = label[1:]
53 else:
54 self.label = label
55 self.fn = callback_name
56 self.type = type
57 self.key = key
58 self.stock = stock
60 def activate(self, caller):
61 getattr(caller, self.fn)()
63 class Action(MenuItem):
64 """A leaf menu item, possibly with a stock icon, which calls a method when clicked."""
65 def __init__(self, label, callback_name, key = None, stock = None, values = ()):
66 """object.callback(*values) is called when the item is activated."""
67 if stock:
68 MenuItem.__init__(self, label, callback_name, '<StockItem>', key, stock)
69 else:
70 MenuItem.__init__(self, label, callback_name, '', key)
71 self.values = values
73 def activate(self, caller):
74 getattr(caller, self.fn)(*self.values)
76 class ToggleItem(MenuItem):
77 """A menu item that has a check icon and toggles state each time it is activated."""
78 def __init__(self, label, property_name):
79 """property_name is a boolean property on the caller object. You can use
80 the built-in Python class property() if you want to perform calculations when
81 getting or setting the value."""
82 MenuItem.__init__(self, label, property_name, '<ToggleItem>')
83 self.updating = False
85 def update(self, menu, widget):
86 """Called when then menu is opened."""
87 self.updating = True
88 state = getattr(menu.caller, self.fn)
89 widget.set_active(state)
90 self.updating = False
92 def activate(self, caller):
93 if not self.updating:
94 setattr(caller, self.fn, not getattr(caller, self.fn))
96 class SubMenu(MenuItem):
97 """A branch menu item leading to a submenu."""
98 def __init__(self, label, submenu):
99 MenuItem.__init__(self, label, None, '<Branch>')
100 self.submenu = submenu
102 class Separator(MenuItem):
103 """A line dividing two parts of the menu."""
104 def __init__(self):
105 MenuItem.__init__(self, '', None, '<Separator>')
107 def _walk(items):
108 for x in items:
109 yield "/" + x.label, x
110 if isinstance(x, SubMenu):
111 for l, y in _walk(x.submenu):
112 yield "/" + x.label + l, y
114 class Menu:
115 """A popup menu. This wraps GtkMenu. It handles setting, loading and saving of
116 keyboard-shortcuts, applies translations, and has a simpler API."""
117 fns = None # List of MenuItem objects which can be activated
118 update_callbacks = None # List of functions to call just before popping up the menu
119 accel_group = None
120 menu = None # The actual GtkMenu
122 def __init__(self, name, items):
123 """names should be unique (eg, 'popup', 'main', etc).
124 items is a list of menu items:
125 [(name, callback_name, type, key), ...].
126 'name' is the item's path.
127 'callback_name' is the NAME of a method to call.
128 'type' is as for g.ItemFactory.
129 'key' is only used if no bindings are in Choices."""
130 if not _save_name:
131 raise Exception('Call rox.Menu.set_save_name() first!')
133 ag = g.AccelGroup()
134 self.accel_group = ag
135 factory = g.ItemFactory(g.Menu, '<%s>' % name, ag)
137 program, save_leaf = _save_name
138 accel_path = choices.load(program, save_leaf)
140 out = []
141 self.fns = []
143 # Convert old-style list of tuples to new classes
144 if items and not isinstance(items[0], MenuItem):
145 items = [MenuItem(*t) for t in items]
147 items_with_update = []
148 for path, item in _walk(items):
149 if item.fn:
150 self.fns.append(item)
151 cb = self._activate
152 else:
153 cb = None
154 if item.stock:
155 out.append((path, item.key, cb, len(self.fns) - 1, item.type, item.stock))
156 else:
157 out.append((path, item.key, cb, len(self.fns) - 1, item.type))
158 if hasattr(item, 'update'):
159 items_with_update.append((path, item))
161 factory.create_items(out)
162 self.factory = factory
164 self.update_callbacks = []
165 for path, item in items_with_update:
166 widget = factory.get_widget(path)
167 fn = item.update
168 self.update_callbacks.append(lambda: fn(self, widget))
170 if accel_path:
171 g.accel_map_load(accel_path)
173 self.caller = None # Caller of currently open menu
174 self.menu = factory.get_widget('<%s>' % name)
176 def keys_changed(*unused):
177 program, name = _save_name
178 path = choices.save(program, name)
179 if path:
180 try:
181 g.accel_map_save(path)
182 except AttributeError:
183 print "Error saving keybindings to", path
184 # GtkAccelGroup has its own (unrelated) connect method,
185 # so the obvious approach doesn't work.
186 #ag.connect('accel_changed', keys_changed)
187 import gobject
188 gobject.GObject.connect(ag, 'accel_changed', keys_changed)
190 def attach(self, window, object):
191 """Keypresses on this window will be treated as menu shortcuts
192 for this object, calling 'object.<callback_name>' when used."""
193 def kev(w, k):
194 self.caller = object
195 return 0
196 window.connect('key-press-event', kev)
197 window.add_accel_group(self.accel_group)
199 def _position(self, menu):
200 x, y, mods = g.gdk.get_default_root_window().get_pointer()
201 width, height = menu.size_request()
202 return (x - width * 3 / 4, y - 16, True)
204 def popup(self, caller, event, position_fn = None):
205 """Display the menu. Call 'caller.<callback_name>' when an item is chosen.
206 For applets, position_fn should be my_applet.position_menu)."""
207 self.caller = caller
208 map(apply, self.update_callbacks) # Update toggles, etc
209 if event:
210 self.menu.popup(None, None, position_fn or self._position, event.button, event.time)
211 else:
212 self.menu.popup(None, None, position_fn or self._position, 0, 0)
214 def _activate(self, action, widget):
215 if self.caller:
216 try:
217 self.fns[action].activate(self.caller)
218 except:
219 rox.report_exception()
220 else:
221 raise Exception("No caller for menu!")