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, Action, Separator, SubMenu
12 SubMenu('File', Menu([
13 Action('Save', 'save'),
14 Action('Open Parent', 'up'),
15 Action('Close', 'close'),
19 SubMenu('/Edit', Menu([
20 Action('Undo', 'undo'),
21 Action('Redo', 'redo'),
23 Action('Search...', 'search'),
24 Action('Goto line...', 'goto'),
26 Action('Process...', 'process'),
28 Action('Options', 'show_options', 'F1', stock=g.STOCK_HELP)),
29 Action('Quit', 'quit', stock=g.STOCK_QUIT),
32 There is also an older syntax, where you pass tuples of strings
33 to the Menu constructor. This has not been required since 1.9.13.
36 from __future__
import generators
41 import choices
, basedir
44 warnings
.filterwarnings('ignore', 'use gtk.UIManager', DeprecationWarning,
48 def set_save_name(prog
, leaf
= 'menus', site
= None):
49 """Set the directory/leafname (see choices) used to save the menu keys.
50 Call this before creating any menus.
51 If 'site' is given, the basedir module is used for saving bindings (the
52 new system). Otherwise, the deprecated choices module is used."""
54 _save_name
= (site
, prog
, leaf
)
57 """Base class for menu items. You should normally use one of the subclasses..."""
58 def __init__(self
, label
, callback_name
, type = '', key
= None, stock
= None):
59 if label
and label
[0] == '/':
60 self
.label
= label
[1:]
63 self
.fn
= callback_name
68 def activate(self
, caller
):
69 getattr(caller
, self
.fn
)()
71 class Action(MenuItem
):
72 """A leaf menu item, possibly with a stock icon, which calls a method when clicked."""
73 def __init__(self
, label
, callback_name
, key
= None, stock
= None, values
= ()):
74 """object.callback(*values) is called when the item is activated."""
76 MenuItem
.__init
__(self
, label
, callback_name
, '<StockItem>', key
, stock
)
78 MenuItem
.__init
__(self
, label
, callback_name
, '', key
)
81 def activate(self
, caller
):
82 getattr(caller
, self
.fn
)(*self
.values
)
84 class ToggleItem(MenuItem
):
85 """A menu item that has a check icon and toggles state each time it is activated."""
86 def __init__(self
, label
, property_name
):
87 """property_name is a boolean property on the caller object. You can use
88 the built-in Python class property() if you want to perform calculations when
89 getting or setting the value."""
90 MenuItem
.__init
__(self
, label
, property_name
, '<ToggleItem>')
93 def update(self
, menu
, widget
):
94 """Called when then menu is opened."""
96 state
= getattr(menu
.caller
, self
.fn
)
97 widget
.set_active(state
)
100 def activate(self
, caller
):
101 if not self
.updating
:
102 setattr(caller
, self
.fn
, not getattr(caller
, self
.fn
))
104 class SubMenu(MenuItem
):
105 """A branch menu item leading to a submenu."""
106 def __init__(self
, label
, submenu
):
107 MenuItem
.__init
__(self
, label
, None, '<Branch>')
108 self
.submenu
= submenu
110 class Separator(MenuItem
):
111 """A line dividing two parts of the menu."""
113 MenuItem
.__init
__(self
, '', None, '<Separator>')
117 yield "/" + x
.label
, x
118 if isinstance(x
, SubMenu
):
119 for l
, y
in _walk(x
.submenu
):
120 yield "/" + x
.label
+ l
, y
123 """A popup menu. This wraps GtkMenu. It handles setting, loading and saving of
124 keyboard-shortcuts, applies translations, and has a simpler API."""
125 fns
= None # List of MenuItem objects which can be activated
126 update_callbacks
= None # List of functions to call just before popping up the menu
128 menu
= None # The actual GtkMenu
130 def __init__(self
, name
, items
):
131 """names should be unique (eg, 'popup', 'main', etc).
132 items is a list of menu items:
133 [(name, callback_name, type, key), ...].
134 'name' is the item's path.
135 'callback_name' is the NAME of a method to call.
136 'type' is as for g.ItemFactory.
137 'key' is only used if no bindings are in Choices."""
139 raise Exception('Call rox.Menu.set_save_name() first!')
142 self
.accel_group
= ag
143 factory
= g
.ItemFactory(g
.Menu
, '<%s>' % name
, ag
)
145 site
, program
, save_leaf
= _save_name
147 accel_path
= basedir
.load_first_config(site
, program
, save_leaf
)
149 accel_path
= choices
.load(program
, save_leaf
)
154 # Convert old-style list of tuples to new classes
155 if items
and not isinstance(items
[0], MenuItem
):
156 items
= [MenuItem(*t
) for t
in items
]
158 items_with_update
= []
159 for path
, item
in _walk(items
):
161 self
.fns
.append(item
)
166 out
.append((path
, item
.key
, cb
, len(self
.fns
) - 1, item
.type, item
.stock
))
168 out
.append((path
, item
.key
, cb
, len(self
.fns
) - 1, item
.type))
169 if hasattr(item
, 'update'):
170 items_with_update
.append((path
, item
))
172 factory
.create_items(out
)
173 self
.factory
= factory
175 self
.update_callbacks
= []
176 for path
, item
in items_with_update
:
177 widget
= factory
.get_widget(path
)
179 self
.update_callbacks
.append(lambda f
= fn
, w
= widget
: f(self
, w
))
182 g
.accel_map_load(accel_path
)
184 self
.caller
= None # Caller of currently open menu
185 self
.menu
= factory
.get_widget('<%s>' % name
)
187 def keys_changed(*unused
):
188 site
, program
, name
= _save_name
190 d
= basedir
.save_config_path(site
, program
)
191 path
= os
.path
.join(d
, name
)
193 path
= choices
.save(program
, name
)
196 g
.accel_map_save(path
)
197 except AttributeError:
198 print "Error saving keybindings to", path
199 # GtkAccelGroup has its own (unrelated) connect method,
200 # so the obvious approach doesn't work.
201 #ag.connect('accel_changed', keys_changed)
203 gobject
.GObject
.connect(ag
, 'accel_changed', keys_changed
)
205 def attach(self
, window
, object):
206 """Keypresses on this window will be treated as menu shortcuts
207 for this object, calling 'object.<callback_name>' when used."""
211 window
.connect('key-press-event', kev
)
212 window
.add_accel_group(self
.accel_group
)
214 def _position(self
, menu
):
215 x
, y
, mods
= g
.gdk
.get_default_root_window().get_pointer()
216 width
, height
= menu
.size_request()
217 return (x
- width
* 3 / 4, y
- 16, True)
219 def popup(self
, caller
, event
, position_fn
= None):
220 """Display the menu. Call 'caller.<callback_name>' when an item is chosen.
221 For applets, position_fn should be my_applet.position_menu)."""
223 map(apply, self
.update_callbacks
) # Update toggles, etc
225 self
.menu
.popup(None, None, position_fn
or self
._position
, event
.button
, event
.time
)
227 self
.menu
.popup(None, None, position_fn
or self
._position
, 0, 0)
229 def _activate(self
, action
, widget
):
232 self
.fns
[action
].activate(self
.caller
)
234 rox
.report_exception()
236 raise Exception("No caller for menu!")