Allow MakeDist to use a specified sf.net account
[rox-lib.git] / python / rox / Menu.py
blob628c8c9197e1702343b4554a0d57763e0994748d
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 os
38 import rox
39 from rox import g
40 import choices, basedir
42 import warnings
43 warnings.filterwarnings('ignore', 'use gtk.UIManager', DeprecationWarning,
44 'rox')
46 _save_name = None
47 def set_save_name(prog, leaf = 'menus', site = None):
48 """Set the directory/leafname (see choices) used to save the menu keys.
49 Call this before creating any menus.
50 If 'site' is given, the basedir module is used for saving bindings (the
51 new system). Otherwise, the deprecated choices module is used."""
52 global _save_name
53 _save_name = (site, prog, leaf)
55 class MenuItem:
56 """Base class for menu items. You should normally use one of the subclasses..."""
57 def __init__(self, label, callback_name, type = '', key = None, stock = None):
58 if label and label[0] == '/':
59 self.label = label[1:]
60 else:
61 self.label = label
62 self.fn = callback_name
63 self.type = type
64 self.key = key
65 self.stock = stock
67 def activate(self, caller):
68 getattr(caller, self.fn)()
70 class Action(MenuItem):
71 """A leaf menu item, possibly with a stock icon, which calls a method when clicked."""
72 def __init__(self, label, callback_name, key = None, stock = None, values = ()):
73 """object.callback(*values) is called when the item is activated."""
74 if stock:
75 MenuItem.__init__(self, label, callback_name, '<StockItem>', key, stock)
76 else:
77 MenuItem.__init__(self, label, callback_name, '', key)
78 self.values = values
80 def activate(self, caller):
81 getattr(caller, self.fn)(*self.values)
83 class ToggleItem(MenuItem):
84 """A menu item that has a check icon and toggles state each time it is activated."""
85 def __init__(self, label, property_name):
86 """property_name is a boolean property on the caller object. You can use
87 the built-in Python class property() if you want to perform calculations when
88 getting or setting the value."""
89 MenuItem.__init__(self, label, property_name, '<ToggleItem>')
90 self.updating = False
92 def update(self, menu, widget):
93 """Called when then menu is opened."""
94 self.updating = True
95 state = getattr(menu.caller, self.fn)
96 widget.set_active(state)
97 self.updating = False
99 def activate(self, caller):
100 if not self.updating:
101 setattr(caller, self.fn, not getattr(caller, self.fn))
103 class SubMenu(MenuItem):
104 """A branch menu item leading to a submenu."""
105 def __init__(self, label, submenu):
106 MenuItem.__init__(self, label, None, '<Branch>')
107 self.submenu = submenu
109 class Separator(MenuItem):
110 """A line dividing two parts of the menu."""
111 def __init__(self):
112 MenuItem.__init__(self, '', None, '<Separator>')
114 def _walk(items):
115 for x in items:
116 yield "/" + x.label, x
117 if isinstance(x, SubMenu):
118 for l, y in _walk(x.submenu):
119 yield "/" + x.label + l, y
121 class Menu:
122 """A popup menu. This wraps GtkMenu. It handles setting, loading and saving of
123 keyboard-shortcuts, applies translations, and has a simpler API."""
124 fns = None # List of MenuItem objects which can be activated
125 update_callbacks = None # List of functions to call just before popping up the menu
126 accel_group = None
127 menu = None # The actual GtkMenu
129 def __init__(self, name, items):
130 """names should be unique (eg, 'popup', 'main', etc).
131 items is a list of menu items:
132 [(name, callback_name, type, key), ...].
133 'name' is the item's path.
134 'callback_name' is the NAME of a method to call.
135 'type' is as for g.ItemFactory.
136 'key' is only used if no bindings are in Choices."""
137 if not _save_name:
138 raise Exception('Call rox.Menu.set_save_name() first!')
140 ag = g.AccelGroup()
141 self.accel_group = ag
142 factory = g.ItemFactory(g.Menu, '<%s>' % name, ag)
144 site, program, save_leaf = _save_name
145 if site:
146 accel_path = basedir.load_first_config(site, program, save_leaf)
147 else:
148 accel_path = choices.load(program, save_leaf)
150 out = []
151 self.fns = []
153 # Convert old-style list of tuples to new classes
154 if items and not isinstance(items[0], MenuItem):
155 items = [MenuItem(*t) for t in items]
157 items_with_update = []
158 for path, item in _walk(items):
159 if item.fn:
160 self.fns.append(item)
161 cb = self._activate
162 else:
163 cb = None
164 if item.stock:
165 out.append((path, item.key, cb, len(self.fns) - 1, item.type, item.stock))
166 else:
167 out.append((path, item.key, cb, len(self.fns) - 1, item.type))
168 if hasattr(item, 'update'):
169 items_with_update.append((path, item))
171 factory.create_items(out)
172 self.factory = factory
174 self.update_callbacks = []
175 for path, item in items_with_update:
176 widget = factory.get_widget(path)
177 fn = item.update
178 self.update_callbacks.append(lambda f = fn, w = widget: f(self, w))
180 if accel_path:
181 g.accel_map_load(accel_path)
183 self.caller = None # Caller of currently open menu
184 self.menu = factory.get_widget('<%s>' % name)
186 def keys_changed(*unused):
187 site, program, name = _save_name
188 if site:
189 d = basedir.save_config_path(site, program)
190 path = os.path.join(d, name)
191 else:
192 path = choices.save(program, name)
193 if path:
194 try:
195 g.accel_map_save(path)
196 except AttributeError:
197 print "Error saving keybindings to", path
198 # GtkAccelGroup has its own (unrelated) connect method,
199 # so the obvious approach doesn't work.
200 #ag.connect('accel_changed', keys_changed)
201 import gobject
202 gobject.GObject.connect(ag, 'accel_changed', keys_changed)
204 def attach(self, window, object):
205 """Keypresses on this window will be treated as menu shortcuts
206 for this object, calling 'object.<callback_name>' when used."""
207 def kev(w, k):
208 self.caller = object
209 return 0
210 window.connect('key-press-event', kev)
211 window.add_accel_group(self.accel_group)
213 def _position(self, menu):
214 x, y, mods = g.gdk.get_default_root_window().get_pointer()
215 width, height = menu.size_request()
216 return (x - width * 3 / 4, y - 16, True)
218 def popup(self, caller, event, position_fn = None):
219 """Display the menu. Call 'caller.<callback_name>' when an item is chosen.
220 For applets, position_fn should be my_applet.position_menu)."""
221 self.caller = caller
222 map(apply, self.update_callbacks) # Update toggles, etc
223 if event:
224 self.menu.popup(None, None, position_fn or self._position, event.button, event.time)
225 else:
226 self.menu.popup(None, None, position_fn or self._position, 0, 0)
228 def _activate(self, action, widget):
229 if self.caller:
230 try:
231 self.fns[action].activate(self.caller)
232 except:
233 rox.report_exception()
234 else:
235 raise Exception("No caller for menu!")