Remove pickle converter
[panucci.git] / src / panucci / main.py
blob088191f36eb087ac56944b56710161acf95138cd
1 #!/usr/bin/env python
3 # This file is part of Panucci.
4 # Copyright (c) 2008-2010 The Panucci Audiobook and Podcast Player Project
6 # Panucci is free software: you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation, either version 3 of the License, or
9 # (at your option) any later version.
11 # Panucci is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
16 # You should have received a copy of the GNU General Public License
17 # along with Panucci. If not, see <http://www.gnu.org/licenses/>.
19 # Based on http://thpinfo.com/2008/panucci/:
20 # A resuming media player for Podcasts and Audiobooks
21 # Copyright (c) 2008-05-26 Thomas Perl <thpinfo.com>
22 # (based on http://pygstdocs.berlios.de/pygst-tutorial/seeking.html)
25 from __future__ import absolute_import
28 import logging
29 import sys
30 import os, os.path
31 import time
33 import gtk
34 import gobject
35 import pango
37 import panucci
39 from panucci import widgets
40 from panucci import util
42 log = logging.getLogger('panucci.panucci')
45 try:
46 import hildon
47 except:
48 if util.platform.MAEMO:
49 log.critical( 'Using GTK widgets, install "python2.5-hildon" '
50 'for this to work properly.' )
52 from panucci.simplegconf import gconf
53 from panucci.settings import settings
54 from panucci.player import player
55 from panucci.dbusinterface import interface
56 from panucci.services import ObservableService
58 about_name = 'Panucci'
59 about_text = _('Resuming audiobook and podcast player')
60 about_authors = ['Thomas Perl', 'Nick (nikosapi)', 'Matthew Taylor']
61 about_website = 'http://panucci.garage.maemo.org/'
63 coverart_sizes = {
64 'normal' : 110,
65 'maemo' : 200,
66 'maemo fullscreen' : 275,
69 gtk.about_dialog_set_url_hook(util.open_link, None)
70 gtk.icon_size_register('panucci-button', 32, 32)
72 def generate_image(filename, is_stock=False):
73 image = None
74 if is_stock:
75 image = gtk.image_new_from_stock(
76 filename, gtk.icon_size_from_name('panucci-button') )
77 else:
78 filename = util.find_image(filename)
79 if filename is not None:
80 image = gtk.image_new_from_file(filename)
81 if image is not None:
82 if util.platform.MAEMO:
83 image.set_padding(20, 20)
84 else:
85 image.set_padding(5, 5)
86 image.show()
87 return image
89 def image(widget, filename, is_stock=False):
90 child = widget.get_child()
91 if child is not None:
92 widget.remove(child)
93 image = generate_image(filename, is_stock)
94 if image is not None:
95 widget.add(image)
97 def dialog( toplevel_window, title, question, description,
98 affirmative_button=gtk.STOCK_YES, negative_button=gtk.STOCK_NO,
99 abortion_button=gtk.STOCK_CANCEL ):
101 """Present the user with a yes/no/cancel dialog.
102 The return value is either True, False or None, depending on which
103 button has been pressed in the dialog:
105 affirmative button (default: Yes) => True
106 negative button (defaut: No) => False
107 abortion button (default: Cancel) => None
109 When the dialog is closed with the "X" button in the window manager
110 decoration, the return value is always None (same as abortion button).
112 You can set any of the affirmative_button, negative_button or
113 abortion_button values to "None" to hide the corresponding action.
115 dlg = gtk.MessageDialog( toplevel_window, gtk.DIALOG_MODAL,
116 gtk.MESSAGE_QUESTION, message_format=question )
118 dlg.set_title(title)
120 if abortion_button is not None:
121 dlg.add_button(abortion_button, gtk.RESPONSE_CANCEL)
122 if negative_button is not None:
123 dlg.add_button(negative_button, gtk.RESPONSE_NO)
124 if affirmative_button is not None:
125 dlg.add_button(affirmative_button, gtk.RESPONSE_YES)
127 dlg.format_secondary_text(description)
129 response = dlg.run()
130 dlg.destroy()
132 if response == gtk.RESPONSE_YES:
133 return True
134 elif response == gtk.RESPONSE_NO:
135 return False
136 elif response in [gtk.RESPONSE_CANCEL, gtk.RESPONSE_DELETE_EVENT]:
137 return None
139 def get_file_from_filechooser(
140 toplevel_window, folder=False, save_file=False, save_to=None):
142 if folder:
143 open_action = gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER
144 else:
145 open_action = gtk.FILE_CHOOSER_ACTION_OPEN
147 if util.platform.MAEMO:
148 if save_file:
149 args = ( toplevel_window, gtk.FILE_CHOOSER_ACTION_SAVE )
150 else:
151 args = ( toplevel_window, open_action )
153 dlg = hildon.FileChooserDialog( *args )
154 else:
155 if save_file:
156 args = ( _('Select file to save playlist to'), None,
157 gtk.FILE_CHOOSER_ACTION_SAVE,
158 (( gtk.STOCK_CANCEL, gtk.RESPONSE_REJECT,
159 gtk.STOCK_SAVE, gtk.RESPONSE_OK )) )
160 else:
161 args = ( _('Select podcast or audiobook'), None, open_action,
162 (( gtk.STOCK_CANCEL, gtk.RESPONSE_REJECT,
163 gtk.STOCK_OPEN, gtk.RESPONSE_OK )) )
165 dlg = gtk.FileChooserDialog(*args)
167 current_folder = os.path.expanduser(settings.last_folder)
169 if current_folder is not None and os.path.isdir(current_folder):
170 dlg.set_current_folder(current_folder)
172 if save_file and save_to is not None:
173 dlg.set_current_name(save_to)
175 if dlg.run() == gtk.RESPONSE_OK:
176 filename = dlg.get_filename()
177 settings.last_folder = dlg.get_current_folder()
178 else:
179 filename = None
181 dlg.destroy()
182 return filename
184 def set_stock_button_text( button, text ):
185 alignment = button.get_child()
186 hbox = alignment.get_child()
187 image, label = hbox.get_children()
188 label.set_text(text)
190 ##################################################
191 # PanucciGUI
192 ##################################################
193 class PanucciGUI(object):
194 """ The object that holds the entire panucci gui """
196 def __init__(self, filename=None):
197 self.__log = logging.getLogger('panucci.panucci.PanucciGUI')
198 interface.register_gui(self)
200 # Build the base ui (window and menubar)
201 if util.platform.MAEMO:
202 self.app = hildon.Program()
203 window = hildon.Window()
204 self.app.add_window(window)
205 else:
206 window = gtk.Window(gtk.WINDOW_TOPLEVEL)
208 self.main_window = window
209 window.set_title('Panucci')
210 self.window_icon = util.find_image('panucci.png')
211 if self.window_icon is not None:
212 window.set_icon_from_file( self.window_icon )
213 window.set_default_size(400, -1)
214 window.set_border_width(0)
215 window.connect("destroy", self.destroy)
217 # Add the tabs (they are private to prevent us from trying to do
218 # something like gui_root.player_tab.some_function() from inside
219 # playlist_tab or vice-versa)
220 self.__player_tab = PlayerTab(self)
221 self.__playlist_tab = PlaylistTab(self)
223 self.playlist_window = gtk.Window(gtk.WINDOW_TOPLEVEL)
224 self.playlist_window.connect('delete-event', gtk.Widget.hide_on_delete)
225 self.playlist_window.set_title(_('Panucci playlist'))
226 self.playlist_window.set_transient_for(self.main_window)
227 self.playlist_window.add(self.__playlist_tab)
229 self.create_actions()
231 if util.platform.MAEMO:
232 window.set_menu(self.create_menu())
233 window.add(self.__player_tab)
234 else:
235 menu_vbox = gtk.VBox()
236 menu_vbox.set_spacing(0)
237 window.add(menu_vbox)
238 menu_bar = gtk.MenuBar()
239 self.create_desktop_menu(menu_bar)
240 menu_vbox.pack_start(menu_bar, False, False, 0)
241 menu_bar.show()
242 menu_vbox.pack_end(self.__player_tab, True, True, 6)
244 # Tie it all together!
245 self.__ignore_queue_check = False
246 self.__window_fullscreen = False
248 if util.platform.MAEMO and interface.headset_device is not None:
249 # Enable play/pause with headset button
250 interface.headset_device.connect_to_signal(
251 'Condition', self.handle_headset_button )
253 self.main_window.connect('key-press-event', self.on_key_press)
254 player.playlist.register( 'file_queued', self.on_file_queued )
256 self.__anti_blank_timer = None
257 settings.register('allow_blanking_changed',self.__set_anti_blank_timer)
258 self.__set_anti_blank_timer( settings.allow_blanking )
260 player.playlist.register( 'playlist-to-be-overwritten',
261 self.check_queue )
262 self.__player_tab.register( 'select-current-item-request',
263 self.__select_current_item )
265 self.main_window.show_all()
267 # this should be done when the gui is ready
268 player.init(filepath=filename)
270 def create_actions(self):
271 self.action_open = gtk.Action('open', _('Open'), _('Open a file or playlist'), gtk.STOCK_OPEN)
272 self.action_open.connect('activate', self.open_file_callback)
273 self.action_save = gtk.Action('save', _('Save playlist'), _('Save current playlist to file'), gtk.STOCK_SAVE_AS)
274 self.action_save.connect('activate', self.save_to_playlist_callback)
275 self.action_playlist = gtk.Action('playlist', _('Playlist'), _('Open the current playlist'), None)
276 self.action_playlist.connect('activate', lambda a: self.playlist_window.show())
277 self.action_about = gtk.Action('about', _('About Panucci'), _('Show application version'), gtk.STOCK_ABOUT)
278 self.action_about.connect('activate', self.about_callback)
279 self.action_quit = gtk.Action('quit', _('Quit'), _('Close Panucci'), gtk.STOCK_QUIT)
280 self.action_quit.connect('activate', self.destroy)
282 def create_desktop_menu(self, menu_bar):
283 file_menu_item = gtk.MenuItem(_('File'))
284 file_menu = gtk.Menu()
285 file_menu.append(self.action_open.create_menu_item())
286 file_menu.append(self.action_save.create_menu_item())
287 file_menu.append(gtk.SeparatorMenuItem())
288 file_menu.append(self.action_quit.create_menu_item())
289 file_menu_item.set_submenu(file_menu)
290 menu_bar.append(file_menu_item)
292 tools_menu_item = gtk.MenuItem(_('Tools'))
293 tools_menu = gtk.Menu()
294 tools_menu.append(self.action_playlist.create_menu_item())
295 tools_menu_item.set_submenu(tools_menu)
296 menu_bar.append(tools_menu_item)
298 help_menu_item = gtk.MenuItem(_('Help'))
299 help_menu = gtk.Menu()
300 help_menu.append(self.action_about.create_menu_item())
301 help_menu_item.set_submenu(help_menu)
302 menu_bar.append(help_menu_item)
304 def create_menu(self):
305 # the main menu
306 menu = gtk.Menu()
308 menu_open = gtk.ImageMenuItem(_('Open playlist'))
309 menu_open.set_image(
310 gtk.image_new_from_stock(gtk.STOCK_OPEN, gtk.ICON_SIZE_MENU))
311 menu_open.connect("activate", self.open_file_callback)
312 menu.append(menu_open)
314 # the recent files menu
315 self.menu_recent = gtk.MenuItem(_('Open recent playlist'))
316 menu.append(self.menu_recent)
317 self.create_recent_files_menu()
319 menu.append(gtk.SeparatorMenuItem())
321 menu_save = gtk.ImageMenuItem(_('Save current playlist'))
322 menu_save.set_image(
323 gtk.image_new_from_stock(gtk.STOCK_SAVE_AS, gtk.ICON_SIZE_MENU))
324 menu_save.connect("activate", self.save_to_playlist_callback)
325 menu.append(menu_save)
327 menu.append(gtk.SeparatorMenuItem())
329 # the settings sub-menu
330 menu_settings = gtk.MenuItem(_('Settings'))
331 menu.append(menu_settings)
333 menu_settings_sub = gtk.Menu()
334 menu_settings.set_submenu(menu_settings_sub)
336 menu_settings_enable_dual_action = gtk.CheckMenuItem(
337 _('Enable dual-action buttons') )
338 settings.attach_checkbutton( menu_settings_enable_dual_action,
339 'enable_dual_action_btn' )
340 menu_settings_sub.append(menu_settings_enable_dual_action)
342 if util.platform.MAEMO:
343 menu_settings_enable_hw_decoding = gtk.CheckMenuItem(
344 _('Enable hardware decoding') )
345 settings.attach_checkbutton( menu_settings_enable_hw_decoding,
346 'enable_hardware_decoding' )
347 menu_settings_sub.append(menu_settings_enable_hw_decoding)
349 menu_settings_lock_progress = gtk.CheckMenuItem(_('Lock Progress Bar'))
350 settings.attach_checkbutton( menu_settings_lock_progress,
351 'progress_locked' )
352 menu_settings_sub.append(menu_settings_lock_progress)
354 menu_about = gtk.ImageMenuItem(gtk.STOCK_ABOUT)
355 menu_about.connect("activate", self.about_callback)
356 menu.append(menu_about)
358 menu.append(gtk.SeparatorMenuItem())
360 menu_quit = gtk.ImageMenuItem(gtk.STOCK_QUIT)
361 menu_quit.connect("activate", self.destroy)
362 menu.append(menu_quit)
364 return menu
366 def create_recent_files_menu( self ):
367 max_files = settings.max_recent_files
368 self.recent_files = player.playlist.get_recent_files(max_files)
369 menu_recent_sub = gtk.Menu()
371 temp_playlist = os.path.expanduser(settings.temp_playlist)
373 if len(self.recent_files) > 0:
374 for f in self.recent_files:
375 # don't include the temporary playlist in the file list
376 if f == temp_playlist: continue
377 # don't include non-existant files
378 if not os.path.exists( f ): continue
379 filename, extension = os.path.splitext(os.path.basename(f))
380 menu_item = gtk.MenuItem( filename.replace('_', ' '))
381 menu_item.connect('activate', self.on_recent_file_activate, f)
382 menu_recent_sub.append(menu_item)
383 else:
384 menu_item = gtk.MenuItem(_('No recent files available.'))
385 menu_item.set_sensitive(False)
386 menu_recent_sub.append(menu_item)
388 self.menu_recent.set_submenu(menu_recent_sub)
390 def destroy(self, widget):
391 player.quit()
392 gtk.main_quit()
394 def show_main_window(self):
395 self.main_window.present()
397 def check_queue(self):
398 """ Makes sure the queue is saved if it has been modified
399 True means a new file can be opened
400 False means the user does not want to continue """
402 if not self.__ignore_queue_check and player.playlist.queue_modified:
403 response = dialog(
404 self.main_window, _('Save current playlist'),
405 _('Current playlist has been modified'),
406 _('Opening a new file will replace the current playlist. ') +
407 _('Do you want to save it before creating a new one?'),
408 affirmative_button=gtk.STOCK_SAVE,
409 negative_button=_('Discard changes'))
411 self.__log.debug('Response to "Save Queue?": %s', response)
413 if response is None:
414 return False
415 elif response:
416 return self.save_to_playlist_callback()
417 elif not response:
418 return True
419 else:
420 return False
421 else:
422 return True
424 def open_file_callback(self, widget=None):
425 if self.check_queue():
426 # set __ingnore__queue_check because we already did the check
427 self.__ignore_queue_check = True
428 filename = get_file_from_filechooser(self.main_window)
429 if filename is not None:
430 self._play_file(filename)
432 self.__ignore_queue_check = False
434 def save_to_playlist_callback(self, widget=None):
435 filename = get_file_from_filechooser(
436 self.main_window, save_file=True, save_to='playlist.m3u' )
438 if filename is None:
439 return False
441 if os.path.isfile(filename):
442 response = dialog( self.main_window, _('File already exists'),
443 _('File already exists'),
444 _('The file %s already exists. You can choose another name or '
445 'overwrite the existing file.') % os.path.basename(filename),
446 affirmative_button=gtk.STOCK_SAVE,
447 negative_button=_('Rename file'))
449 if response is None:
450 return None
452 elif response:
453 pass
454 elif not response:
455 return self.save_to_playlist_callback()
457 ext = util.detect_filetype(filename)
458 if not player.playlist.save_to_new_playlist(filename, ext):
459 util.notify(_('Error saving playlist...'))
460 return False
462 return True
464 def __get_fullscreen(self):
465 return self.__window_fullscreen
467 def __set_fullscreen(self, value):
468 if value != self.__window_fullscreen:
469 if value:
470 self.main_window.fullscreen()
471 else:
472 self.main_window.unfullscreen()
474 self.__window_fullscreen = value
475 player.playlist.send_metadata()
477 fullscreen = property( __get_fullscreen, __set_fullscreen )
479 def on_key_press(self, widget, event):
480 if util.platform.MAEMO:
481 if event.keyval == gtk.keysyms.F6:
482 self.fullscreen = not self.fullscreen
484 def on_recent_file_activate(self, widget, filepath):
485 self._play_file(filepath)
487 def on_file_queued(self, filepath, success, notify):
488 if notify:
489 filename = os.path.basename(filepath)
490 if success:
491 self.__log.info(
492 util.notify( '%s added successfully.' % filename ))
493 else:
494 self.__log.error(
495 util.notify( 'Error adding %s to the queue.' % filename))
497 def about_callback(self, widget):
498 dialog = gtk.AboutDialog()
499 dialog.set_website(about_website)
500 dialog.set_website_label(about_website)
501 dialog.set_name(about_name)
502 dialog.set_authors(about_authors)
503 dialog.set_comments(about_text)
504 dialog.set_version(panucci.__version__)
505 dialog.run()
506 dialog.destroy()
508 def _play_file(self, filename, pause_on_load=False):
509 player.playlist.load( os.path.abspath(filename) )
511 if player.playlist.is_empty:
512 return False
514 def handle_headset_button(self, event, button):
515 if event == 'ButtonPressed' and button == 'phone':
516 player.play_pause_toggle()
518 def __set_anti_blank_timer(self, allow_blanking):
519 if util.platform.MAEMO:
520 if allow_blanking and self.__anti_blank_timer is not None:
521 self.__log.info('Screen blanking enabled.')
522 gobject.source_remove(self.__anti_blank_timer)
523 self.__anti_blank_timer = None
524 elif not allow_blanking and self.__anti_blank_timer is None:
525 self.__log.info('Attempting to disable screen blanking.')
526 self.__anti_blank_timer = gobject.timeout_add(
527 1000 * 59, util.poke_backlight )
528 else:
529 self.__log.info('Blanking controls are for Maemo only.')
531 def __select_current_item( self ):
532 # Select the currently playing track in the playlist tab
533 # and switch to it (so we can edit bookmarks, etc.. there)
534 self.__playlist_tab.select_current_item()
535 self.playlist_window.show()
537 ##################################################
538 # PlayerTab
539 ##################################################
540 class PlayerTab(ObservableService, gtk.HBox):
541 """ The tab that holds the player elements """
543 signals = [ 'select-current-item-request', ]
545 def __init__(self, gui_root):
546 self.__log = logging.getLogger('panucci.panucci.PlayerTab')
547 self.__gui_root = gui_root
549 gtk.HBox.__init__(self)
550 ObservableService.__init__(self, self.signals, self.__log)
552 # Timers
553 self.progress_timer_id = None
554 self.volume_timer_id = None
556 self.recent_files = []
557 self.make_player_tab()
558 self.has_coverart = False
559 self.set_volume(settings.volume)
561 settings.register( 'enable_dual_action_btn_changed',
562 self.on_dual_action_setting_changed )
563 settings.register( 'dual_action_button_delay_changed',
564 self.on_dual_action_setting_changed )
565 settings.register( 'volume_changed', self.set_volume )
566 settings.register( 'scrolling_labels_changed', lambda v:
567 setattr( self.title_label, 'scrolling', v ) )
569 player.register( 'stopped', self.on_player_stopped )
570 player.register( 'playing', self.on_player_playing )
571 player.register( 'paused', self.on_player_paused )
572 player.playlist.register( 'end-of-playlist',
573 self.on_player_end_of_playlist )
574 player.playlist.register( 'new-track-loaded',
575 self.on_player_new_track )
576 player.playlist.register( 'new-metadata-available',
577 self.on_player_new_metadata )
579 def make_player_tab(self):
580 main_vbox = gtk.VBox()
581 main_vbox.set_spacing(6)
582 # add a vbox to self
583 self.pack_start(main_vbox, True, True)
585 # a hbox to hold the cover art and metadata vbox
586 metadata_hbox = gtk.HBox()
587 metadata_hbox.set_spacing(6)
588 main_vbox.pack_start(metadata_hbox, True, False)
590 self.cover_art = gtk.Image()
591 metadata_hbox.pack_start( self.cover_art, False, False )
593 # vbox to hold metadata
594 metadata_vbox = gtk.VBox()
595 metadata_vbox.set_spacing(8)
596 empty_label = gtk.Label()
597 metadata_vbox.pack_start(empty_label, True, True)
598 self.artist_label = gtk.Label('')
599 self.artist_label.set_ellipsize(pango.ELLIPSIZE_END)
600 metadata_vbox.pack_start(self.artist_label, False, False)
601 self.album_label = gtk.Label('')
602 self.album_label.set_ellipsize(pango.ELLIPSIZE_END)
603 metadata_vbox.pack_start(self.album_label, False, False)
604 self.title_label = widgets.ScrollingLabel( '',
605 update_interval=200,
606 pixel_jump=5,
607 delay_btwn_scrolls=5000,
608 delay_halfway=3000 )
609 self.title_label.scrolling = settings.scrolling_labels
610 metadata_vbox.pack_start(self.title_label, False, False)
611 empty_label = gtk.Label()
612 metadata_vbox.pack_start(empty_label, True, True)
613 metadata_hbox.pack_start( metadata_vbox, True, True )
615 progress_eventbox = gtk.EventBox()
616 progress_eventbox.set_events(gtk.gdk.BUTTON_PRESS_MASK)
617 progress_eventbox.connect(
618 'button-press-event', self.on_progressbar_changed )
619 self.progress = gtk.ProgressBar()
620 # make the progress bar more "finger-friendly"
621 if util.platform.MAEMO5:
622 self.progress.set_size_request(-1, 100)
623 elif util.platform.MAEMO:
624 self.progress.set_size_request(-1, 50)
625 progress_eventbox.add(self.progress)
626 main_vbox.pack_start( progress_eventbox, False, False )
628 # make the button box
629 buttonbox = gtk.HBox()
631 # A wrapper to help create DualActionButtons with the right settings
632 create_da = lambda a, b, c=None, d=None: widgets.DualActionButton(
633 a, b, c, d, settings.dual_action_button_delay,
634 settings.enable_dual_action_btn )
636 self.rrewind_button = create_da(
637 generate_image('media-skip-backward.png'),
638 lambda: self.do_seek(-1*settings.seek_long),
639 generate_image(gtk.STOCK_GOTO_FIRST, True),
640 player.playlist.prev)
641 buttonbox.add(self.rrewind_button)
643 self.rewind_button = create_da(
644 generate_image('media-seek-backward.png'),
645 lambda: self.do_seek(-1*settings.seek_short))
646 buttonbox.add(self.rewind_button)
648 self.play_pause_button = gtk.Button('')
649 image(self.play_pause_button, 'media-playback-start.png')
650 self.play_pause_button.connect( 'clicked',
651 self.on_btn_play_pause_clicked )
652 self.play_pause_button.set_sensitive(False)
653 buttonbox.add(self.play_pause_button)
655 self.forward_button = create_da(
656 generate_image('media-seek-forward.png'),
657 lambda: self.do_seek(settings.seek_short))
658 buttonbox.add(self.forward_button)
660 self.fforward_button = create_da(
661 generate_image('media-skip-forward.png'),
662 lambda: self.do_seek(settings.seek_long),
663 generate_image(gtk.STOCK_GOTO_LAST, True),
664 player.playlist.next)
665 buttonbox.add(self.fforward_button)
667 self.bookmarks_button = create_da(
668 generate_image('bookmark-new.png'),
669 player.add_bookmark_at_current_position,
670 generate_image(gtk.STOCK_JUMP_TO, True),
671 lambda *args: self.notify('select-current-item-request'))
672 buttonbox.add(self.bookmarks_button)
673 self.set_controls_sensitivity(False)
674 main_vbox.pack_start(buttonbox, False, False)
676 if util.platform.MAEMO:
677 self.volume = hildon.VVolumebar()
678 self.volume.set_property('can-focus', False)
679 self.volume.connect('level_changed', self.volume_changed_hildon)
680 self.volume.connect('mute_toggled', self.mute_toggled)
681 self.__gui_root.main_window.connect( 'key-press-event',
682 self.on_key_press )
683 if not util.platform.MAEMO5:
684 self.pack_start(self.volume, False, True)
686 # Add a button to pop out the volume bar
687 self.volume_button = gtk.ToggleButton('')
688 image(self.volume_button, 'media-speaker.png')
689 self.volume_button.connect('clicked', self.toggle_volumebar)
690 self.volume.connect(
691 'show', lambda x: self.volume_button.set_active(True))
692 self.volume.connect(
693 'hide', lambda x: self.volume_button.set_active(False))
694 if not util.platform.MAEMO5:
695 buttonbox.add(self.volume_button)
696 self.volume_button.show()
698 # Disable focus for all widgets, so we can use the cursor
699 # keys + enter to directly control our media player, which
700 # is handled by "key-press-event"
701 for w in (
702 self.rrewind_button, self.rewind_button,
703 self.play_pause_button, self.forward_button,
704 self.fforward_button, self.progress,
705 self.bookmarks_button, self.volume_button, ):
706 w.unset_flags(gtk.CAN_FOCUS)
707 else:
708 self.volume = gtk.VolumeButton()
709 self.volume.connect('value-changed', self.volume_changed_gtk)
710 buttonbox.add(self.volume)
711 self.volume.show()
713 self.set_volume(settings.volume)
715 def set_controls_sensitivity(self, sensitive):
716 for button in self.forward_button, self.rewind_button, \
717 self.fforward_button, self.rrewind_button:
719 button.set_sensitive(sensitive)
721 # the play/pause button should always be available except
722 # for when the player starts without a file
723 self.play_pause_button.set_sensitive(True)
725 def on_dual_action_setting_changed( self, *args ):
726 for button in self.forward_button, self.rewind_button, \
727 self.fforward_button, self.rrewind_button, \
728 self.bookmarks_button:
730 button.set_longpress_enabled( settings.enable_dual_action_btn )
731 button.set_duration( settings.dual_action_button_delay )
733 def on_key_press(self, widget, event):
734 if util.platform.MAEMO:
735 if event.keyval == gtk.keysyms.F7: #plus
736 self.set_volume( min( 1, self.get_volume() + 0.10 ))
737 elif event.keyval == gtk.keysyms.F8: #minus
738 self.set_volume( max( 0, self.get_volume() - 0.10 ))
739 elif event.keyval == gtk.keysyms.Left: # seek back
740 self.do_seek( -1 * settings.seek_long )
741 elif event.keyval == gtk.keysyms.Right: # seek forward
742 self.do_seek( settings.seek_long )
743 elif event.keyval == gtk.keysyms.Return: # play/pause
744 self.on_btn_play_pause_clicked()
746 # The following two functions get and set the
747 # volume from the volume control widgets.
748 def get_volume(self):
749 if util.platform.MAEMO:
750 return self.volume.get_level()/100.0
751 else:
752 return self.volume.get_value()
754 def set_volume(self, vol):
755 """ vol is a float from 0 to 1 """
756 assert 0 <= vol <= 1
758 if util.platform.MAEMO5:
759 # No volume setting on Maemo 5
760 return
762 if util.platform.MAEMO:
763 self.volume.set_level(vol*100.0)
764 else:
765 self.volume.set_value(vol)
767 def __set_volume_hide_timer(self, timeout, force_show=False):
768 if force_show or self.volume_button.get_active():
769 self.volume.show()
770 if self.volume_timer_id is not None:
771 gobject.source_remove(self.volume_timer_id)
772 self.volume_timer_id = None
774 self.volume_timer_id = gobject.timeout_add(
775 1000 * timeout, self.__volume_hide_callback )
777 def __volume_hide_callback(self):
778 self.volume_timer_id = None
779 self.volume.hide()
780 return False
782 def toggle_volumebar(self, widget=None):
783 if self.volume_timer_id is None:
784 self.__set_volume_hide_timer(5)
785 else:
786 self.__volume_hide_callback()
788 def volume_changed_gtk(self, widget, new_value=0.5):
789 settings.volume = new_value
791 def volume_changed_hildon(self, widget):
792 self.__set_volume_hide_timer( 4, force_show=True )
793 settings.volume = widget.get_level()/100.0
795 def mute_toggled(self, widget):
796 if widget.get_mute():
797 settings.volume = 0
798 else:
799 settings.volume = widget.get_level()/100.0
801 def on_player_stopped(self):
802 self.stop_progress_timer()
803 self.set_controls_sensitivity(False)
804 image(self.play_pause_button, 'media-playback-start.png')
806 def on_player_playing(self):
807 self.start_progress_timer()
808 image(self.play_pause_button, 'media-playback-pause.png')
809 self.set_controls_sensitivity(True)
811 def on_player_new_track(self):
812 for widget in [self.title_label,self.artist_label,self.album_label]:
813 widget.set_markup('')
814 widget.hide()
816 self.cover_art.hide()
817 self.has_coverart = False
819 def on_player_new_metadata(self):
820 metadata = player.playlist.get_file_metadata()
821 self.set_metadata(metadata)
823 if not player.playing:
824 position = player.playlist.get_current_position()
825 estimated_length = metadata.get('length', 0)
826 self.set_progress_callback( position, estimated_length )
828 def on_player_paused( self, position, duration ):
829 self.stop_progress_timer() # This should save some power
830 self.set_progress_callback( position, duration )
831 image(self.play_pause_button, 'media-playback-start.png')
833 def on_player_end_of_playlist(self, loop):
834 pass
836 def reset_progress(self):
837 self.progress.set_fraction(0)
838 self.set_progress_callback(0,0)
840 def set_progress_callback(self, time_elapsed, total_time):
841 """ times must be in nanoseconds """
842 time_string = "%s / %s" % ( util.convert_ns(time_elapsed),
843 util.convert_ns(total_time) )
844 self.progress.set_text( time_string )
845 fraction = float(time_elapsed) / float(total_time) if total_time else 0
846 self.progress.set_fraction( fraction )
848 def on_progressbar_changed(self, widget, event):
849 if ( not settings.progress_locked and
850 event.type == gtk.gdk.BUTTON_PRESS and event.button == 1 ):
851 new_fraction = event.x/float(widget.get_allocation().width)
852 resp = player.do_seek(percent=new_fraction)
853 if resp:
854 # Preemptively update the progressbar to make seeking smoother
855 self.set_progress_callback( *resp )
857 def on_btn_play_pause_clicked(self, widget=None):
858 player.play_pause_toggle()
860 def progress_timer_callback( self ):
861 if player.playing and not player.seeking:
862 pos_int, dur_int = player.get_position_duration()
863 # This prevents bogus values from being set while seeking
864 if ( pos_int > 10**9 ) and ( dur_int > 10**9 ):
865 self.set_progress_callback( pos_int, dur_int )
866 return True
868 def start_progress_timer( self ):
869 if self.progress_timer_id is not None:
870 self.stop_progress_timer()
872 self.progress_timer_id = gobject.timeout_add(
873 1000, self.progress_timer_callback )
875 def stop_progress_timer( self ):
876 if self.progress_timer_id is not None:
877 gobject.source_remove( self.progress_timer_id )
878 self.progress_timer_id = None
880 def get_coverart_size( self ):
881 if util.platform.MAEMO:
882 if self.__gui_root.fullscreen:
883 size = coverart_sizes['maemo fullscreen']
884 else:
885 size = coverart_sizes['maemo']
886 else:
887 size = coverart_sizes['normal']
889 return size, size
891 def set_coverart( self, pixbuf ):
892 self.cover_art.set_from_pixbuf(pixbuf)
893 self.cover_art.show()
894 self.has_coverart = True
896 def set_metadata( self, tag_message ):
897 tags = { 'title': self.title_label, 'artist': self.artist_label,
898 'album': self.album_label }
900 # set the coverart
901 if tag_message.has_key('image') and tag_message['image'] is not None:
902 value = tag_message['image']
904 pbl = gtk.gdk.PixbufLoader()
905 try:
906 pbl.write(value)
907 pbl.close()
909 x, y = self.get_coverart_size()
910 pixbuf = pbl.get_pixbuf()
911 pixbuf = pixbuf.scale_simple( x, y, gtk.gdk.INTERP_BILINEAR )
912 self.set_coverart(pixbuf)
913 except Exception, e:
914 self.__log.exception('Error setting coverart...')
916 # set the text metadata
917 for tag,value in tag_message.iteritems():
918 if tags.has_key(tag) and value is not None and value.strip():
919 tags[tag].set_markup('<big>'+value+'</big>')
920 tags[tag].set_alignment( 0.5*int(not self.has_coverart), 0.5)
921 tags[tag].show()
923 if tag == 'title':
924 # make the title bold
925 tags[tag].set_markup('<b><big>'+value+'</big></b>')
927 if not util.platform.MAEMO:
928 value += ' - Panucci'
930 self.__gui_root.main_window.set_title( value )
932 def do_seek(self, seek_amount):
933 resp = player.do_seek(from_current=seek_amount*10**9)
934 if resp:
935 # Preemptively update the progressbar to make seeking smoother
936 self.set_progress_callback( *resp )
940 ##################################################
941 # PlaylistTab
942 ##################################################
943 class PlaylistTab(gtk.VBox):
944 def __init__(self, main_window):
945 gtk.VBox.__init__(self)
946 self.__log = logging.getLogger('panucci.panucci.BookmarksWindow')
947 self.main = main_window
949 self.__model = gtk.TreeStore(
950 # uid, name, position
951 gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING )
953 self.set_spacing(5)
954 self.treeview = gtk.TreeView()
955 self.treeview.set_model(self.__model)
956 self.treeview.set_headers_visible(True)
957 tree_selection = self.treeview.get_selection()
958 # This breaks drag and drop, only use single selection for now
959 # tree_selection.set_mode(gtk.SELECTION_MULTIPLE)
960 tree_selection.connect('changed', self.tree_selection_changed)
962 # The tree lines look nasty on maemo
963 if util.platform.DESKTOP:
964 self.treeview.set_enable_tree_lines(True)
965 self.update_model()
967 ncol = gtk.TreeViewColumn(_('Name'))
968 ncell = gtk.CellRendererText()
969 ncell.set_property('ellipsize', pango.ELLIPSIZE_END)
970 ncell.set_property('editable', True)
971 ncell.connect('edited', self.label_edited)
972 ncol.set_expand(True)
973 ncol.pack_start(ncell)
974 ncol.add_attribute(ncell, 'text', 1)
976 tcol = gtk.TreeViewColumn(_('Position'))
977 tcell = gtk.CellRendererText()
978 tcol.pack_start(tcell)
979 tcol.add_attribute(tcell, 'text', 2)
981 self.treeview.append_column(ncol)
982 self.treeview.append_column(tcol)
983 self.treeview.connect('drag-data-received', self.drag_data_recieved)
984 self.treeview.connect('drag_data_get', self.drag_data_get_data)
986 treeview_targets = [
987 ( 'playlist_row_data', gtk.TARGET_SAME_WIDGET, 0 ) ]
989 self.treeview.enable_model_drag_source(
990 gtk.gdk.BUTTON1_MASK, treeview_targets, gtk.gdk.ACTION_COPY )
992 self.treeview.enable_model_drag_dest(
993 treeview_targets, gtk.gdk.ACTION_COPY )
995 sw = gtk.ScrolledWindow()
996 sw.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
997 sw.set_shadow_type(gtk.SHADOW_IN)
998 sw.add(self.treeview)
999 self.add(sw)
1001 self.hbox = gtk.HBox()
1003 self.add_button = gtk.Button(gtk.STOCK_NEW)
1004 self.add_button.set_use_stock(True)
1005 set_stock_button_text( self.add_button, _('Add File') )
1006 self.add_button.connect('clicked', self.add_file)
1007 self.hbox.pack_start(self.add_button, True, True)
1009 self.dir_button = gtk.Button(gtk.STOCK_OPEN)
1010 self.dir_button.set_use_stock(True)
1011 set_stock_button_text( self.dir_button, _('Add Directory') )
1012 self.dir_button.connect('clicked', self.add_directory)
1013 self.hbox.pack_start(self.dir_button, True, True)
1015 self.remove_button = widgets.DualActionButton(
1016 generate_image(gtk.STOCK_REMOVE, True),
1017 self.remove_bookmark,
1018 generate_image(gtk.STOCK_CANCEL, True),
1019 lambda *a: player.playlist.reset_playlist() )
1020 #self.remove_button.set_use_stock(True)
1021 #self.remove_button.connect('clicked', self.remove_bookmark)
1022 self.hbox.pack_start(self.remove_button, True, True)
1024 self.jump_button = gtk.Button(gtk.STOCK_JUMP_TO)
1025 self.jump_button.set_use_stock(True)
1026 self.jump_button.connect('clicked', self.jump_bookmark)
1027 self.hbox.pack_start(self.jump_button, True, True)
1029 self.info_button = gtk.Button()
1030 self.info_button.add(
1031 gtk.image_new_from_stock(gtk.STOCK_INFO, gtk.ICON_SIZE_BUTTON))
1032 self.info_button.connect('clicked', self.show_playlist_item_details)
1033 self.hbox.pack_start(self.info_button, True, True)
1035 self.pack_start(self.hbox, False, True)
1037 player.playlist.register( 'file_queued',
1038 lambda x,y,z: self.update_model() )
1039 player.playlist.register( 'bookmark_added', self.on_bookmark_added )
1041 self.show_all()
1043 def tree_selection_changed(self, treeselection):
1044 count = treeselection.count_selected_rows()
1045 self.remove_button.set_sensitive(count > 0)
1046 self.jump_button.set_sensitive(count == 1)
1047 self.info_button.set_sensitive(count == 1)
1049 def drag_data_get_data(
1050 self, treeview, context, selection, target_id, timestamp):
1052 treeselection = treeview.get_selection()
1053 model, iter = treeselection.get_selected()
1054 # only allow moving around top-level parents
1055 if model.iter_parent(iter) is None:
1056 # send the path of the selected row
1057 data = model.get_string_from_iter(iter)
1058 selection.set(selection.target, 8, data)
1059 else:
1060 self.__log.debug("Can't move children...")
1062 def drag_data_recieved(
1063 self, treeview, context, x, y, selection, info, timestamp):
1065 drop_info = treeview.get_dest_row_at_pos(x, y)
1067 # TODO: If user drags the row past the last row, drop_info is None
1068 # I'm not sure if it's safe to simply assume that None is
1069 # euqivalent to the last row...
1070 if None not in [ drop_info and selection.data ]:
1071 model = treeview.get_model()
1072 path, position = drop_info
1074 from_iter = model.get_iter_from_string(selection.data)
1076 # make sure the to_iter doesn't have a parent
1077 to_iter = model.get_iter(path)
1078 if model.iter_parent(to_iter) is not None:
1079 to_iter = model.iter_parent(to_iter)
1081 from_row = model.get_path(from_iter)[0]
1082 to_row = path[0]
1084 if ( position == gtk.TREE_VIEW_DROP_BEFORE or
1085 position == gtk.TREE_VIEW_DROP_INTO_OR_BEFORE ):
1086 model.move_before( from_iter, to_iter )
1087 to_row = to_row - 1 if from_row < to_row else to_row
1088 elif ( position == gtk.TREE_VIEW_DROP_AFTER or
1089 position == gtk.TREE_VIEW_DROP_INTO_OR_AFTER ):
1090 model.move_after( from_iter, to_iter )
1091 to_row = to_row + 1 if from_row > to_row else to_row
1092 else:
1093 self.__log.debug('Drop not supported: %s', position)
1095 # don't do anything if we're not actually moving rows around
1096 if from_row != to_row:
1097 player.playlist.move_item( from_row, to_row )
1099 else:
1100 self.__log.debug('No drop_data or selection.data available')
1102 def update_model(self):
1103 plist = player.playlist
1104 path_info = self.treeview.get_path_at_pos(0,0)
1105 path = path_info[0] if path_info is not None else None
1107 self.__model.clear()
1109 # build the tree
1110 for item, data in plist.get_playlist_item_ids():
1111 parent = self.__model.append(None, (item, data.get('title'), None))
1113 for bid, bname, bpos in plist.get_bookmarks_from_item_id( item ):
1114 nice_bpos = util.convert_ns(bpos)
1115 self.__model.append( parent, (bid, bname, nice_bpos) )
1117 self.treeview.expand_all()
1119 if path is not None:
1120 self.treeview.scroll_to_cell(path)
1122 def label_edited(self, cellrenderer, path, new_text):
1123 iter = self.__model.get_iter(path)
1124 old_text = self.__model.get_value(iter, 1)
1126 if new_text.strip() and old_text != new_text:
1127 # this loop will only run once, because only one cell can be
1128 # edited at a time, we use it to get the item and bookmark ids
1129 for m, bkmk_id, biter, item_id, iiter in self.__cur_selection():
1130 self.__model.set_value(iter, 1, new_text)
1131 player.playlist.update_bookmark(
1132 item_id, bkmk_id, name=new_text )
1133 else:
1134 self.__model.set_value(iter, 1, old_text)
1136 def on_bookmark_added(self, parent_id, bookmark_name, position):
1137 util.notify(_('Bookmark added: %s') % bookmark_name)
1138 self.update_model()
1140 def add_file(self, widget):
1141 filename = get_file_from_filechooser(self.main.main_window)
1142 if filename is not None:
1143 player.playlist.append(filename)
1145 def add_directory(self, widget):
1146 directory = get_file_from_filechooser(
1147 self.main.main_window, folder=True )
1148 if directory is not None:
1149 player.playlist.load_directory(directory, append=True)
1151 def __cur_selection(self):
1152 selection = self.treeview.get_selection()
1153 model, bookmark_paths = selection.get_selected_rows()
1155 # Convert the paths to gtk.TreeRowReference objects, because we
1156 # might modify the model while this generator is running
1157 bookmark_refs = [gtk.TreeRowReference(model, p) for p in bookmark_paths]
1159 for reference in bookmark_refs:
1160 bookmark_iter = model.get_iter(reference.get_path())
1161 item_iter = model.iter_parent(bookmark_iter)
1163 # bookmark_iter is actually an item_iter
1164 if item_iter is None:
1165 item_iter = bookmark_iter
1166 item_id = model.get_value(item_iter, 0)
1167 bookmark_id, bookmark_iter = None, None
1168 else:
1169 bookmark_id = model.get_value(bookmark_iter, 0)
1170 item_id = model.get_value(item_iter, 0)
1172 yield model, bookmark_id, bookmark_iter, item_id, item_iter
1174 def remove_bookmark(self, w=None):
1175 for model, bkmk_id, bkmk_iter, item_id, item_iter in self.__cur_selection():
1176 player.playlist.remove_bookmark( item_id, bkmk_id )
1177 if bkmk_iter is not None:
1178 model.remove(bkmk_iter)
1179 elif item_iter is not None:
1180 model.remove(item_iter)
1182 def select_current_item(self):
1183 model = self.treeview.get_model()
1184 selection = self.treeview.get_selection()
1185 current_item_id = str(player.playlist.get_current_item())
1186 for row in iter(model):
1187 if model.get_value(row.iter, 0) == current_item_id:
1188 selection.unselect_all()
1189 self.treeview.set_cursor(row.path)
1190 self.treeview.scroll_to_cell(row.path, use_align=True)
1191 break
1193 def show_playlist_item_details(self, w):
1194 selection = self.treeview.get_selection()
1195 if selection.count_selected_rows() == 1:
1196 selected = self.__cur_selection().next()
1197 model, bkmk_id, bkmk_iter, item_id, item_iter = selected
1198 playlist_item = player.playlist.get_item_by_id(item_id)
1199 PlaylistItemDetails(self.main, playlist_item)
1201 def jump_bookmark(self, w):
1202 selected = list(self.__cur_selection())
1203 if len(selected) == 1:
1204 # It should be guranteed by the fact that we only enable the
1205 # "Jump to" button when the selection count equals 1.
1206 model, bkmk_id, bkmk_iter, item_id, item_iter = selected.pop(0)
1207 player.playlist.load_from_bookmark_id(item_id, bkmk_id)
1209 # FIXME: The player/playlist should be able to take care of this
1210 if not player.playing:
1211 player.play()
1214 ##################################################
1215 # PlaylistItemDetails
1216 ##################################################
1217 class PlaylistItemDetails(gtk.Dialog):
1218 def __init__(self, main, playlist_item):
1219 gtk.Dialog.__init__( self, _('Playlist item details'),
1220 main.main_window, gtk.DIALOG_MODAL,
1221 (gtk.STOCK_CLOSE, gtk.RESPONSE_OK))
1223 self.main = main
1224 self.fill(playlist_item)
1225 self.set_has_separator(False)
1226 self.set_resizable(False)
1227 self.show_all()
1228 self.run()
1229 self.destroy()
1231 def fill(self, playlist_item):
1232 t = gtk.Table(10, 2)
1233 self.vbox.pack_start(t, expand=False)
1235 metadata = playlist_item.metadata
1237 t.attach(gtk.Label(_('Custom title:')), 0, 1, 0, 1)
1238 t.attach(gtk.Label(_('ID:')), 0, 1, 1, 2)
1239 t.attach(gtk.Label(_('Playlist ID:')), 0, 1, 2, 3)
1240 t.attach(gtk.Label(_('Filepath:')), 0, 1, 3, 4)
1242 row_num = 4
1243 for key in metadata:
1244 if metadata[key] is not None:
1245 t.attach( gtk.Label(key.capitalize()+':'),
1246 0, 1, row_num, row_num+1 )
1247 row_num += 1
1249 t.foreach(lambda x, y: x.set_alignment(1, 0.5), None)
1250 t.foreach(lambda x, y: x.set_markup('<b>%s</b>' % x.get_label()), None)
1252 t.attach(gtk.Label(playlist_item.title or _('<not modified>')),1,2,0,1)
1253 t.attach(gtk.Label(str(playlist_item)), 1, 2, 1, 2)
1254 t.attach(gtk.Label(playlist_item.playlist_id), 1, 2, 2, 3)
1255 t.attach(gtk.Label(playlist_item.filepath), 1, 2, 3, 4)
1257 row_num = 4
1258 for key in metadata:
1259 value = metadata[key]
1260 if key == 'length':
1261 value = util.convert_ns(value)
1262 if metadata[key] is not None:
1263 t.attach( gtk.Label( str(value) or _('<not set>')),
1264 1, 2, row_num, row_num+1)
1265 row_num += 1
1267 t.foreach(lambda x, y: x.get_alignment() == (0.5, 0.5) and \
1268 x.set_alignment(0, 0.5), None)
1270 t.set_border_width(8)
1271 t.set_row_spacings(4)
1272 t.set_col_spacings(8)
1274 l = gtk.ListStore(str, str)
1275 t = gtk.TreeView(l)
1276 cr = gtk.CellRendererText()
1277 cr.set_property('ellipsize', pango.ELLIPSIZE_END)
1278 c = gtk.TreeViewColumn(_('Title'), cr, text=0)
1279 c.set_expand(True)
1280 t.append_column(c)
1281 c = gtk.TreeViewColumn(_('Time'), gtk.CellRendererText(), text=1)
1282 t.append_column(c)
1283 playlist_item.load_bookmarks()
1284 for bookmark in playlist_item.bookmarks:
1285 l.append([bookmark.bookmark_name, \
1286 util.convert_ns(bookmark.seek_position)])
1288 sw = gtk.ScrolledWindow()
1289 sw.set_shadow_type(gtk.SHADOW_IN)
1290 sw.add(t)
1291 sw.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
1292 e = gtk.Expander(_('Bookmarks'))
1293 e.add(sw)
1294 self.vbox.pack_start(e)
1297 def run(filename=None):
1298 PanucciGUI( filename )
1299 gtk.main()
1301 if __name__ == '__main__':
1302 log.error( 'Use the "panucci" executable to run this program.' )
1303 log.error( 'Exiting...' )
1304 sys.exit(1)