1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
|
# menus.py
#
# Most of these are taken from libs/frontends/widgets/menus.py in the miro
# project.
#
# TODO: merge common bits!
import collections
from lvc import signals
from lvc.widgets import widgetutil
from lvc.widgets import widgetset
from lvc.widgets import app
from lvc.widgets.keyboard import (Shortcut, CTRL, ALT, SHIFT, CMD,
MOD, RIGHT_ARROW, LEFT_ARROW, UP_ARROW,
DOWN_ARROW, SPACE, ENTER, DELETE, BKSPACE,
ESCAPE, F1, F2, F3, F4, F5, F6, F7, F8, F9,
F10, F11, F12)
# XXX hack:
def _(text, *params):
if params:
return text % params[0]
return text
class MenuItem(widgetset.MenuItem):
"""Portable MenuItem class.
This adds group handling to the platform menu items.
"""
# group_map is used for the legacy menu updater code
group_map = collections.defaultdict(set)
def __init__(self, label, name, shortcut=None, groups=None,
**state_labels):
widgetset.MenuItem.__init__(self, label, name, shortcut)
# state_labels is used for the legacy menu updater code
self.state_labels = state_labels
if groups:
if len(groups) > 1:
raise ValueError("only support one group")
MenuItem.group_map[groups[0]].add(self)
class MenuItemFetcher(object):
"""Get MenuItems by their name quickly. """
def __init__(self):
self._cache = {}
def __getitem__(self, name):
if name in self._cache:
return self._cache[name]
else:
menu_item = app.widgetapp.menubar.find(name)
self._cache[name] = menu_item
return menu_item
def get_app_menu():
"""Returns the default menu structure."""
app_name = "Libre Video Converter" # XXX HACK
file_menu = widgetset.Menu(_("_File"), "FileMenu",
[MenuItem(_("_Open"), "Open",
Shortcut("o", MOD),
groups=["NonPlaying"]),
MenuItem(_("_Quit"), "Quit",
Shortcut("q", MOD)), ])
help_menu = widgetset.Menu(_("_Help"), "HelpMenu",
[MenuItem(_("About %(name)s",
{'name': app_name}),
"About")])
all_menus = [file_menu, help_menu]
return all_menus
action_handlers = {}
group_action_handlers = {}
def on_menubar_activate(menubar, action_name):
callback = lookup_handler(action_name)
if callback is not None:
callback()
def lookup_handler(action_name):
"""For a given action name, get a callback to handle it. Return
None if no callback is found.
"""
retval = _lookup_group_handler(action_name)
if retval is None:
retval = action_handlers.get(action_name)
return retval
def _lookup_group_handler(action_name):
try:
group_name, callback_arg = action_name.split('-', 1)
except ValueError:
return None # split return tuple of length 1
try:
group_handler = group_action_handlers[group_name]
except KeyError:
return None
else:
return lambda: group_handler(callback_arg)
def action_handler(name):
"""Decorator for functions that handle menu actions."""
def decorator(func):
action_handlers[name] = func
return func
return decorator
def group_action_handler(action_prefix):
def decorator(func):
group_action_handlers[action_prefix] = func
return func
return decorator
# File menu
@action_handler("Open")
def on_open():
app.widgetapp.choose_file()
@action_handler("Quit")
def on_quit():
app.widgetapp.quit()
# Help menu
@action_handler("About")
def on_about():
app.widgetapp.about()
class MenuManager(signals.SignalEmitter):
"""Updates the menu based on the current selection.
This includes enabling/disabling menu items, changing menu text
for plural selection and enabling/disabling the play button. The
play button is obviously not a menu item, but it's pretty closely
related
Whenever code makes a change that could possibly affect which menu
items should be enabled/disabled, it should call the
update_menus() method.
Signals:
- menus-updated(reasons): Emitted whenever update_menus() is called
"""
def __init__(self):
signals.SignalEmitter.__init__(self, 'menus-updated')
self.menu_item_fetcher = MenuItemFetcher()
# self.subtitle_encoding_updater = SubtitleEncodingMenuUpdater()
self.subtitle_encoding_updater = None
def setup_menubar(self, menubar):
"""Setup the main miro menubar.
"""
menubar.add_initial_menus(get_app_menu())
menubar.connect("activate", on_menubar_activate)
self.menu_updaters = []
def _set_play_pause(self):
if ((not app.playback_manager.is_playing or
app.playback_manager.is_paused)):
label = _('Play')
else:
label = _('Pause')
self.menu_item_fetcher['PlayPauseItem'].set_label(label)
def add_subtitle_encoding_menu(self, category_label, *encodings):
"""Set up a subtitles encoding menu.
This method should be called for each category of subtitle encodings
(East Asian, Western European, Unicode, etc). Pass it the list of
encodings for that category.
:param category_label: human-readable name for the category
:param encodings: list of (label, encoding) tuples. label is a
human-readable name, and encoding is a value that we can pass to
VideoDisplay.select_subtitle_encoding()
"""
self.subtitle_encoding_updater.add_menu(category_label, encodings)
def select_subtitle_encoding(self, encoding):
if not self.subtitle_encoding_updater.has_encodings():
# OSX never sets up the subtitle encoding menu
return
menu_item_name = self.subtitle_encoding_updater.action_name(encoding)
try:
self.menu_item_fetcher[menu_item_name].set_state(True)
except KeyError:
logging.warn("Error enabling subtitle encoding menu item: %s",
menu_item_name)
def update_menus(self, *reasons):
"""Call this when a change is made that could change the menus
Use reasons to describe why the menus could change. Some MenuUpdater
objects will do some optimizations based on that
"""
reasons = set(reasons)
self._set_play_pause()
for menu_updater in self.menu_updaters:
menu_updater.update(reasons)
self.emit('menus-updated', reasons)
class MenuUpdater(object):
"""Base class for objects that dynamically update menus."""
def __init__(self, menu_name):
self.menu_name = menu_name
self.first_update = False
# we lazily access our menu item, since we are created before the menubar
# is fully setup.
def get_menu(self):
try:
return self._menu
except AttributeError:
self._menu = app.widgetapp.menubar.find(self.menu_name)
return self._menu
menu = property(get_menu)
def update(self, reasons):
if not self.first_update and not self.should_process_update(reasons):
return
self.first_update = False
self.start_update()
if not self.should_show_menu():
self.menu.hide()
return
self.menu.show()
if self.should_rebuild_menu():
self.clear_menu()
self.populate_menu()
self.update_items()
def should_process_update(self, reasons):
"""Test if we should ignore the update call.
:param reasons: the reasons passed in to MenuManager.update_menus()
"""
return True
def clear_menu(self):
"""Remove items from our menu before rebuilding it."""
for child in self.menu.get_children():
self.menu.remove(child)
def start_update(self):
"""Called at the very start of the update method. """
pass
def should_show_menu(self):
"""Should we display the menu? """
return True
def should_rebuild_menu(self):
"""Should we rebuild the menu structure?"""
return False
def populate_menu(self):
"""Add MenuItems to our menu."""
pass
def update_items(self):
"""Update our menu items."""
pass
|