Added ToggleItem (based on a patch from Ken Hayber).
[rox-lib.git] / python / rox / Menu.py
blobec77a99136ba378cd024c096ce27bd9bcb79beac
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 function property() if you want to perform calculations when
81 getting or setting the value."""
82 MenuItem.__init__(self, label, property_name, '<ToggleItem>')
83 self.widget = None
84 self.updating = False
86 def update(self, menu, widget):
87 """Called when then menu is opened."""
88 self.updating = True
89 state = getattr(menu.caller, self.fn)
90 widget.set_active(state)
91 self.updating = False
93 def activate(self, caller):
94 if not self.updating:
95 setattr(caller, self.fn, not getattr(caller, self.fn))
97 class SubMenu(MenuItem):
98 """A branch menu item leading to a submenu."""
99 def __init__(self, label, submenu):
100 MenuItem.__init__(self, label, None, '<Branch>')
101 self.submenu = submenu
103 class Separator(MenuItem):
104 """A line dividing two parts of the menu."""
105 def __init__(self):
106 MenuItem.__init__(self, '', None, '<Separator>')
108 def _walk(items):
109 for x in items:
110 yield "/" + x.label, x
111 if isinstance(x, SubMenu):
112 for l, y in _walk(x.submenu):
113 yield "/" + x.label + l, y
115 class Menu:
116 def __init__(self, name, items):
117 """names should be unique (eg, 'popup', 'main', etc).
118 items is a list of menu items:
119 [(name, callback_name, type, key), ...].
120 'name' is the item's path.
121 'callback_name' is the NAME of a method to call.
122 'type' is as for g.ItemFactory.
123 'key' is only used if no bindings are in Choices."""
124 if not _save_name:
125 raise Exception('Call rox.Menu.set_save_name() first!')
127 ag = g.AccelGroup()
128 self.accel_group = ag
129 factory = g.ItemFactory(g.Menu, '<%s>' % name, ag)
131 program, save_leaf = _save_name
132 accel_path = choices.load(program, save_leaf)
134 out = []
135 self.fns = []
137 # Convert old-style list of tuples to new classes
138 if items and not isinstance(items[0], MenuItem):
139 items = [MenuItem(*t) for t in items]
141 items_with_update = []
142 for path, item in _walk(items):
143 if item.fn:
144 self.fns.append(item)
145 cb = self._activate
146 else:
147 cb = None
148 if item.stock:
149 out.append((path, item.key, cb, len(self.fns) - 1, item.type, item.stock))
150 else:
151 out.append((path, item.key, cb, len(self.fns) - 1, item.type))
152 if hasattr(item, 'update'):
153 items_with_update.append((path, item))
155 factory.create_items(out)
156 self.factory = factory
158 self.update_callbacks = []
159 for path, item in items_with_update:
160 widget = factory.get_widget(path)
161 fn = item.update
162 self.update_callbacks.append(lambda: fn(self, widget))
164 if accel_path:
165 g.accel_map_load(accel_path)
167 self.caller = None # Caller of currently open menu
168 self.menu = factory.get_widget('<%s>' % name)
170 def keys_changed(*unused):
171 program, name = _save_name
172 path = choices.save(program, name)
173 if path:
174 try:
175 g.accel_map_save(path)
176 except AttributeError:
177 print "Error saving keybindings to", path
178 # GtkAccelGroup has its own (unrelated) connect method,
179 # so the obvious approach doesn't work.
180 #ag.connect('accel_changed', keys_changed)
181 import gobject
182 gobject.GObject.connect(ag, 'accel_changed', keys_changed)
184 def attach(self, window, object):
185 """Keypresses on this window will be treated as menu shortcuts
186 for this object, calling 'object.<callback_name>' when used."""
187 def kev(w, k):
188 self.caller = object
189 return 0
190 window.connect('key-press-event', kev)
191 window.add_accel_group(self.accel_group)
193 def _position(self, menu):
194 x, y, mods = g.gdk.get_default_root_window().get_pointer()
195 width, height = menu.size_request()
196 return (x - width * 3 / 4, y - 16, True)
198 def popup(self, caller, event, position_fn = None):
199 """Display the menu. Call 'caller.<callback_name>' when an item is chosen.
200 For applets, position_fn should be my_applet.position_menu)."""
201 self.caller = caller
202 map(apply, self.update_callbacks) # Update toggles, etc
203 if event:
204 self.menu.popup(None, None, position_fn or self._position, event.button, event.time)
205 else:
206 self.menu.popup(None, None, position_fn or self._position, 0, 0)
208 def _activate(self, action, widget):
209 if self.caller:
210 try:
211 self.fns[action].activate(self.caller)
212 except:
213 rox.report_exception()
214 else:
215 raise Exception("No caller for menu!")