Add GUI option to enable hardware decoding, bump version to 0.3-pre0.4
[panucci.git] / src / panucci / panucci.py
blob5b7337cb83b43a4ff2231d4182f7997cd076da25
1 #!/usr/bin/env python
3 # This file is part of Panucci.
4 # Copyright (c) 2008-2009 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)
26 import logging
27 import sys
28 import os, os.path
29 import time
31 import gtk
32 import gobject
33 import pango
35 import widgets
37 # At the moment, we don't have gettext support, so
38 # make a dummy "_" function to passthrough the string
39 _ = lambda s: s
41 log = logging.getLogger('panucci.panucci')
43 import util
45 try:
46 import hildon
47 except:
48 if util.platform == util.MAEMO:
49 log.critical( 'Using GTK widgets, install "python2.5-hildon" '
50 'for this to work properly.' )
52 from simplegconf import gconf
53 from settings import settings
54 from player import player
55 from dbusinterface import interface
56 from 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/'
62 app_version = ''
64 coverart_sizes = {
65 'normal' : 110,
66 'maemo' : 200,
67 'maemo fullscreen' : 275,
70 gtk.about_dialog_set_url_hook(util.open_link, None)
71 gtk.icon_size_register('panucci-button', 32, 32)
73 def generate_image(filename, is_stock=False):
74 image = None
75 if is_stock:
76 image = gtk.image_new_from_stock(
77 filename, gtk.icon_size_from_name('panucci-button') )
78 else:
79 filename = util.find_image(filename)
80 if filename is not None:
81 image = gtk.image_new_from_file(filename)
82 if image is not None:
83 if util.platform == util.MAEMO:
84 image.set_padding(20, 20)
85 else:
86 image.set_padding(5, 5)
87 image.show()
88 return image
90 def image(widget, filename, is_stock=False):
91 child = widget.get_child()
92 if child is not None:
93 widget.remove(child)
94 image = generate_image(filename, is_stock)
95 if image is not None:
96 widget.add(image)
98 def dialog( toplevel_window, title, question, description,
99 affirmative_button=gtk.STOCK_YES, negative_button=gtk.STOCK_NO,
100 abortion_button=gtk.STOCK_CANCEL ):
102 """Present the user with a yes/no/cancel dialog.
103 The return value is either True, False or None, depending on which
104 button has been pressed in the dialog:
106 affirmative button (default: Yes) => True
107 negative button (defaut: No) => False
108 abortion button (default: Cancel) => None
110 When the dialog is closed with the "X" button in the window manager
111 decoration, the return value is always None (same as abortion button).
113 You can set any of the affirmative_button, negative_button or
114 abortion_button values to "None" to hide the corresponding action.
116 dlg = gtk.MessageDialog( toplevel_window, gtk.DIALOG_MODAL,
117 gtk.MESSAGE_QUESTION, message_format=question )
119 dlg.set_title(title)
121 if abortion_button is not None:
122 dlg.add_button(abortion_button, gtk.RESPONSE_CANCEL)
123 if negative_button is not None:
124 dlg.add_button(negative_button, gtk.RESPONSE_NO)
125 if affirmative_button is not None:
126 dlg.add_button(affirmative_button, gtk.RESPONSE_YES)
128 dlg.format_secondary_text(description)
130 response = dlg.run()
131 dlg.destroy()
133 if response == gtk.RESPONSE_YES:
134 return True
135 elif response == gtk.RESPONSE_NO:
136 return False
137 elif response in [gtk.RESPONSE_CANCEL, gtk.RESPONSE_DELETE_EVENT]:
138 return None
140 def get_file_from_filechooser(
141 toplevel_window, folder=False, save_file=False, save_to=None):
143 if folder:
144 open_action = gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER
145 else:
146 open_action = gtk.FILE_CHOOSER_ACTION_OPEN
148 if util.platform == util.MAEMO:
149 if save_file:
150 args = ( toplevel_window, gtk.FILE_CHOOSER_ACTION_SAVE )
151 else:
152 args = ( toplevel_window, open_action )
154 dlg = hildon.FileChooserDialog( *args )
155 else:
156 if save_file:
157 args = ( _('Select file to save playlist to'), None,
158 gtk.FILE_CHOOSER_ACTION_SAVE,
159 (( gtk.STOCK_CANCEL, gtk.RESPONSE_REJECT,
160 gtk.STOCK_SAVE, gtk.RESPONSE_OK )) )
161 else:
162 args = ( _('Select podcast or audiobook'), None, open_action,
163 (( gtk.STOCK_CANCEL, gtk.RESPONSE_REJECT,
164 gtk.STOCK_OPEN, gtk.RESPONSE_OK )) )
166 dlg = gtk.FileChooserDialog(*args)
168 current_folder = os.path.expanduser(settings.last_folder)
170 if current_folder is not None and os.path.isdir(current_folder):
171 dlg.set_current_folder(current_folder)
173 if save_file and save_to is not None:
174 dlg.set_current_name(save_to)
176 if dlg.run() == gtk.RESPONSE_OK:
177 filename = dlg.get_filename()
178 settings.last_folder = dlg.get_current_folder()
179 else:
180 filename = None
182 dlg.destroy()
183 return filename
185 def set_stock_button_text( button, text ):
186 alignment = button.get_child()
187 hbox = alignment.get_child()
188 image, label = hbox.get_children()
189 label.set_text(text)
191 ##################################################
192 # PanucciGUI
193 ##################################################
194 class PanucciGUI(object):
195 """ The object that holds the entire panucci gui """
197 PLAYER_TAB, PLAYLIST_TAB = range(2)
199 def __init__(self, filename=None):
200 self.__log = logging.getLogger('panucci.panucci.PanucciGUI')
201 interface.register_gui(self)
203 # Build the base ui (window and menubar)
204 if util.platform == util.MAEMO:
205 self.app = hildon.Program()
206 window = hildon.Window()
207 self.app.add_window(window)
208 else:
209 window = gtk.Window(gtk.WINDOW_TOPLEVEL)
211 self.main_window = window
212 window.set_title('Panucci')
213 self.window_icon = util.find_image('panucci.png')
214 if self.window_icon is not None:
215 window.set_icon_from_file( self.window_icon )
216 window.set_default_size(400, -1)
217 window.set_border_width(0)
218 window.connect("destroy", self.destroy)
220 self.notebook = gtk.Notebook()
221 self.notebook.unset_flags(gtk.CAN_FOCUS)
223 if util.platform == util.MAEMO:
224 window.set_menu(self.create_menu())
225 window.add(self.notebook)
226 else:
227 menu_vbox = gtk.VBox()
228 menu_vbox.set_spacing(0)
229 window.add(menu_vbox)
230 menu_bar = gtk.MenuBar()
231 root_menu = gtk.MenuItem('Panucci')
232 root_menu.set_submenu(self.create_menu())
233 menu_bar.append(root_menu)
234 menu_vbox.pack_start(menu_bar, False, False, 0)
235 menu_bar.show()
236 menu_vbox.pack_end(self.notebook, True, True, 6)
238 # Add the tabs (they are private to prevent us from trying to do
239 # something like gui_root.player_tab.some_function() from inside
240 # playlist_tab or vice-versa)
241 self.__player_tab = PlayerTab(self)
242 self.__playlist_tab = PlaylistTab(self)
244 self.notebook.insert_page( self.__player_tab,
245 gtk.Label(_('Player')),
246 self.PLAYER_TAB )
247 self.notebook.set_tab_label_packing( self.__player_tab, True, True,
248 gtk.PACK_START)
250 self.notebook.insert_page( self.__playlist_tab,
251 gtk.Label(_('Playlist')),
252 self.PLAYLIST_TAB )
253 self.notebook.set_tab_label_packing( self.__playlist_tab, True, True,
254 gtk.PACK_START )
256 # Tie it all together!
257 self.__ignore_queue_check = False
258 self.__window_fullscreen = False
260 if util.platform==util.MAEMO and interface.headset_device is not None:
261 # Enable play/pause with headset button
262 interface.headset_device.connect_to_signal(
263 'Condition', self.handle_headset_button )
265 self.main_window.connect('key-press-event', self.on_key_press)
266 player.playlist.register( 'file_queued', self.on_file_queued )
268 self.__anti_blank_timer = None
269 settings.register('allow_blanking_changed',self.__set_anti_blank_timer)
270 self.__set_anti_blank_timer( settings.allow_blanking )
272 player.playlist.register( 'playlist-to-be-overwritten',
273 self.check_queue )
274 self.__player_tab.register( 'select-current-item-request',
275 self.__select_current_item )
277 self.main_window.show_all()
279 # this should be done when the gui is ready
280 self.notebook.set_current_page(self.PLAYER_TAB)
281 self.pickle_file_conversion()
282 player.init(filepath=filename)
284 def create_menu(self):
285 # the main menu
286 menu = gtk.Menu()
288 menu_open = gtk.ImageMenuItem(_('Open playlist'))
289 menu_open.set_image(
290 gtk.image_new_from_stock(gtk.STOCK_OPEN, gtk.ICON_SIZE_MENU))
291 menu_open.connect("activate", self.open_file_callback)
292 menu.append(menu_open)
294 # the recent files menu
295 self.menu_recent = gtk.MenuItem(_('Open recent playlist'))
296 menu.append(self.menu_recent)
297 self.create_recent_files_menu()
299 menu.append(gtk.SeparatorMenuItem())
301 menu_save = gtk.ImageMenuItem(_('Save current playlist'))
302 menu_save.set_image(
303 gtk.image_new_from_stock(gtk.STOCK_SAVE_AS, gtk.ICON_SIZE_MENU))
304 menu_save.connect("activate", self.save_to_playlist_callback)
305 menu.append(menu_save)
307 menu.append(gtk.SeparatorMenuItem())
309 # the settings sub-menu
310 menu_settings = gtk.MenuItem(_('Settings'))
311 menu.append(menu_settings)
313 menu_settings_sub = gtk.Menu()
314 menu_settings.set_submenu(menu_settings_sub)
316 menu_settings_enable_dual_action = gtk.CheckMenuItem(
317 _('Enable dual-action buttons') )
318 settings.attach_checkbutton( menu_settings_enable_dual_action,
319 'enable_dual_action_btn' )
320 menu_settings_sub.append(menu_settings_enable_dual_action)
322 if util.platform == util.MAEMO:
323 menu_settings_enable_hw_decoding = gtk.CheckMenuItem(
324 _('Enable hardware decoding') )
325 settings.attach_checkbutton( menu_settings_enable_hw_decoding,
326 'enable_hardware_decoding' )
327 menu_settings_sub.append(menu_settings_enable_hw_decoding)
329 menu_settings_lock_progress = gtk.CheckMenuItem(_('Lock Progress Bar'))
330 settings.attach_checkbutton( menu_settings_lock_progress,
331 'progress_locked' )
332 menu_settings_sub.append(menu_settings_lock_progress)
334 menu_about = gtk.ImageMenuItem(gtk.STOCK_ABOUT)
335 menu_about.connect("activate", self.show_about, self.main_window)
336 menu.append(menu_about)
338 menu.append(gtk.SeparatorMenuItem())
340 menu_quit = gtk.ImageMenuItem(gtk.STOCK_QUIT)
341 menu_quit.connect("activate", self.destroy)
342 menu.append(menu_quit)
344 return menu
346 def create_recent_files_menu( self ):
347 max_files = settings.max_recent_files
348 self.recent_files = player.playlist.get_recent_files(max_files)
349 menu_recent_sub = gtk.Menu()
351 temp_playlist = os.path.expanduser(settings.temp_playlist)
353 if len(self.recent_files) > 0:
354 for f in self.recent_files:
355 # don't include the temporary playlist in the file list
356 if f == temp_playlist: continue
357 # don't include non-existant files
358 if not os.path.exists( f ): continue
359 filename, extension = os.path.splitext(os.path.basename(f))
360 menu_item = gtk.MenuItem( filename.replace('_', ' '))
361 menu_item.connect('activate', self.on_recent_file_activate, f)
362 menu_recent_sub.append(menu_item)
363 else:
364 menu_item = gtk.MenuItem(_('No recent files available.'))
365 menu_item.set_sensitive(False)
366 menu_recent_sub.append(menu_item)
368 self.menu_recent.set_submenu(menu_recent_sub)
370 def destroy(self, widget):
371 player.quit()
372 gtk.main_quit()
374 def show_main_window(self):
375 self.main_window.present()
377 def check_queue(self):
378 """ Makes sure the queue is saved if it has been modified
379 True means a new file can be opened
380 False means the user does not want to continue """
382 if not self.__ignore_queue_check and player.playlist.queue_modified:
383 response = dialog(
384 self.main_window, _('Save current playlist'),
385 _('Current playlist has been modified'),
386 _('Opening a new file will replace the current playlist. ') +
387 _('Do you want to save it before creating a new one?'),
388 affirmative_button=gtk.STOCK_SAVE,
389 negative_button=_('Discard changes'))
391 self.__log.debug('Response to "Save Queue?": %s', response)
393 if response is None:
394 return False
395 elif response:
396 return self.save_to_playlist_callback()
397 elif not response:
398 return True
399 else:
400 return False
401 else:
402 return True
404 def open_file_callback(self, widget=None):
405 if self.check_queue():
406 # set __ingnore__queue_check because we already did the check
407 self.__ignore_queue_check = True
408 filename = get_file_from_filechooser(self.main_window)
409 if filename is not None:
410 self._play_file(filename)
412 self.__ignore_queue_check = False
414 def save_to_playlist_callback(self, widget=None):
415 filename = get_file_from_filechooser(
416 self.main_window, save_file=True, save_to='playlist.m3u' )
418 if filename is None:
419 return False
421 if os.path.isfile(filename):
422 response = dialog( self.main_window, _('File already exists'),
423 _('File already exists'),
424 _('The file %s already exists. You can choose another name or '
425 'overwrite the existing file.') % os.path.basename(filename),
426 affirmative_button=gtk.STOCK_SAVE,
427 negative_button=_('Rename file'))
429 if response is None:
430 return None
432 elif response:
433 pass
434 elif not response:
435 return self.save_to_playlist_callback()
437 ext = util.detect_filetype(filename)
438 if not player.playlist.save_to_new_playlist(filename, ext):
439 util.notify(_('Error saving playlist...'))
440 return False
442 return True
444 def __get_fullscreen(self):
445 return self.__window_fullscreen
447 def __set_fullscreen(self, value):
448 if value != self.__window_fullscreen:
449 if value:
450 self.main_window.fullscreen()
451 else:
452 self.main_window.unfullscreen()
454 self.__window_fullscreen = value
455 player.playlist.send_metadata()
457 fullscreen = property( __get_fullscreen, __set_fullscreen )
459 def on_key_press(self, widget, event):
460 if util.platform == util.MAEMO:
461 if event.keyval == gtk.keysyms.F6:
462 self.fullscreen = not self.fullscreen
464 def on_recent_file_activate(self, widget, filepath):
465 self._play_file(filepath)
467 def on_file_queued(self, filepath, success, notify):
468 if notify:
469 filename = os.path.basename(filepath)
470 if success:
471 self.__log.info(
472 util.notify( '%s added successfully.' % filename ))
473 else:
474 self.__log.error(
475 util.notify( 'Error adding %s to the queue.' % filename))
477 def show_about(self, w, win):
478 dialog = gtk.AboutDialog()
479 dialog.set_website(about_website)
480 dialog.set_website_label(about_website)
481 dialog.set_name(about_name)
482 dialog.set_authors(about_authors)
483 dialog.set_comments(about_text)
484 dialog.set_version(app_version)
485 dialog.run()
486 dialog.destroy()
488 def _play_file(self, filename, pause_on_load=False):
489 player.playlist.load( os.path.abspath(filename) )
490 if player.playlist.is_empty:
491 return False
493 player.play()
495 def handle_headset_button(self, event, button):
496 if event == 'ButtonPressed' and button == 'phone':
497 player.play_pause_toggle()
499 def __set_anti_blank_timer(self, allow_blanking):
500 if util.platform == util.MAEMO:
501 if allow_blanking and self.__anti_blank_timer is not None:
502 self.__log.info('Screen blanking enabled.')
503 gobject.source_remove(self.__anti_blank_timer)
504 self.__anti_blank_timer = None
505 elif not allow_blanking and self.__anti_blank_timer is None:
506 self.__log.info('Attempting to disable screen blanking.')
507 self.__anti_blank_timer = gobject.timeout_add(
508 1000 * 59, util.poke_backlight )
509 else:
510 self.__log.info('Blanking controls are for Maemo only.')
512 def __select_current_item( self ):
513 # Select the currently playing track in the playlist tab
514 # and switch to it (so we can edit bookmarks, etc.. there)
515 self.notebook.set_current_page(self.PLAYLIST_TAB)
516 self.__playlist_tab.select_current_item()
518 def pickle_file_conversion(self):
519 pickle_file = os.path.expanduser('~/.rmp-bookmarks')
520 if os.path.isfile(pickle_file):
521 import pickle_converter
523 self.__log.info(
524 util.notify( _('Converting old pickle format to SQLite.') ))
525 self.__log.info( util.notify( _('This may take a while...') ))
527 if pickle_converter.load_pickle_file(pickle_file):
528 self.__log.info(
529 util.notify( _('Pickle file converted successfully.') ))
530 else:
531 self.__log.error( util.notify(
532 _('Error converting pickle file, check your log...') ))
534 ##################################################
535 # PlayerTab
536 ##################################################
537 class PlayerTab(ObservableService, gtk.HBox):
538 """ The tab that holds the player elements """
540 signals = [ 'select-current-item-request', ]
542 def __init__(self, gui_root):
543 self.__log = logging.getLogger('panucci.panucci.PlayerTab')
544 self.__gui_root = gui_root
546 gtk.HBox.__init__(self)
547 ObservableService.__init__(self, self.signals, self.__log)
549 # Timers
550 self.progress_timer_id = None
551 self.volume_timer_id = None
553 self.recent_files = []
554 self.make_player_tab()
555 self.has_coverart = False
556 self.set_volume(settings.volume)
558 settings.register( 'enable_dual_action_btn_changed',
559 self.on_dual_action_setting_changed )
560 settings.register( 'dual_action_button_delay_changed',
561 self.on_dual_action_setting_changed )
562 settings.register( 'volume_changed', self.set_volume )
563 settings.register( 'scrolling_labels_changed', lambda v:
564 setattr( self.title_label, 'scrolling', v ) )
566 player.register( 'stopped', self.on_player_stopped )
567 player.register( 'playing', self.on_player_playing )
568 player.register( 'paused', self.on_player_paused )
569 player.playlist.register( 'end-of-playlist',
570 self.on_player_end_of_playlist )
571 player.playlist.register( 'new-track-playing',
572 self.on_player_new_track )
573 player.playlist.register( 'new-metadata-available',
574 self.on_player_new_metadata )
576 def make_player_tab(self):
577 main_vbox = gtk.VBox()
578 main_vbox.set_spacing(6)
579 # add a vbox to self
580 self.pack_start(main_vbox, True, True)
582 # a hbox to hold the cover art and metadata vbox
583 metadata_hbox = gtk.HBox()
584 metadata_hbox.set_spacing(6)
585 main_vbox.pack_start(metadata_hbox, True, False)
587 self.cover_art = gtk.Image()
588 metadata_hbox.pack_start( self.cover_art, False, False )
590 # vbox to hold metadata
591 metadata_vbox = gtk.VBox()
592 metadata_vbox.set_spacing(8)
593 empty_label = gtk.Label()
594 metadata_vbox.pack_start(empty_label, True, True)
595 self.artist_label = gtk.Label('')
596 self.artist_label.set_ellipsize(pango.ELLIPSIZE_END)
597 metadata_vbox.pack_start(self.artist_label, False, False)
598 self.album_label = gtk.Label('')
599 self.album_label.set_ellipsize(pango.ELLIPSIZE_END)
600 metadata_vbox.pack_start(self.album_label, False, False)
601 self.title_label = widgets.ScrollingLabel( '',
602 update_interval=200,
603 pixel_jump=5,
604 delay_btwn_scrolls=5000,
605 delay_halfway=3000 )
606 self.title_label.scrolling = settings.scrolling_labels
607 metadata_vbox.pack_start(self.title_label, False, False)
608 empty_label = gtk.Label()
609 metadata_vbox.pack_start(empty_label, True, True)
610 metadata_hbox.pack_start( metadata_vbox, True, True )
612 progress_eventbox = gtk.EventBox()
613 progress_eventbox.set_events(gtk.gdk.BUTTON_PRESS_MASK)
614 progress_eventbox.connect(
615 'button-press-event', self.on_progressbar_changed )
616 self.progress = gtk.ProgressBar()
617 # make the progress bar more "finger-friendly"
618 if util.platform == util.MAEMO:
619 self.progress.set_size_request( -1, 50 )
620 progress_eventbox.add(self.progress)
621 main_vbox.pack_start( progress_eventbox, False, False )
623 # make the button box
624 buttonbox = gtk.HBox()
626 # A wrapper to help create DualActionButtons with the right settings
627 create_da = lambda a, b, c=None, d=None: widgets.DualActionButton(
628 a, b, c, d, settings.dual_action_button_delay,
629 settings.enable_dual_action_btn )
631 self.rrewind_button = create_da(
632 generate_image('media-skip-backward.png'),
633 lambda: self.do_seek(-1*settings.seek_long),
634 generate_image(gtk.STOCK_GOTO_FIRST, True),
635 player.playlist.prev)
636 buttonbox.add(self.rrewind_button)
638 self.rewind_button = create_da(
639 generate_image('media-seek-backward.png'),
640 lambda: self.do_seek(-1*settings.seek_short))
641 buttonbox.add(self.rewind_button)
643 self.play_pause_button = gtk.Button('')
644 image(self.play_pause_button, 'media-playback-start.png')
645 self.play_pause_button.connect( 'clicked',
646 self.on_btn_play_pause_clicked )
647 self.play_pause_button.set_sensitive(False)
648 buttonbox.add(self.play_pause_button)
650 self.forward_button = create_da(
651 generate_image('media-seek-forward.png'),
652 lambda: self.do_seek(settings.seek_short))
653 buttonbox.add(self.forward_button)
655 self.fforward_button = create_da(
656 generate_image('media-skip-forward.png'),
657 lambda: self.do_seek(settings.seek_long),
658 generate_image(gtk.STOCK_GOTO_LAST, True),
659 player.playlist.next)
660 buttonbox.add(self.fforward_button)
662 self.bookmarks_button = create_da(
663 generate_image('bookmark-new.png'),
664 player.add_bookmark_at_current_position,
665 generate_image(gtk.STOCK_JUMP_TO, True),
666 lambda *args: self.notify('select-current-item-request'))
667 buttonbox.add(self.bookmarks_button)
668 self.set_controls_sensitivity(False)
669 main_vbox.pack_start(buttonbox, False, False)
671 if util.platform == util.MAEMO:
672 self.volume = hildon.VVolumebar()
673 self.volume.set_property('can-focus', False)
674 self.volume.connect('level_changed', self.volume_changed_hildon)
675 self.volume.connect('mute_toggled', self.mute_toggled)
676 self.__gui_root.main_window.connect( 'key-press-event',
677 self.on_key_press )
678 self.pack_start(self.volume, False, True)
680 # Add a button to pop out the volume bar
681 self.volume_button = gtk.ToggleButton('')
682 image(self.volume_button, 'media-speaker.png')
683 self.volume_button.connect('clicked', self.toggle_volumebar)
684 self.volume.connect(
685 'show', lambda x: self.volume_button.set_active(True))
686 self.volume.connect(
687 'hide', lambda x: self.volume_button.set_active(False))
688 buttonbox.add(self.volume_button)
689 self.volume_button.show()
691 # Disable focus for all widgets, so we can use the cursor
692 # keys + enter to directly control our media player, which
693 # is handled by "key-press-event"
694 for w in (
695 self.rrewind_button, self.rewind_button,
696 self.play_pause_button, self.forward_button,
697 self.fforward_button, self.progress,
698 self.bookmarks_button, self.volume_button, ):
699 w.unset_flags(gtk.CAN_FOCUS)
700 else:
701 self.volume = gtk.VolumeButton()
702 self.volume.connect('value-changed', self.volume_changed_gtk)
703 buttonbox.add(self.volume)
704 self.volume.show()
706 self.set_volume(settings.volume)
708 def set_controls_sensitivity(self, sensitive):
709 for button in self.forward_button, self.rewind_button, \
710 self.fforward_button, self.rrewind_button:
712 button.set_sensitive(sensitive)
714 # the play/pause button should always be available except
715 # for when the player starts without a file
716 self.play_pause_button.set_sensitive(True)
718 def on_dual_action_setting_changed( self, *args ):
719 for button in self.forward_button, self.rewind_button, \
720 self.fforward_button, self.rrewind_button, \
721 self.bookmarks_button:
723 button.set_longpress_enabled( settings.enable_dual_action_btn )
724 button.set_duration( settings.dual_action_button_delay )
726 def on_key_press(self, widget, event):
727 if util.platform == util.MAEMO:
728 if event.keyval == gtk.keysyms.F7: #plus
729 self.set_volume( min( 1, self.get_volume() + 0.10 ))
730 elif event.keyval == gtk.keysyms.F8: #minus
731 self.set_volume( max( 0, self.get_volume() - 0.10 ))
732 elif event.keyval == gtk.keysyms.Left: # seek back
733 self.do_seek( -1 * settings.seek_long )
734 elif event.keyval == gtk.keysyms.Right: # seek forward
735 self.do_seek( settings.seek_long )
736 elif event.keyval == gtk.keysyms.Return: # play/pause
737 self.on_btn_play_pause_clicked()
739 # The following two functions get and set the
740 # volume from the volume control widgets.
741 def get_volume(self):
742 if util.platform == util.MAEMO:
743 return self.volume.get_level()/100.0
744 else:
745 return self.volume.get_value()
747 def set_volume(self, vol):
748 """ vol is a float from 0 to 1 """
749 assert 0 <= vol <= 1
750 if util.platform == util.MAEMO:
751 self.volume.set_level(vol*100.0)
752 else:
753 self.volume.set_value(vol)
755 def __set_volume_hide_timer(self, timeout, force_show=False):
756 if force_show or self.volume_button.get_active():
757 self.volume.show()
758 if self.volume_timer_id is not None:
759 gobject.source_remove(self.volume_timer_id)
760 self.volume_timer_id = None
762 self.volume_timer_id = gobject.timeout_add(
763 1000 * timeout, self.__volume_hide_callback )
765 def __volume_hide_callback(self):
766 self.volume_timer_id = None
767 self.volume.hide()
768 return False
770 def toggle_volumebar(self, widget=None):
771 if self.volume_timer_id is None:
772 self.__set_volume_hide_timer(5)
773 else:
774 self.__volume_hide_callback()
776 def volume_changed_gtk(self, widget, new_value=0.5):
777 settings.volume = new_value
779 def volume_changed_hildon(self, widget):
780 self.__set_volume_hide_timer( 4, force_show=True )
781 settings.volume = widget.get_level()/100.0
783 def mute_toggled(self, widget):
784 if widget.get_mute():
785 settings.volume = 0
786 else:
787 settings.volume = widget.get_level()/100.0
789 def on_player_stopped(self):
790 self.stop_progress_timer()
791 self.set_controls_sensitivity(False)
792 image(self.play_pause_button, 'media-playback-start.png')
794 def on_player_playing(self):
795 self.start_progress_timer()
796 image(self.play_pause_button, 'media-playback-pause.png')
797 self.set_controls_sensitivity(True)
799 def on_player_new_track(self):
800 for widget in [self.title_label,self.artist_label,self.album_label]:
801 widget.set_markup('')
802 widget.hide()
804 self.cover_art.hide()
805 self.has_coverart = False
807 def on_player_new_metadata(self):
808 metadata = player.playlist.get_file_metadata()
809 self.set_metadata(metadata)
811 if not player.playing:
812 text, position = player.get_formatted_position()
813 estimated_length = metadata.get('length', 0)
814 self.set_progress_callback( position, estimated_length )
816 def on_player_paused( self, position, duration ):
817 self.stop_progress_timer() # This should save some power
818 self.set_progress_callback( position, duration )
819 image(self.play_pause_button, 'media-playback-start.png')
821 def on_player_end_of_playlist(self, loop):
822 pass
824 def reset_progress(self):
825 self.progress.set_fraction(0)
826 self.set_progress_callback(0,0)
828 def set_progress_callback(self, time_elapsed, total_time):
829 """ times must be in nanoseconds """
830 time_string = "%s / %s" % ( util.convert_ns(time_elapsed),
831 util.convert_ns(total_time) )
832 self.progress.set_text( time_string )
833 fraction = float(time_elapsed) / float(total_time) if total_time else 0
834 self.progress.set_fraction( fraction )
836 def on_progressbar_changed(self, widget, event):
837 if ( not settings.progress_locked and
838 event.type == gtk.gdk.BUTTON_PRESS and event.button == 1 ):
839 new_fraction = event.x/float(widget.get_allocation().width)
840 resp = player.do_seek(percent=new_fraction)
841 if resp:
842 # Preemptively update the progressbar to make seeking smoother
843 self.set_progress_callback( *resp )
845 def on_btn_play_pause_clicked(self, widget=None):
846 player.play_pause_toggle()
848 def progress_timer_callback( self ):
849 if player.playing and not player.seeking:
850 pos_int, dur_int = player.get_position_duration()
851 # This prevents bogus values from being set while seeking
852 if ( pos_int > 10**9 ) and ( dur_int > 10**9 ):
853 self.set_progress_callback( pos_int, dur_int )
854 return True
856 def start_progress_timer( self ):
857 if self.progress_timer_id is not None:
858 self.stop_progress_timer()
860 self.progress_timer_id = gobject.timeout_add(
861 1000, self.progress_timer_callback )
863 def stop_progress_timer( self ):
864 if self.progress_timer_id is not None:
865 gobject.source_remove( self.progress_timer_id )
866 self.progress_timer_id = None
868 def get_coverart_size( self ):
869 if util.platform == util.MAEMO:
870 if self.__gui_root.fullscreen:
871 size = coverart_sizes['maemo fullscreen']
872 else:
873 size = coverart_sizes['maemo']
874 else:
875 size = coverart_sizes['normal']
877 return size, size
879 def set_coverart( self, pixbuf ):
880 self.cover_art.set_from_pixbuf(pixbuf)
881 self.cover_art.show()
882 self.has_coverart = True
884 def set_metadata( self, tag_message ):
885 tags = { 'title': self.title_label, 'artist': self.artist_label,
886 'album': self.album_label }
888 # set the coverart
889 if tag_message.has_key('image') and tag_message['image'] is not None:
890 value = tag_message['image']
892 pbl = gtk.gdk.PixbufLoader()
893 try:
894 pbl.write(value)
895 pbl.close()
897 x, y = self.get_coverart_size()
898 pixbuf = pbl.get_pixbuf()
899 pixbuf = pixbuf.scale_simple( x, y, gtk.gdk.INTERP_BILINEAR )
900 self.set_coverart(pixbuf)
901 except Exception, e:
902 self.__log.exception('Error setting coverart...')
904 # set the text metadata
905 for tag,value in tag_message.iteritems():
906 if tags.has_key(tag) and value is not None and value.strip():
907 tags[tag].set_markup('<big>'+value+'</big>')
908 tags[tag].set_alignment( 0.5*int(not self.has_coverart), 0.5)
909 tags[tag].show()
911 if tag == 'title':
912 # make the title bold
913 tags[tag].set_markup('<b><big>'+value+'</big></b>')
915 if util.platform != util.MAEMO:
916 value += ' - Panucci'
918 self.__gui_root.main_window.set_title( value )
920 def do_seek(self, seek_amount):
921 resp = player.do_seek(from_current=seek_amount*10**9)
922 if resp:
923 # Preemptively update the progressbar to make seeking smoother
924 self.set_progress_callback( *resp )
928 ##################################################
929 # PlaylistTab
930 ##################################################
931 class PlaylistTab(gtk.VBox):
932 def __init__(self, main_window):
933 gtk.VBox.__init__(self)
934 self.__log = logging.getLogger('panucci.panucci.BookmarksWindow')
935 self.main = main_window
937 self.__model = gtk.TreeStore(
938 # uid, name, position
939 gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING )
941 self.set_spacing(5)
942 self.treeview = gtk.TreeView()
943 self.treeview.set_model(self.__model)
944 self.treeview.set_headers_visible(True)
945 tree_selection = self.treeview.get_selection()
946 tree_selection.set_mode(gtk.SELECTION_MULTIPLE)
947 tree_selection.connect('changed', self.tree_selection_changed)
949 # The tree lines look nasty on maemo
950 if util.platform == util.LINUX:
951 self.treeview.set_enable_tree_lines(True)
952 self.update_model()
954 ncol = gtk.TreeViewColumn(_('Name'))
955 ncell = gtk.CellRendererText()
956 ncell.set_property('ellipsize', pango.ELLIPSIZE_END)
957 ncell.set_property('editable', True)
958 ncell.connect('edited', self.label_edited)
959 ncol.set_expand(True)
960 ncol.pack_start(ncell)
961 ncol.add_attribute(ncell, 'text', 1)
963 tcol = gtk.TreeViewColumn(_('Position'))
964 tcell = gtk.CellRendererText()
965 tcol.pack_start(tcell)
966 tcol.add_attribute(tcell, 'text', 2)
968 self.treeview.append_column(ncol)
969 self.treeview.append_column(tcol)
970 self.treeview.connect('drag-data-received', self.drag_data_recieved)
971 self.treeview.connect('drag_data_get', self.drag_data_get_data)
973 treeview_targets = [
974 ( 'playlist_row_data', gtk.TARGET_SAME_WIDGET, 0 ) ]
976 self.treeview.enable_model_drag_source(
977 gtk.gdk.BUTTON1_MASK, treeview_targets, gtk.gdk.ACTION_COPY )
979 self.treeview.enable_model_drag_dest(
980 treeview_targets, gtk.gdk.ACTION_COPY )
982 sw = gtk.ScrolledWindow()
983 sw.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
984 sw.set_shadow_type(gtk.SHADOW_IN)
985 sw.add(self.treeview)
986 self.add(sw)
988 self.hbox = gtk.HBox()
990 self.add_button = gtk.Button(gtk.STOCK_NEW)
991 self.add_button.set_use_stock(True)
992 set_stock_button_text( self.add_button, _('Add File') )
993 self.add_button.connect('clicked', self.add_file)
994 self.hbox.pack_start(self.add_button, True, True)
996 self.dir_button = gtk.Button(gtk.STOCK_OPEN)
997 self.dir_button.set_use_stock(True)
998 set_stock_button_text( self.dir_button, _('Add Directory') )
999 self.dir_button.connect('clicked', self.add_directory)
1000 self.hbox.pack_start(self.dir_button, True, True)
1002 self.remove_button = gtk.Button(gtk.STOCK_REMOVE)
1003 self.remove_button.set_use_stock(True)
1004 self.remove_button.connect('clicked', self.remove_bookmark)
1005 self.hbox.pack_start(self.remove_button, True, True)
1007 self.jump_button = gtk.Button(gtk.STOCK_JUMP_TO)
1008 self.jump_button.set_use_stock(True)
1009 self.jump_button.connect('clicked', self.jump_bookmark)
1010 self.hbox.pack_start(self.jump_button, True, True)
1012 self.info_button = gtk.Button()
1013 self.info_button.add(
1014 gtk.image_new_from_stock(gtk.STOCK_INFO, gtk.ICON_SIZE_BUTTON))
1015 self.info_button.connect('clicked', self.show_playlist_item_details)
1016 self.hbox.pack_start(self.info_button, True, True)
1018 self.pack_start(self.hbox, False, True)
1020 player.playlist.register( 'file_queued',
1021 lambda x,y,z: self.update_model() )
1022 player.playlist.register( 'bookmark_added', self.on_bookmark_added )
1024 self.show_all()
1026 def tree_selection_changed(self, treeselection):
1027 count = treeselection.count_selected_rows()
1028 self.remove_button.set_sensitive(count > 0)
1029 self.jump_button.set_sensitive(count == 1)
1030 self.info_button.set_sensitive(count == 1)
1032 def drag_data_get_data(
1033 self, treeview, context, selection, target_id, timestamp):
1035 treeselection = treeview.get_selection()
1036 model, iter = treeselection.get_selected()
1037 # only allow moving around top-level parents
1038 if model.iter_parent(iter) is None:
1039 # send the path of the selected row
1040 data = model.get_string_from_iter(iter)
1041 selection.set(selection.target, 8, data)
1042 else:
1043 self.__log.debug("Can't move children...")
1045 def drag_data_recieved(
1046 self, treeview, context, x, y, selection, info, timestamp):
1048 drop_info = treeview.get_dest_row_at_pos(x, y)
1050 # TODO: If user drags the row past the last row, drop_info is None
1051 # I'm not sure if it's safe to simply assume that None is
1052 # euqivalent to the last row...
1053 if None not in [ drop_info and selection.data ]:
1054 model = treeview.get_model()
1055 path, position = drop_info
1057 from_iter = model.get_iter_from_string(selection.data)
1059 # make sure the to_iter doesn't have a parent
1060 to_iter = model.get_iter(path)
1061 if model.iter_parent(to_iter) is not None:
1062 to_iter = model.iter_parent(to_iter)
1064 from_row = model.get_path(from_iter)[0]
1065 to_row = path[0]
1067 if ( position == gtk.TREE_VIEW_DROP_BEFORE or
1068 position == gtk.TREE_VIEW_DROP_INTO_OR_BEFORE ):
1069 model.move_before( from_iter, to_iter )
1070 to_row = to_row - 1 if from_row < to_row else to_row
1071 elif ( position == gtk.TREE_VIEW_DROP_AFTER or
1072 position == gtk.TREE_VIEW_DROP_INTO_OR_AFTER ):
1073 model.move_after( from_iter, to_iter )
1074 to_row = to_row + 1 if from_row > to_row else to_row
1075 else:
1076 self.__log.debug('Drop not supported: %s', position)
1078 # don't do anything if we're not actually moving rows around
1079 if from_row != to_row:
1080 player.playlist.move_item( from_row, to_row )
1082 else:
1083 self.__log.debug('No drop_data or selection.data available')
1085 def update_model(self):
1086 plist = player.playlist
1087 path_info = self.treeview.get_path_at_pos(0,0)
1088 path = path_info[0] if path_info is not None else None
1090 self.__model.clear()
1092 # build the tree
1093 for item, data in plist.get_playlist_item_ids():
1094 parent = self.__model.append(None, (item, data.get('title'), None))
1096 for bid, bname, bpos in plist.get_bookmarks_from_item_id( item ):
1097 nice_bpos = util.convert_ns(bpos)
1098 self.__model.append( parent, (bid, bname, nice_bpos) )
1100 self.treeview.expand_all()
1102 if path is not None:
1103 self.treeview.scroll_to_cell(path)
1105 def label_edited(self, cellrenderer, path, new_text):
1106 iter = self.__model.get_iter(path)
1107 old_text = self.__model.get_value(iter, 1)
1109 if new_text.strip() and old_text != new_text:
1110 # this loop will only run once, because only one cell can be
1111 # edited at a time, we use it to get the item and bookmark ids
1112 for m, bkmk_id, biter, item_id, iiter in self.__cur_selection():
1113 self.__model.set_value(iter, 1, new_text)
1114 player.playlist.update_bookmark(
1115 item_id, bkmk_id, name=new_text )
1116 else:
1117 self.__model.set_value(iter, 1, old_text)
1119 def on_bookmark_added(self, parent_id, bookmark_name, position):
1120 util.notify(_('Bookmark added: %s') % bookmark_name)
1121 self.update_model()
1123 def add_file(self, widget):
1124 filename = get_file_from_filechooser(self.main.main_window)
1125 if filename is not None:
1126 player.playlist.append(filename)
1128 def add_directory(self, widget):
1129 directory = get_file_from_filechooser(
1130 self.main.main_window, folder=True )
1131 if directory is not None:
1132 player.playlist.load_directory(directory, append=True)
1134 def __cur_selection(self):
1135 selection = self.treeview.get_selection()
1136 model, bookmark_paths = selection.get_selected_rows()
1138 # Convert the paths to gtk.TreeRowReference objects, because we
1139 # might modify the model while this generator is running
1140 bookmark_refs = [gtk.TreeRowReference(model, p) for p in bookmark_paths]
1142 for reference in bookmark_refs:
1143 bookmark_iter = model.get_iter(reference.get_path())
1144 item_iter = model.iter_parent(bookmark_iter)
1146 # bookmark_iter is actually an item_iter
1147 if item_iter is None:
1148 item_iter = bookmark_iter
1149 item_id = model.get_value(item_iter, 0)
1150 bookmark_id, bookmark_iter = None, None
1151 else:
1152 bookmark_id = model.get_value(bookmark_iter, 0)
1153 item_id = model.get_value(item_iter, 0)
1155 yield model, bookmark_id, bookmark_iter, item_id, item_iter
1157 def remove_bookmark(self, w):
1158 for model, bkmk_id, bkmk_iter, item_id, item_iter in self.__cur_selection():
1159 player.playlist.remove_bookmark( item_id, bkmk_id )
1160 if bkmk_iter is not None:
1161 model.remove(bkmk_iter)
1162 elif item_iter is not None:
1163 model.remove(item_iter)
1165 def select_current_item(self):
1166 model = self.treeview.get_model()
1167 selection = self.treeview.get_selection()
1168 current_item_id = str(player.playlist.get_current_item())
1169 for row in iter(model):
1170 if model.get_value(row.iter, 0) == current_item_id:
1171 selection.unselect_all()
1172 self.treeview.set_cursor(row.path)
1173 self.treeview.scroll_to_cell(row.path, use_align=True)
1174 break
1176 def show_playlist_item_details(self, w):
1177 selection = self.treeview.get_selection()
1178 if selection.count_selected_rows() == 1:
1179 selected = self.__cur_selection().next()
1180 model, bkmk_id, bkmk_iter, item_id, item_iter = selected
1181 playlist_item = player.playlist.get_item_by_id(item_id)
1182 PlaylistItemDetails(self.main, playlist_item)
1184 def jump_bookmark(self, w):
1185 selected = list(self.__cur_selection())
1186 if len(selected) == 1:
1187 # It should be guranteed by the fact that we only enable the
1188 # "Jump to" button when the selection count equals 1.
1189 model, bkmk_id, bkmk_iter, item_id, item_iter = selected.pop(0)
1190 player.playlist.load_from_bookmark_id(item_id, bkmk_id)
1192 # FIXME: The player/playlist should be able to take care of this
1193 if not player.playing:
1194 player.play()
1197 ##################################################
1198 # PlaylistItemDetails
1199 ##################################################
1200 class PlaylistItemDetails(gtk.Dialog):
1201 def __init__(self, main, playlist_item):
1202 gtk.Dialog.__init__( self, _('Playlist item details'),
1203 main.main_window, gtk.DIALOG_MODAL,
1204 (gtk.STOCK_CLOSE, gtk.RESPONSE_OK))
1206 self.main = main
1207 self.fill(playlist_item)
1208 self.set_has_separator(False)
1209 self.set_resizable(False)
1210 self.show_all()
1211 self.run()
1212 self.destroy()
1214 def fill(self, playlist_item):
1215 t = gtk.Table(10, 2)
1216 self.vbox.pack_start(t, expand=False)
1218 metadata = playlist_item.metadata
1220 t.attach(gtk.Label(_('Custom title:')), 0, 1, 0, 1)
1221 t.attach(gtk.Label(_('ID:')), 0, 1, 1, 2)
1222 t.attach(gtk.Label(_('Playlist ID:')), 0, 1, 2, 3)
1223 t.attach(gtk.Label(_('Filepath:')), 0, 1, 3, 4)
1225 row_num = 4
1226 for key in metadata:
1227 if metadata[key] is not None:
1228 t.attach( gtk.Label(key.capitalize()+':'),
1229 0, 1, row_num, row_num+1 )
1230 row_num += 1
1232 t.foreach(lambda x, y: x.set_alignment(1, 0.5), None)
1233 t.foreach(lambda x, y: x.set_markup('<b>%s</b>' % x.get_label()), None)
1235 t.attach(gtk.Label(playlist_item.title or _('<not modified>')),1,2,0,1)
1236 t.attach(gtk.Label(str(playlist_item)), 1, 2, 1, 2)
1237 t.attach(gtk.Label(playlist_item.playlist_id), 1, 2, 2, 3)
1238 t.attach(gtk.Label(playlist_item.filepath), 1, 2, 3, 4)
1240 row_num = 4
1241 for key in metadata:
1242 value = metadata[key]
1243 if key == 'length':
1244 value = util.convert_ns(value)
1245 if metadata[key] is not None:
1246 t.attach( gtk.Label( str(value) or _('<not set>')),
1247 1, 2, row_num, row_num+1)
1248 row_num += 1
1250 t.foreach(lambda x, y: x.get_alignment() == (0.5, 0.5) and \
1251 x.set_alignment(0, 0.5), None)
1253 t.set_border_width(8)
1254 t.set_row_spacings(4)
1255 t.set_col_spacings(8)
1257 l = gtk.ListStore(str, str)
1258 t = gtk.TreeView(l)
1259 cr = gtk.CellRendererText()
1260 cr.set_property('ellipsize', pango.ELLIPSIZE_END)
1261 c = gtk.TreeViewColumn(_('Title'), cr, text=0)
1262 c.set_expand(True)
1263 t.append_column(c)
1264 c = gtk.TreeViewColumn(_('Time'), gtk.CellRendererText(), text=1)
1265 t.append_column(c)
1266 playlist_item.load_bookmarks()
1267 for bookmark in playlist_item.bookmarks:
1268 l.append([bookmark.bookmark_name, \
1269 util.convert_ns(bookmark.seek_position)])
1271 sw = gtk.ScrolledWindow()
1272 sw.set_shadow_type(gtk.SHADOW_IN)
1273 sw.add(t)
1274 sw.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
1275 e = gtk.Expander(_('Bookmarks'))
1276 e.add(sw)
1277 self.vbox.pack_start(e)
1280 def run(filename=None):
1281 PanucciGUI( filename )
1282 gtk.main()
1284 if __name__ == '__main__':
1285 log.error( 'Use the "panucci" executable to run this program.' )
1286 log.error( 'Exiting...' )
1287 sys.exit(1)