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.
7 from rox.Menu import Menu, set_save_name
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
35 from __future__
import generators
40 import choices
, basedir
43 def set_save_name(prog
, leaf
= 'menus', site
= None):
44 """Set the directory/leafname (see choices) used to save the menu keys.
45 Call this before creating any menus.
46 If 'site' is given, the basedir module is used for saving bindings (the
47 new system). Otherwise, the deprecated choices module is used."""
49 _save_name
= (site
, prog
, leaf
)
52 """Base class for menu items. You should normally use one of the subclasses..."""
53 def __init__(self
, label
, callback_name
, type = '', key
= None, stock
= None):
54 if label
and label
[0] == '/':
55 self
.label
= label
[1:]
58 self
.fn
= callback_name
63 def activate(self
, caller
):
64 getattr(caller
, self
.fn
)()
66 class Action(MenuItem
):
67 """A leaf menu item, possibly with a stock icon, which calls a method when clicked."""
68 def __init__(self
, label
, callback_name
, key
= None, stock
= None, values
= ()):
69 """object.callback(*values) is called when the item is activated."""
71 MenuItem
.__init
__(self
, label
, callback_name
, '<StockItem>', key
, stock
)
73 MenuItem
.__init
__(self
, label
, callback_name
, '', key
)
76 def activate(self
, caller
):
77 getattr(caller
, self
.fn
)(*self
.values
)
79 class ToggleItem(MenuItem
):
80 """A menu item that has a check icon and toggles state each time it is activated."""
81 def __init__(self
, label
, property_name
):
82 """property_name is a boolean property on the caller object. You can use
83 the built-in Python class property() if you want to perform calculations when
84 getting or setting the value."""
85 MenuItem
.__init
__(self
, label
, property_name
, '<ToggleItem>')
88 def update(self
, menu
, widget
):
89 """Called when then menu is opened."""
91 state
= getattr(menu
.caller
, self
.fn
)
92 widget
.set_active(state
)
95 def activate(self
, caller
):
97 setattr(caller
, self
.fn
, not getattr(caller
, self
.fn
))
99 class SubMenu(MenuItem
):
100 """A branch menu item leading to a submenu."""
101 def __init__(self
, label
, submenu
):
102 MenuItem
.__init
__(self
, label
, None, '<Branch>')
103 self
.submenu
= submenu
105 class Separator(MenuItem
):
106 """A line dividing two parts of the menu."""
108 MenuItem
.__init
__(self
, '', None, '<Separator>')
112 yield "/" + x
.label
, x
113 if isinstance(x
, SubMenu
):
114 for l
, y
in _walk(x
.submenu
):
115 yield "/" + x
.label
+ l
, y
118 """A popup menu. This wraps GtkMenu. It handles setting, loading and saving of
119 keyboard-shortcuts, applies translations, and has a simpler API."""
120 fns
= None # List of MenuItem objects which can be activated
121 update_callbacks
= None # List of functions to call just before popping up the menu
123 menu
= None # The actual GtkMenu
125 def __init__(self
, name
, items
):
126 """names should be unique (eg, 'popup', 'main', etc).
127 items is a list of menu items:
128 [(name, callback_name, type, key), ...].
129 'name' is the item's path.
130 'callback_name' is the NAME of a method to call.
131 'type' is as for g.ItemFactory.
132 'key' is only used if no bindings are in Choices."""
134 raise Exception('Call rox.Menu.set_save_name() first!')
137 self
.accel_group
= ag
138 factory
= g
.ItemFactory(g
.Menu
, '<%s>' % name
, ag
)
140 site
, program
, save_leaf
= _save_name
142 accel_path
= basedir
.load_first_config(site
, program
, save_leaf
)
144 accel_path
= choices
.load(program
, save_leaf
)
149 # Convert old-style list of tuples to new classes
150 if items
and not isinstance(items
[0], MenuItem
):
151 items
= [MenuItem(*t
) for t
in items
]
153 items_with_update
= []
154 for path
, item
in _walk(items
):
156 self
.fns
.append(item
)
161 out
.append((path
, item
.key
, cb
, len(self
.fns
) - 1, item
.type, item
.stock
))
163 out
.append((path
, item
.key
, cb
, len(self
.fns
) - 1, item
.type))
164 if hasattr(item
, 'update'):
165 items_with_update
.append((path
, item
))
167 factory
.create_items(out
)
168 self
.factory
= factory
170 self
.update_callbacks
= []
171 for path
, item
in items_with_update
:
172 widget
= factory
.get_widget(path
)
174 self
.update_callbacks
.append(lambda f
= fn
, w
= widget
: f(self
, w
))
177 g
.accel_map_load(accel_path
)
179 self
.caller
= None # Caller of currently open menu
180 self
.menu
= factory
.get_widget('<%s>' % name
)
182 def keys_changed(*unused
):
183 site
, program
, name
= _save_name
185 d
= basedir
.save_config_path(site
, program
)
186 path
= os
.path
.join(d
, name
)
188 path
= choices
.save(program
, name
)
191 g
.accel_map_save(path
)
192 except AttributeError:
193 print "Error saving keybindings to", path
194 # GtkAccelGroup has its own (unrelated) connect method,
195 # so the obvious approach doesn't work.
196 #ag.connect('accel_changed', keys_changed)
198 gobject
.GObject
.connect(ag
, 'accel_changed', keys_changed
)
200 def attach(self
, window
, object):
201 """Keypresses on this window will be treated as menu shortcuts
202 for this object, calling 'object.<callback_name>' when used."""
206 window
.connect('key-press-event', kev
)
207 window
.add_accel_group(self
.accel_group
)
209 def _position(self
, menu
):
210 x
, y
, mods
= g
.gdk
.get_default_root_window().get_pointer()
211 width
, height
= menu
.size_request()
212 return (x
- width
* 3 / 4, y
- 16, True)
214 def popup(self
, caller
, event
, position_fn
= None):
215 """Display the menu. Call 'caller.<callback_name>' when an item is chosen.
216 For applets, position_fn should be my_applet.position_menu)."""
218 map(apply, self
.update_callbacks
) # Update toggles, etc
220 self
.menu
.popup(None, None, position_fn
or self
._position
, event
.button
, event
.time
)
222 self
.menu
.popup(None, None, position_fn
or self
._position
, 0, 0)
224 def _activate(self
, action
, widget
):
227 self
.fns
[action
].activate(self
.caller
)
229 rox
.report_exception()
231 raise Exception("No caller for menu!")