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 warnings
.filterwarnings('ignore', 'use gtk.UIManager', DeprecationWarning,
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."""
53 _save_name
= (site
, prog
, leaf
)
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:]
62 self
.fn
= callback_name
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."""
75 MenuItem
.__init
__(self
, label
, callback_name
, '<StockItem>', key
, stock
)
77 MenuItem
.__init
__(self
, label
, callback_name
, '', key
)
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>')
92 def update(self
, menu
, widget
):
93 """Called when then menu is opened."""
95 state
= getattr(menu
.caller
, self
.fn
)
96 widget
.set_active(state
)
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."""
112 MenuItem
.__init
__(self
, '', None, '<Separator>')
116 yield "/" + x
.label
, x
117 if isinstance(x
, SubMenu
):
118 for l
, y
in _walk(x
.submenu
):
119 yield "/" + x
.label
+ l
, y
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
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."""
138 raise Exception('Call rox.Menu.set_save_name() first!')
141 self
.accel_group
= ag
142 factory
= g
.ItemFactory(g
.Menu
, '<%s>' % name
, ag
)
144 site
, program
, save_leaf
= _save_name
146 accel_path
= basedir
.load_first_config(site
, program
, save_leaf
)
148 accel_path
= choices
.load(program
, save_leaf
)
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
):
160 self
.fns
.append(item
)
165 out
.append((path
, item
.key
, cb
, len(self
.fns
) - 1, item
.type, item
.stock
))
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
)
178 self
.update_callbacks
.append(lambda f
= fn
, w
= widget
: f(self
, w
))
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
189 d
= basedir
.save_config_path(site
, program
)
190 path
= os
.path
.join(d
, name
)
192 path
= choices
.save(program
, name
)
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)
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."""
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)."""
222 map(apply, self
.update_callbacks
) # Update toggles, etc
224 self
.menu
.popup(None, None, position_fn
or self
._position
, event
.button
, event
.time
)
226 self
.menu
.popup(None, None, position_fn
or self
._position
, 0, 0)
228 def _activate(self
, action
, widget
):
231 self
.fns
[action
].activate(self
.caller
)
233 rox
.report_exception()
235 raise Exception("No caller for menu!")