1
# Miro - an RSS based video player application
2
# Copyright (C) 2005-2010 Participatory Culture Foundation
4
# This program is free software; you can redistribute it and/or modify
5
# it under the terms of the GNU General Public License as published by
6
# the Free Software Foundation; either version 2 of the License, or
7
# (at your option) any later version.
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
# GNU General Public License for more details.
14
# You should have received a copy of the GNU General Public License
15
# along with this program; if not, write to the Free Software
16
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
18
# In addition, as a special exception, the copyright holders give
19
# permission to link the code of portions of this program with the OpenSSL
22
# You must obey the GNU General Public License in all respects for all of
23
# the code used other than OpenSSL. If you modify file(s) with this
24
# exception, you may extend this exception to your version of the file(s),
25
# but you are not obligated to do so. If you do not wish to do so, delete
26
# this exception statement from your version. If you delete this exception
27
# statement from all source files in the program, then also delete it here.
29
"""displays.py -- Handle switching the content on the right hand side of the
37
from miro import messages
38
from miro import signals
39
from miro import config
40
from miro import prefs
41
from miro import filetypes
42
from miro.gtcache import gettext as _
43
from miro.gtcache import ngettext
44
from miro.frontends.widgets import browser
45
from miro.frontends.widgets import downloadscontroller
46
from miro.frontends.widgets import videoconversionscontroller
47
from miro.frontends.widgets import feedcontroller
48
from miro.frontends.widgets import itemlistcontroller
49
from miro.frontends.widgets import playlist
50
from miro.frontends.widgets import widgetutil
51
from miro.plat.frontends.widgets import widgetset
53
class Display(signals.SignalEmitter):
54
"""A display is a view that can be shown in the right hand side of the
59
widget -- Widget to show to the user.
63
signals.SignalEmitter.__init__(self)
64
self.create_signal('removed')
66
def on_selected(self):
67
"""Perform any code that needs to be run every time the display is
72
def on_activate(self):
73
"""Perform code that needs to be run when the display becomes the
74
active display (the one on the top of the display stack).
78
def on_deactivate(self):
79
"""Perform code that needs to be run when another display gets pushed
80
on top of this display.
85
"""Cleanup any resources allocated in create. This will be called
86
after the widget for this display is removed.
90
class TabDisplay(Display):
91
"""Display that displays the selection in the tab list."""
93
def __init__(self, tab_type, selected_tabs):
94
raise NotImplementedError()
96
def on_activate(self):
97
app.menu_manager.update_menus()
100
def should_display(tab_type, selected_tabs):
101
"""Test if this display should be shown. """
102
raise NotImplementedError()
104
class DisplayManager(object):
105
"""Handles managing the display in the right-side of miro.
107
DisplayManagers keep a stack of Displays that are currently is use. This
108
is used to allow us to switch to a new display, but still keep the old
109
display's state. For example, when we switch to a video display, we want
110
to keep around the channel display that we switched from and go back to it
111
when the playback is finished.
114
self.display_classes = [
124
VideoConversionsDisplay,
126
MultipleSelectionDisplay,
129
self.display_stack = []
130
self.selected_tab_list = self.selected_tabs = None
131
app.info_updater.connect('sites-removed', SiteDisplay.on_sites_removed)
133
def get_current_display(self):
135
return self.display_stack[-1]
138
current_display = property(get_current_display)
140
def select_display_for_tabs(self, selected_tab_list, selected_tabs):
141
"""Select a display to show in the right-hand side. """
142
if (selected_tab_list is self.selected_tab_list and
143
selected_tabs == self.selected_tabs and
144
len(self.display_stack) > 0 and
145
isinstance(self.display_stack[-1], TabDisplay)):
146
logging.debug('not reselecting')
149
self.selected_tab_list = selected_tab_list
150
self.selected_tabs = selected_tabs
151
tab_type = selected_tab_list.type
153
for klass in self.display_classes:
154
if klass.should_display(tab_type, selected_tabs):
155
self.select_display(klass(tab_type, selected_tabs))
157
raise AssertionError(
158
"Can't find display for %s %s" % (tab_type, selected_tabs))
160
def select_display(self, display):
161
"""Select a display and clear out the current display stack."""
162
self.deselect_all_displays()
163
self.push_display(display)
165
def deselect_all_displays(self):
166
"""Deselect all displays."""
167
for old_display in self.display_stack:
168
self._unselect_display(old_display)
169
self.display_stack = []
171
def push_display(self, display):
172
"""Select a display and push it on top of the display stack"""
173
if len(self.display_stack) > 0:
174
self.current_display.on_deactivate()
175
self.display_stack.append(display)
176
display.on_selected()
177
display.on_activate()
178
app.widgetapp.window.set_main_area(display.widget)
180
def pop_display(self, unselect=True):
181
"""Remove the current display, then select the next one in the display
184
display = self.display_stack.pop()
186
self._unselect_display(display)
187
self.current_display.on_activate()
188
app.widgetapp.window.set_main_area(self.current_display.widget)
190
def _unselect_display(self, display):
191
display.on_deactivate()
193
display.emit("removed")
195
def push_folder_contents_display(self, folder_info):
196
self.push_display(FolderContentsDisplay(folder_info))
198
class GuideDisplay(TabDisplay):
200
def should_display(tab_type, selected_tabs):
201
return tab_type == 'static' and selected_tabs[0].id == 'guide'
203
def __init__(self, tab_type, selected_tabs):
204
Display.__init__(self)
205
self.widget = selected_tabs[0].browser
207
class SiteDisplay(TabDisplay):
208
_open_sites = {} # maps site ids -> BrowserNav objects for them
211
def on_sites_removed(cls, info_updater, id_list):
214
del cls._open_sites[id]
219
def should_display(tab_type, selected_tabs):
220
return tab_type == 'site' and len(selected_tabs) == 1
222
def __init__(self, tab_type, selected_tabs):
223
Display.__init__(self)
224
guide_info = selected_tabs[0]
225
if guide_info.id not in self._open_sites:
226
self._open_sites[guide_info.id] = browser.BrowserNav(guide_info)
227
self.widget = self._open_sites[guide_info.id]
229
class ItemListDisplayMixin(object):
230
def on_selected(self):
231
app.item_list_controller_manager.controller_created(self.controller)
232
self.controller.start_tracking()
235
def on_activate(self):
236
app.item_list_controller_manager.controller_displayed(self.controller)
237
super(ItemListDisplayMixin, self).on_activate()
239
def on_deactivate(self):
240
app.item_list_controller_manager.controller_no_longer_displayed(
244
self.controller.stop_tracking()
245
self.remember_state()
246
app.item_list_controller_manager.controller_destroyed(self.controller)
248
def remember_state(self):
249
if self.controller.widget.in_list_view:
250
app.frontend_states_memory.set_list_view(self.type, self.id)
252
app.frontend_states_memory.set_std_view(self.type, self.id)
254
def restore_state(self):
255
if app.frontend_states_memory.query_list_view(self.type, self.id):
256
self.widget.switch_to_list_view()
258
class ItemListDisplay(ItemListDisplayMixin, TabDisplay):
259
def __init__(self, tab_type, selected_tabs):
260
Display.__init__(self)
261
tab = selected_tabs[0]
262
self.controller = self.make_controller(tab)
263
self.widget = self.controller.widget
267
def make_controller(self, tab):
268
raise NotImplementedError()
270
class FeedDisplay(ItemListDisplay):
272
UPDATER_SIGNAL_NAME = 'feeds-changed'
275
def should_display(cls, tab_type, selected_tabs):
276
return tab_type == cls.TAB_TYPE and len(selected_tabs) == 1
278
def on_selected(self):
279
ItemListDisplay.on_selected(self)
280
self._signal_handler = app.info_updater.connect(
281
self.UPDATER_SIGNAL_NAME, self._on_feeds_changed)
283
def _on_feeds_changed(self, updater, info_list):
284
for info in info_list:
285
if info.id == self.id:
286
self.controller.titlebar.update_title(info.name)
290
ItemListDisplay.cleanup(self)
291
app.info_updater.disconnect(self._signal_handler)
292
if widgetutil.feed_exists(self.feed_id):
293
messages.MarkFeedSeen(self.feed_id).send_to_backend()
295
def make_controller(self, tab):
296
self.feed_id = tab.id
297
return feedcontroller.FeedController(tab.id, tab.is_folder, tab.is_directory_feed)
299
class AudioFeedDisplay(FeedDisplay):
300
TAB_TYPE = 'audio-feed'
301
UPDATER_SIGNAL_NAME = 'audio-feeds-changed'
303
class PlaylistDisplay(ItemListDisplay):
305
def should_display(tab_type, selected_tabs):
306
return tab_type == 'playlist' and len(selected_tabs) == 1
308
def on_selected(self):
309
ItemListDisplay.on_selected(self)
310
self._signal_handler = app.info_updater.connect('playlists-changed',
311
self._on_playlists_changed)
313
def _on_playlists_changed(self, updater, info_list):
314
for info in info_list:
315
if info.id == self.id:
316
self.controller.titlebar.update_title(info.name)
320
ItemListDisplay.cleanup(self)
321
app.info_updater.disconnect(self._signal_handler)
323
def make_controller(self, playlist_info):
324
return playlist.PlaylistView(playlist_info)
326
class SearchDisplay(ItemListDisplay):
328
def should_display(tab_type, selected_tabs):
329
return tab_type == 'static' and selected_tabs[0].id == 'search'
331
def make_controller(self, tab):
332
return itemlistcontroller.SearchController()
334
class AudioVideoItemsDisplay(ItemListDisplay):
335
def remember_state(self):
336
ItemListDisplay.remember_state(self)
337
filters = self.widget.toolbar.active_filters()
338
app.frontend_states_memory.set_filters(self.type, self.id, filters)
340
def restore_state(self):
341
initial_filters = app.frontend_states_memory.query_filters(self.type,
344
self.controller.set_item_filters(initial_filters)
345
ItemListDisplay.restore_state(self)
347
class VideoItemsDisplay(AudioVideoItemsDisplay):
349
def should_display(tab_type, selected_tabs):
350
return tab_type == 'library' and selected_tabs[0].id == 'videos'
352
def make_controller(self, tab):
353
return itemlistcontroller.VideoItemsController()
355
class AudioItemsDisplay(AudioVideoItemsDisplay):
357
def should_display(tab_type, selected_tabs):
358
return tab_type == 'library' and selected_tabs[0].id == 'audios'
360
def make_controller(self, tab):
361
return itemlistcontroller.AudioItemsController()
363
class OtherItemsDisplay(ItemListDisplay):
365
def should_display(tab_type, selected_tabs):
366
return tab_type == 'library' and selected_tabs[0].id == 'others'
368
def make_controller(self, tab):
369
return itemlistcontroller.OtherItemsController()
371
class DownloadingDisplay(ItemListDisplay):
373
def should_display(tab_type, selected_tabs):
374
return tab_type == 'library' and selected_tabs[0].id == 'downloading'
376
def make_controller(self, tab):
377
return downloadscontroller.DownloadsController()
379
class VideoConversionsDisplay(TabDisplay):
381
def should_display(tab_type, selected_tabs):
382
return tab_type == 'library' and selected_tabs[0].id == 'conversions'
384
def __init__(self, tab_type, selected_tabs):
385
Display.__init__(self)
386
self.controller = videoconversionscontroller.VideoConversionsController()
387
self.widget = self.controller.widget
389
class FolderContentsDisplay(ItemListDisplayMixin, Display):
390
def __init__(self, info):
391
self.type = 'folder-contents'
393
self.controller = itemlistcontroller.FolderContentsController(info)
394
self.widget = self.controller.widget
395
Display.__init__(self)
397
class CantPlayWidget(widgetset.SolidBackground):
399
widgetset.SolidBackground.__init__(self, (0, 0, 0))
400
vbox = widgetset.VBox()
401
label = widgetset.Label(_(
402
"%(appname)s can't play this file. You may "
403
"be able to open it with a different program",
404
{"appname": config.get(prefs.SHORT_APP_NAME)}
406
label.set_color((1, 1, 1))
407
vbox.pack_start(label)
408
table = widgetset.Table(2, 2)
409
table.set_column_spacing(6)
410
self.filename_label = self._make_label('')
411
self.filetype_label = self._make_label('')
412
table.pack(widgetutil.align_left(self._make_heading(_('Filename:'))),
414
table.pack(widgetutil.align_left(self.filename_label), 1, 0)
415
table.pack(widgetutil.align_left(self._make_heading(_('File type:'))),
417
table.pack(widgetutil.align_left(self.filetype_label), 1, 1)
418
vbox.pack_start(widgetutil.align_left(table, top_pad=12))
419
hbox = widgetset.HBox(spacing=12)
420
reveal_button = widgetset.Button(_('Reveal File'))
421
self.play_externally_button = widgetset.Button(_('Play Externally'))
422
self.play_externally_button.connect('clicked', self._on_play_externally)
423
skip_button = widgetset.Button(_('Skip'))
424
reveal_button.connect('clicked', self._on_reveal)
425
skip_button.connect('clicked', self._on_skip)
426
hbox.pack_start(reveal_button)
427
hbox.pack_start(self.play_externally_button)
428
hbox.pack_start(skip_button)
429
vbox.pack_start(widgetutil.align_center(hbox, top_pad=24))
430
alignment = widgetset.Alignment(xalign=0.5, yalign=0.5)
434
def _make_label(self, text):
435
label = widgetset.Label(text)
436
label.set_color((1, 1, 1))
439
def _make_heading(self, text):
440
label = self._make_label(text)
444
def _on_reveal(self, button):
445
app.widgetapp.reveal_file(self.video_path)
447
def _on_play_externally(self, button):
448
app.widgetapp.open_file(self.video_path)
450
def _on_skip(self, button):
451
app.playback_manager.play_next_item(False)
453
def set_video_path(self, video_path):
454
self.video_path = video_path
455
self.filename_label.set_text(os.path.split(video_path)[-1])
456
self.filetype_label.set_text(os.path.splitext(video_path)[1])
457
if filetypes.is_playable_filename(video_path):
458
self.play_externally_button.set_text(_('Play Externally'))
460
self.play_externally_button.set_text(_('Open Externally'))
462
class VideoDisplay(Display):
463
def __init__(self, renderer):
464
Display.__init__(self)
465
self.create_signal('cant-play')
466
self.create_signal('ready-to-play')
467
self.renderer = renderer
468
self.widget = widgetset.VBox()
469
self.widget.pack_start(self.renderer, expand=True)
470
self.cant_play_widget = CantPlayWidget()
471
self._showing_renderer = True
472
self.in_fullscreen = False
474
def show_renderer(self):
475
if not self._showing_renderer:
476
self.widget.remove(self.cant_play_widget)
477
self.widget.pack_start(self.renderer, expand=True)
478
self._showing_renderer = True
480
def show_play_external(self):
481
if self._showing_renderer:
482
self._prepare_remove_renderer()
483
self.widget.remove(self.renderer)
484
self.widget.pack_start(self.cant_play_widget, expand=True)
485
self._showing_renderer = False
487
def _open_success(self):
488
self.emit('ready-to-play')
490
def _open_error(self):
491
messages.MarkItemWatched(self.item_info_id).send_to_backend()
492
self.show_play_external()
493
self.emit('cant-play')
495
def setup(self, item_info, volume):
497
self.cant_play_widget.set_video_path(item_info.video_path)
498
self.item_info_id = item_info.id
499
self.renderer.set_item(item_info, self._open_success, self._open_error)
500
self.renderer.set_volume(volume)
502
def enter_fullscreen(self):
503
self.renderer.enter_fullscreen()
504
self.in_fullscreen = True
506
def exit_fullscreen(self):
507
self.renderer.exit_fullscreen()
508
self.in_fullscreen = False
510
def prepare_switch_to_attached_playback(self):
511
self.renderer.prepare_switch_to_attached_playback()
513
def prepare_switch_to_detached_playback(self):
514
self.renderer.prepare_switch_to_detached_playback()
516
def _prepare_remove_renderer(self):
517
if self.in_fullscreen:
518
self.exit_fullscreen()
523
if self._showing_renderer:
524
self._prepare_remove_renderer()
525
self.renderer.teardown()
528
class MultipleSelectionDisplay(TabDisplay):
530
def should_display(tab_type, selected_tabs):
531
return len(selected_tabs) > 1
533
def __init__(self, tab_type, selected_tabs):
534
Display.__init__(self)
536
self.child_count = self.folder_count = self.folder_child_count = 0
537
if tab_type == 'feed':
538
tab_list = app.tab_list_manager.feed_list
539
elif tab_type == 'audio-feed':
540
tab_list = app.tab_list_manager.audio_feed_list
541
elif tab_type == 'site':
542
tab_list = app.tab_list_manager.site_list
544
tab_list = app.tab_list_manager.playlist_list
545
for tab in selected_tabs:
546
if hasattr(tab, "is_folder") and tab.is_folder:
547
self.folder_count += 1
548
self.folder_child_count += tab_list.get_child_count(tab.id)
550
self.child_count += 1
551
vbox = widgetset.VBox(spacing=20)
552
label = self._make_label(tab_type, selected_tabs)
554
label.set_color((0.3, 0.3, 0.3))
555
vbox.pack_start(widgetutil.align_center(label))
556
vbox.pack_start(widgetutil.align_center(
557
self._make_buttons(tab_type)))
558
self.widget = widgetutil.align_middle(vbox)
560
def _make_label(self, tab_type, selected_tabs):
562
# NOTE: we need to use ngettext because some languages have multiple
564
if self.folder_count > 0:
565
if tab_type in ('feed', 'audio-feed'):
566
label_parts.append(ngettext(
567
'%(count)d Feed Folder Selected',
568
'%(count)d Feed Folders Selected',
570
{"count": self.folder_count}))
571
label_parts.append(ngettext(
572
'(contains %(count)d feed)',
573
'(contains %(count)d feeds)',
574
self.folder_child_count,
575
{"count": self.folder_child_count}))
577
label_parts.append(ngettext(
578
'%(count)d Playlist Folder Selected',
579
'%(count)d Playlist Folders Selected',
581
{"count": self.folder_count}))
582
label_parts.append(ngettext(
583
'(contains %(count)d playlist)',
584
'(contains %(count)d playlist)',
585
self.folder_child_count,
586
{"count": self.folder_child_count}))
588
if self.child_count > 0 and self.folder_count > 0:
589
label_parts.append('')
590
if self.child_count > 0:
591
if tab_type in ('feed', 'audio-feed'):
592
label_parts.append(ngettext(
593
'%(count)d Feed Selected',
594
'%(count)d Feeds Selected',
596
{"count": self.child_count}))
597
elif tab_type == "site":
598
label_parts.append(ngettext(
599
'%(count)d Website Selected',
600
'%(count)d Websites Selected',
602
{"count": self.child_count}))
604
label_parts.append(ngettext(
605
'%(count)d Playlist Selected',
606
'%(count)d Playlists Selected',
608
{"count": self.child_count}))
609
return widgetset.Label('\n'.join(label_parts))
611
def _make_buttons(self, tab_type):
612
delete_button = widgetset.Button(_('Delete All'))
613
delete_button.connect('clicked', self._on_delete_clicked)
614
if self.folder_count > 0 or tab_type == "site":
616
create_folder_button = widgetset.Button(_('Put Into a New Folder'))
617
create_folder_button.connect('clicked', self._on_new_folder_clicked)
618
hbox = widgetset.HBox(spacing=12)
619
hbox.pack_start(delete_button)
620
hbox.pack_start(create_folder_button)
623
def _on_delete_clicked(self, button):
624
if self.type in ('feed', 'audio-feed'):
625
app.widgetapp.remove_current_feed()
626
elif self.type == 'site':
627
app.widgetapp.remove_current_site()
629
app.widgetapp.remove_current_playlist()
631
def _on_new_folder_clicked(self, button):
632
if self.type in ('feed', 'audio-feed'):
633
section = {"feed": u"video", "audio-feed": u"audio"}
634
app.widgetapp.add_new_feed_folder(add_selected=True,
635
default_type=self.type)
637
app.widgetapp.add_new_playlist_folder(add_selected=True)
639
class DummyDisplay(TabDisplay):
641
def should_display(tab_type, selected_tabs):
644
def __init__(self, tab_type, selected_tabs):
645
Display.__init__(self)
646
text = '\n'.join(tab.name for tab in selected_tabs)
647
label = widgetset.Label(text)
650
label.set_color((1.0, 0, 0))
651
alignment = widgetset.Alignment(xalign=0.5, yalign=0.0)
653
self.widget = alignment