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
37 from panucci
import widgets
38 from panucci
import util
39 from panucci
import ossohelper
41 log
= logging
.getLogger('panucci.panucci')
47 if util
.platform
.MAEMO
:
48 log
.critical( 'Using GTK widgets, install "python2.5-hildon" '
49 'for this to work properly.' )
51 from panucci
.simplegconf
import gconf
52 from panucci
.settings
import settings
53 from panucci
.player
import player
54 from panucci
.dbusinterface
import interface
55 from panucci
.services
import ObservableService
57 about_name
= 'Panucci'
58 about_text
= _('Resuming audiobook and podcast player')
59 about_authors
= ['Thomas Perl', 'Nick (nikosapi)', 'Matthew Taylor']
60 about_website
= 'http://panucci.garage.maemo.org/'
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):
75 image
= gtk
.image_new_from_stock(
76 filename
, gtk
.icon_size_from_name('panucci-button') )
78 filename
= util
.find_image(filename
)
79 if filename
is not None:
80 image
= gtk
.image_new_from_file(filename
)
82 if util
.platform
.MAEMO
:
83 image
.set_padding(20, 20)
85 image
.set_padding(5, 5)
89 def image(widget
, filename
, is_stock
=False):
90 child
= widget
.get_child()
93 image
= generate_image(filename
, is_stock
)
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
)
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
)
132 if response
== gtk
.RESPONSE_YES
:
134 elif response
== gtk
.RESPONSE_NO
:
136 elif response
in [gtk
.RESPONSE_CANCEL
, gtk
.RESPONSE_DELETE_EVENT
]:
139 def get_file_from_filechooser(
140 toplevel_window
, folder
=False, save_file
=False, save_to
=None):
143 open_action
= gtk
.FILE_CHOOSER_ACTION_SELECT_FOLDER
145 open_action
= gtk
.FILE_CHOOSER_ACTION_OPEN
147 if util
.platform
.MAEMO
:
149 args
= ( toplevel_window
, gtk
.FILE_CHOOSER_ACTION_SAVE
)
151 args
= ( toplevel_window
, open_action
)
153 dlg
= hildon
.FileChooserDialog( *args
)
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
)) )
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()
184 def set_stock_button_text( button
, text
):
185 alignment
= button
.get_child()
186 hbox
= alignment
.get_child()
187 image
, label
= hbox
.get_children()
190 ##################################################
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
)
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 if util
.platform
.MAEMO
:
230 window
.set_menu(self
.create_menu())
231 window
.add(self
.__player
_tab
)
233 menu_vbox
= gtk
.VBox()
234 menu_vbox
.set_spacing(0)
235 window
.add(menu_vbox
)
236 menu_bar
= gtk
.MenuBar()
237 root_menu
= gtk
.MenuItem('Panucci')
238 root_menu
.set_submenu(self
.create_menu())
239 menu_bar
.append(root_menu
)
240 menu_vbox
.pack_start(menu_bar
, False, False, 0)
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',
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 self
.pickle_file_conversion()
269 player
.init(filepath
=filename
)
271 def create_menu(self
):
275 menu_open
= gtk
.ImageMenuItem(_('Open playlist'))
277 gtk
.image_new_from_stock(gtk
.STOCK_OPEN
, gtk
.ICON_SIZE_MENU
))
278 menu_open
.connect("activate", self
.open_file_callback
)
279 menu
.append(menu_open
)
281 # the recent files menu
282 self
.menu_recent
= gtk
.MenuItem(_('Open recent playlist'))
283 menu
.append(self
.menu_recent
)
284 self
.create_recent_files_menu()
286 menu
.append(gtk
.SeparatorMenuItem())
288 menu_save
= gtk
.ImageMenuItem(_('Save current playlist'))
290 gtk
.image_new_from_stock(gtk
.STOCK_SAVE_AS
, gtk
.ICON_SIZE_MENU
))
291 menu_save
.connect("activate", self
.save_to_playlist_callback
)
292 menu
.append(menu_save
)
294 menu
.append(gtk
.SeparatorMenuItem())
296 # the settings sub-menu
297 menu_settings
= gtk
.MenuItem(_('Settings'))
298 menu
.append(menu_settings
)
300 menu_settings_sub
= gtk
.Menu()
301 menu_settings
.set_submenu(menu_settings_sub
)
303 menu_settings_enable_dual_action
= gtk
.CheckMenuItem(
304 _('Enable dual-action buttons') )
305 settings
.attach_checkbutton( menu_settings_enable_dual_action
,
306 'enable_dual_action_btn' )
307 menu_settings_sub
.append(menu_settings_enable_dual_action
)
309 if util
.platform
.MAEMO
:
310 menu_settings_enable_hw_decoding
= gtk
.CheckMenuItem(
311 _('Enable hardware decoding') )
312 settings
.attach_checkbutton( menu_settings_enable_hw_decoding
,
313 'enable_hardware_decoding' )
314 menu_settings_sub
.append(menu_settings_enable_hw_decoding
)
316 menu_settings_lock_progress
= gtk
.CheckMenuItem(_('Lock Progress Bar'))
317 settings
.attach_checkbutton( menu_settings_lock_progress
,
319 menu_settings_sub
.append(menu_settings_lock_progress
)
321 menu_about
= gtk
.ImageMenuItem(gtk
.STOCK_ABOUT
)
322 menu_about
.connect("activate", self
.show_about
, self
.main_window
)
323 menu
.append(menu_about
)
325 menu
.append(gtk
.SeparatorMenuItem())
327 menu_quit
= gtk
.ImageMenuItem(gtk
.STOCK_QUIT
)
328 menu_quit
.connect("activate", self
.destroy
)
329 menu
.append(menu_quit
)
333 def create_recent_files_menu( self
):
334 max_files
= settings
.max_recent_files
335 self
.recent_files
= player
.playlist
.get_recent_files(max_files
)
336 menu_recent_sub
= gtk
.Menu()
338 temp_playlist
= os
.path
.expanduser(settings
.temp_playlist
)
340 if len(self
.recent_files
) > 0:
341 for f
in self
.recent_files
:
342 # don't include the temporary playlist in the file list
343 if f
== temp_playlist
: continue
344 # don't include non-existant files
345 if not os
.path
.exists( f
): continue
346 filename
, extension
= os
.path
.splitext(os
.path
.basename(f
))
347 menu_item
= gtk
.MenuItem( filename
.replace('_', ' '))
348 menu_item
.connect('activate', self
.on_recent_file_activate
, f
)
349 menu_recent_sub
.append(menu_item
)
351 menu_item
= gtk
.MenuItem(_('No recent files available.'))
352 menu_item
.set_sensitive(False)
353 menu_recent_sub
.append(menu_item
)
355 self
.menu_recent
.set_submenu(menu_recent_sub
)
357 def destroy(self
, widget
):
361 def show_main_window(self
):
362 self
.main_window
.present()
364 def check_queue(self
):
365 """ Makes sure the queue is saved if it has been modified
366 True means a new file can be opened
367 False means the user does not want to continue """
369 if not self
.__ignore
_queue
_check
and player
.playlist
.queue_modified
:
371 self
.main_window
, _('Save current playlist'),
372 _('Current playlist has been modified'),
373 _('Opening a new file will replace the current playlist. ') +
374 _('Do you want to save it before creating a new one?'),
375 affirmative_button
=gtk
.STOCK_SAVE
,
376 negative_button
=_('Discard changes'))
378 self
.__log
.debug('Response to "Save Queue?": %s', response
)
383 return self
.save_to_playlist_callback()
391 def open_file_callback(self
, widget
=None):
392 if self
.check_queue():
393 # set __ingnore__queue_check because we already did the check
394 self
.__ignore
_queue
_check
= True
395 filename
= get_file_from_filechooser(self
.main_window
)
396 if filename
is not None:
397 self
._play
_file
(filename
)
399 self
.__ignore
_queue
_check
= False
401 def save_to_playlist_callback(self
, widget
=None):
402 filename
= get_file_from_filechooser(
403 self
.main_window
, save_file
=True, save_to
='playlist.m3u' )
408 if os
.path
.isfile(filename
):
409 response
= dialog( self
.main_window
, _('File already exists'),
410 _('File already exists'),
411 _('The file %s already exists. You can choose another name or '
412 'overwrite the existing file.') % os
.path
.basename(filename
),
413 affirmative_button
=gtk
.STOCK_SAVE
,
414 negative_button
=_('Rename file'))
422 return self
.save_to_playlist_callback()
424 ext
= util
.detect_filetype(filename
)
425 if not player
.playlist
.save_to_new_playlist(filename
, ext
):
426 util
.notify(_('Error saving playlist...'))
431 def __get_fullscreen(self
):
432 return self
.__window
_fullscreen
434 def __set_fullscreen(self
, value
):
435 if value
!= self
.__window
_fullscreen
:
437 self
.main_window
.fullscreen()
439 self
.main_window
.unfullscreen()
441 self
.__window
_fullscreen
= value
442 player
.playlist
.send_metadata()
444 fullscreen
= property( __get_fullscreen
, __set_fullscreen
)
446 def on_key_press(self
, widget
, event
):
447 if util
.platform
.MAEMO
:
448 if event
.keyval
== gtk
.keysyms
.F6
:
449 self
.fullscreen
= not self
.fullscreen
451 def on_recent_file_activate(self
, widget
, filepath
):
452 self
._play
_file
(filepath
)
454 def on_file_queued(self
, filepath
, success
, notify
):
456 filename
= os
.path
.basename(filepath
)
459 util
.notify( '%s added successfully.' % filename
))
462 util
.notify( 'Error adding %s to the queue.' % filename
))
464 def show_about(self
, w
, win
):
465 dialog
= gtk
.AboutDialog()
466 dialog
.set_website(about_website
)
467 dialog
.set_website_label(about_website
)
468 dialog
.set_name(about_name
)
469 dialog
.set_authors(about_authors
)
470 dialog
.set_comments(about_text
)
471 dialog
.set_version(app_version
)
475 def _play_file(self
, filename
, pause_on_load
=False):
476 player
.playlist
.load( os
.path
.abspath(filename
) )
478 if player
.playlist
.is_empty
:
481 def handle_headset_button(self
, event
, button
):
482 if event
== 'ButtonPressed' and button
== 'phone':
483 player
.play_pause_toggle()
485 def __set_anti_blank_timer(self
, allow_blanking
):
486 if util
.platform
.MAEMO
:
487 if allow_blanking
and self
.__anti
_blank
_timer
is not None:
488 self
.__log
.info('Screen blanking enabled.')
489 gobject
.source_remove(self
.__anti
_blank
_timer
)
490 self
.__anti
_blank
_timer
= None
491 elif not allow_blanking
and self
.__anti
_blank
_timer
is None:
492 self
.__log
.info('Attempting to disable screen blanking.')
493 self
.__anti
_blank
_timer
= gobject
.timeout_add(
494 1000 * 59, util
.poke_backlight
)
496 self
.__log
.info('Blanking controls are for Maemo only.')
498 def __select_current_item( self
):
499 # Select the currently playing track in the playlist tab
500 # and switch to it (so we can edit bookmarks, etc.. there)
501 self
.__playlist
_tab
.select_current_item()
502 self
.playlist_window
.show()
504 def pickle_file_conversion(self
):
505 pickle_file
= os
.path
.expanduser('~/.rmp-bookmarks')
506 if os
.path
.isfile(pickle_file
):
507 import pickle_converter
510 util
.notify( _('Converting old pickle format to SQLite.') ))
511 self
.__log
.info( util
.notify( _('This may take a while...') ))
513 if pickle_converter
.load_pickle_file(pickle_file
):
515 util
.notify( _('Pickle file converted successfully.') ))
517 self
.__log
.error( util
.notify(
518 _('Error converting pickle file, check your log...') ))
520 ##################################################
522 ##################################################
523 class PlayerTab(ObservableService
, gtk
.HBox
):
524 """ The tab that holds the player elements """
526 signals
= [ 'select-current-item-request', ]
528 def __init__(self
, gui_root
):
529 self
.__log
= logging
.getLogger('panucci.panucci.PlayerTab')
530 self
.__gui
_root
= gui_root
532 gtk
.HBox
.__init
__(self
)
533 ObservableService
.__init
__(self
, self
.signals
, self
.__log
)
536 self
.progress_timer_id
= None
537 self
.volume_timer_id
= None
539 self
.recent_files
= []
540 self
.make_player_tab()
541 self
.has_coverart
= False
542 self
.set_volume(settings
.volume
)
544 settings
.register( 'enable_dual_action_btn_changed',
545 self
.on_dual_action_setting_changed
)
546 settings
.register( 'dual_action_button_delay_changed',
547 self
.on_dual_action_setting_changed
)
548 settings
.register( 'volume_changed', self
.set_volume
)
549 settings
.register( 'scrolling_labels_changed', lambda v
:
550 setattr( self
.title_label
, 'scrolling', v
) )
552 player
.register( 'stopped', self
.on_player_stopped
)
553 player
.register( 'playing', self
.on_player_playing
)
554 player
.register( 'paused', self
.on_player_paused
)
555 player
.playlist
.register( 'end-of-playlist',
556 self
.on_player_end_of_playlist
)
557 player
.playlist
.register( 'new-track-loaded',
558 self
.on_player_new_track
)
559 player
.playlist
.register( 'new-metadata-available',
560 self
.on_player_new_metadata
)
562 def make_player_tab(self
):
563 main_vbox
= gtk
.VBox()
564 main_vbox
.set_spacing(6)
566 self
.pack_start(main_vbox
, True, True)
568 # a hbox to hold the cover art and metadata vbox
569 metadata_hbox
= gtk
.HBox()
570 metadata_hbox
.set_spacing(6)
571 main_vbox
.pack_start(metadata_hbox
, True, False)
573 self
.cover_art
= gtk
.Image()
574 metadata_hbox
.pack_start( self
.cover_art
, False, False )
576 # vbox to hold metadata
577 metadata_vbox
= gtk
.VBox()
578 metadata_vbox
.set_spacing(8)
579 empty_label
= gtk
.Label()
580 metadata_vbox
.pack_start(empty_label
, True, True)
581 self
.artist_label
= gtk
.Label('')
582 self
.artist_label
.set_ellipsize(pango
.ELLIPSIZE_END
)
583 metadata_vbox
.pack_start(self
.artist_label
, False, False)
584 self
.album_label
= gtk
.Label('')
585 self
.album_label
.set_ellipsize(pango
.ELLIPSIZE_END
)
586 metadata_vbox
.pack_start(self
.album_label
, False, False)
587 self
.title_label
= widgets
.ScrollingLabel( '',
590 delay_btwn_scrolls
=5000,
592 self
.title_label
.scrolling
= settings
.scrolling_labels
593 metadata_vbox
.pack_start(self
.title_label
, False, False)
594 empty_label
= gtk
.Label()
595 metadata_vbox
.pack_start(empty_label
, True, True)
596 metadata_hbox
.pack_start( metadata_vbox
, True, True )
598 progress_eventbox
= gtk
.EventBox()
599 progress_eventbox
.set_events(gtk
.gdk
.BUTTON_PRESS_MASK
)
600 progress_eventbox
.connect(
601 'button-press-event', self
.on_progressbar_changed
)
602 self
.progress
= gtk
.ProgressBar()
603 # make the progress bar more "finger-friendly"
604 if util
.platform
.MAEMO
:
605 self
.progress
.set_size_request( -1, 50 )
606 progress_eventbox
.add(self
.progress
)
607 main_vbox
.pack_start( progress_eventbox
, False, False )
609 # make the button box
610 buttonbox
= gtk
.HBox()
612 # A wrapper to help create DualActionButtons with the right settings
613 create_da
= lambda a
, b
, c
=None, d
=None: widgets
.DualActionButton(
614 a
, b
, c
, d
, settings
.dual_action_button_delay
,
615 settings
.enable_dual_action_btn
)
617 self
.rrewind_button
= create_da(
618 generate_image('media-skip-backward.png'),
619 lambda: self
.do_seek(-1*settings
.seek_long
),
620 generate_image(gtk
.STOCK_GOTO_FIRST
, True),
621 player
.playlist
.prev
)
622 buttonbox
.add(self
.rrewind_button
)
624 self
.rewind_button
= create_da(
625 generate_image('media-seek-backward.png'),
626 lambda: self
.do_seek(-1*settings
.seek_short
))
627 buttonbox
.add(self
.rewind_button
)
629 self
.play_pause_button
= gtk
.Button('')
630 image(self
.play_pause_button
, 'media-playback-start.png')
631 self
.play_pause_button
.connect( 'clicked',
632 self
.on_btn_play_pause_clicked
)
633 self
.play_pause_button
.set_sensitive(False)
634 buttonbox
.add(self
.play_pause_button
)
636 self
.forward_button
= create_da(
637 generate_image('media-seek-forward.png'),
638 lambda: self
.do_seek(settings
.seek_short
))
639 buttonbox
.add(self
.forward_button
)
641 self
.fforward_button
= create_da(
642 generate_image('media-skip-forward.png'),
643 lambda: self
.do_seek(settings
.seek_long
),
644 generate_image(gtk
.STOCK_GOTO_LAST
, True),
645 player
.playlist
.next
)
646 buttonbox
.add(self
.fforward_button
)
648 self
.bookmarks_button
= create_da(
649 generate_image('bookmark-new.png'),
650 player
.add_bookmark_at_current_position
,
651 generate_image(gtk
.STOCK_JUMP_TO
, True),
652 lambda *args
: self
.notify('select-current-item-request'))
653 buttonbox
.add(self
.bookmarks_button
)
654 self
.set_controls_sensitivity(False)
655 main_vbox
.pack_start(buttonbox
, False, False)
657 if util
.platform
.MAEMO
:
658 self
.volume
= hildon
.VVolumebar()
659 self
.volume
.set_property('can-focus', False)
660 self
.volume
.connect('level_changed', self
.volume_changed_hildon
)
661 self
.volume
.connect('mute_toggled', self
.mute_toggled
)
662 self
.__gui
_root
.main_window
.connect( 'key-press-event',
664 self
.pack_start(self
.volume
, False, True)
666 # Add a button to pop out the volume bar
667 self
.volume_button
= gtk
.ToggleButton('')
668 image(self
.volume_button
, 'media-speaker.png')
669 self
.volume_button
.connect('clicked', self
.toggle_volumebar
)
671 'show', lambda x
: self
.volume_button
.set_active(True))
673 'hide', lambda x
: self
.volume_button
.set_active(False))
674 buttonbox
.add(self
.volume_button
)
675 self
.volume_button
.show()
677 # Disable focus for all widgets, so we can use the cursor
678 # keys + enter to directly control our media player, which
679 # is handled by "key-press-event"
681 self
.rrewind_button
, self
.rewind_button
,
682 self
.play_pause_button
, self
.forward_button
,
683 self
.fforward_button
, self
.progress
,
684 self
.bookmarks_button
, self
.volume_button
, ):
685 w
.unset_flags(gtk
.CAN_FOCUS
)
687 self
.volume
= gtk
.VolumeButton()
688 self
.volume
.connect('value-changed', self
.volume_changed_gtk
)
689 buttonbox
.add(self
.volume
)
692 self
.set_volume(settings
.volume
)
694 def set_controls_sensitivity(self
, sensitive
):
695 for button
in self
.forward_button
, self
.rewind_button
, \
696 self
.fforward_button
, self
.rrewind_button
:
698 button
.set_sensitive(sensitive
)
700 # the play/pause button should always be available except
701 # for when the player starts without a file
702 self
.play_pause_button
.set_sensitive(True)
704 def on_dual_action_setting_changed( self
, *args
):
705 for button
in self
.forward_button
, self
.rewind_button
, \
706 self
.fforward_button
, self
.rrewind_button
, \
707 self
.bookmarks_button
:
709 button
.set_longpress_enabled( settings
.enable_dual_action_btn
)
710 button
.set_duration( settings
.dual_action_button_delay
)
712 def on_key_press(self
, widget
, event
):
713 if util
.platform
.MAEMO
:
714 if event
.keyval
== gtk
.keysyms
.F7
: #plus
715 self
.set_volume( min( 1, self
.get_volume() + 0.10 ))
716 elif event
.keyval
== gtk
.keysyms
.F8
: #minus
717 self
.set_volume( max( 0, self
.get_volume() - 0.10 ))
718 elif event
.keyval
== gtk
.keysyms
.Left
: # seek back
719 self
.do_seek( -1 * settings
.seek_long
)
720 elif event
.keyval
== gtk
.keysyms
.Right
: # seek forward
721 self
.do_seek( settings
.seek_long
)
722 elif event
.keyval
== gtk
.keysyms
.Return
: # play/pause
723 self
.on_btn_play_pause_clicked()
725 # The following two functions get and set the
726 # volume from the volume control widgets.
727 def get_volume(self
):
728 if util
.platform
.MAEMO
:
729 return self
.volume
.get_level()/100.0
731 return self
.volume
.get_value()
733 def set_volume(self
, vol
):
734 """ vol is a float from 0 to 1 """
736 if util
.platform
.MAEMO
:
737 self
.volume
.set_level(vol
*100.0)
739 self
.volume
.set_value(vol
)
741 def __set_volume_hide_timer(self
, timeout
, force_show
=False):
742 if force_show
or self
.volume_button
.get_active():
744 if self
.volume_timer_id
is not None:
745 gobject
.source_remove(self
.volume_timer_id
)
746 self
.volume_timer_id
= None
748 self
.volume_timer_id
= gobject
.timeout_add(
749 1000 * timeout
, self
.__volume
_hide
_callback
)
751 def __volume_hide_callback(self
):
752 self
.volume_timer_id
= None
756 def toggle_volumebar(self
, widget
=None):
757 if self
.volume_timer_id
is None:
758 self
.__set
_volume
_hide
_timer
(5)
760 self
.__volume
_hide
_callback
()
762 def volume_changed_gtk(self
, widget
, new_value
=0.5):
763 settings
.volume
= new_value
765 def volume_changed_hildon(self
, widget
):
766 self
.__set
_volume
_hide
_timer
( 4, force_show
=True )
767 settings
.volume
= widget
.get_level()/100.0
769 def mute_toggled(self
, widget
):
770 if widget
.get_mute():
773 settings
.volume
= widget
.get_level()/100.0
775 def on_player_stopped(self
):
776 self
.stop_progress_timer()
777 self
.set_controls_sensitivity(False)
778 image(self
.play_pause_button
, 'media-playback-start.png')
780 def on_player_playing(self
):
781 self
.start_progress_timer()
782 image(self
.play_pause_button
, 'media-playback-pause.png')
783 self
.set_controls_sensitivity(True)
785 def on_player_new_track(self
):
786 for widget
in [self
.title_label
,self
.artist_label
,self
.album_label
]:
787 widget
.set_markup('')
790 self
.cover_art
.hide()
791 self
.has_coverart
= False
793 def on_player_new_metadata(self
):
794 metadata
= player
.playlist
.get_file_metadata()
795 self
.set_metadata(metadata
)
797 if not player
.playing
:
798 position
= player
.playlist
.get_current_position()
799 estimated_length
= metadata
.get('length', 0)
800 self
.set_progress_callback( position
, estimated_length
)
802 def on_player_paused( self
, position
, duration
):
803 self
.stop_progress_timer() # This should save some power
804 self
.set_progress_callback( position
, duration
)
805 image(self
.play_pause_button
, 'media-playback-start.png')
807 def on_player_end_of_playlist(self
, loop
):
810 def reset_progress(self
):
811 self
.progress
.set_fraction(0)
812 self
.set_progress_callback(0,0)
814 def set_progress_callback(self
, time_elapsed
, total_time
):
815 """ times must be in nanoseconds """
816 time_string
= "%s / %s" % ( util
.convert_ns(time_elapsed
),
817 util
.convert_ns(total_time
) )
818 self
.progress
.set_text( time_string
)
819 fraction
= float(time_elapsed
) / float(total_time
) if total_time
else 0
820 self
.progress
.set_fraction( fraction
)
822 def on_progressbar_changed(self
, widget
, event
):
823 if ( not settings
.progress_locked
and
824 event
.type == gtk
.gdk
.BUTTON_PRESS
and event
.button
== 1 ):
825 new_fraction
= event
.x
/float(widget
.get_allocation().width
)
826 resp
= player
.do_seek(percent
=new_fraction
)
828 # Preemptively update the progressbar to make seeking smoother
829 self
.set_progress_callback( *resp
)
831 def on_btn_play_pause_clicked(self
, widget
=None):
832 player
.play_pause_toggle()
834 def progress_timer_callback( self
):
835 if player
.playing
and not player
.seeking
:
836 pos_int
, dur_int
= player
.get_position_duration()
837 # This prevents bogus values from being set while seeking
838 if ( pos_int
> 10**9 ) and ( dur_int
> 10**9 ):
839 self
.set_progress_callback( pos_int
, dur_int
)
842 def start_progress_timer( self
):
843 if self
.progress_timer_id
is not None:
844 self
.stop_progress_timer()
846 self
.progress_timer_id
= gobject
.timeout_add(
847 1000, self
.progress_timer_callback
)
849 def stop_progress_timer( self
):
850 if self
.progress_timer_id
is not None:
851 gobject
.source_remove( self
.progress_timer_id
)
852 self
.progress_timer_id
= None
854 def get_coverart_size( self
):
855 if util
.platform
.MAEMO
:
856 if self
.__gui
_root
.fullscreen
:
857 size
= coverart_sizes
['maemo fullscreen']
859 size
= coverart_sizes
['maemo']
861 size
= coverart_sizes
['normal']
865 def set_coverart( self
, pixbuf
):
866 self
.cover_art
.set_from_pixbuf(pixbuf
)
867 self
.cover_art
.show()
868 self
.has_coverart
= True
870 def set_metadata( self
, tag_message
):
871 tags
= { 'title': self
.title_label
, 'artist': self
.artist_label
,
872 'album': self
.album_label
}
875 if tag_message
.has_key('image') and tag_message
['image'] is not None:
876 value
= tag_message
['image']
878 pbl
= gtk
.gdk
.PixbufLoader()
883 x
, y
= self
.get_coverart_size()
884 pixbuf
= pbl
.get_pixbuf()
885 pixbuf
= pixbuf
.scale_simple( x
, y
, gtk
.gdk
.INTERP_BILINEAR
)
886 self
.set_coverart(pixbuf
)
888 self
.__log
.exception('Error setting coverart...')
890 # set the text metadata
891 for tag
,value
in tag_message
.iteritems():
892 if tags
.has_key(tag
) and value
is not None and value
.strip():
893 tags
[tag
].set_markup('<big>'+value
+'</big>')
894 tags
[tag
].set_alignment( 0.5*int(not self
.has_coverart
), 0.5)
898 # make the title bold
899 tags
[tag
].set_markup('<b><big>'+value
+'</big></b>')
901 if not util
.platform
.MAEMO
:
902 value
+= ' - Panucci'
904 self
.__gui
_root
.main_window
.set_title( value
)
906 def do_seek(self
, seek_amount
):
907 resp
= player
.do_seek(from_current
=seek_amount
*10**9)
909 # Preemptively update the progressbar to make seeking smoother
910 self
.set_progress_callback( *resp
)
914 ##################################################
916 ##################################################
917 class PlaylistTab(gtk
.VBox
):
918 def __init__(self
, main_window
):
919 gtk
.VBox
.__init
__(self
)
920 self
.__log
= logging
.getLogger('panucci.panucci.BookmarksWindow')
921 self
.main
= main_window
923 self
.__model
= gtk
.TreeStore(
924 # uid, name, position
925 gobject
.TYPE_STRING
, gobject
.TYPE_STRING
, gobject
.TYPE_STRING
)
928 self
.treeview
= gtk
.TreeView()
929 self
.treeview
.set_model(self
.__model
)
930 self
.treeview
.set_headers_visible(True)
931 tree_selection
= self
.treeview
.get_selection()
932 # This breaks drag and drop, only use single selection for now
933 # tree_selection.set_mode(gtk.SELECTION_MULTIPLE)
934 tree_selection
.connect('changed', self
.tree_selection_changed
)
936 # The tree lines look nasty on maemo
937 if util
.platform
.DESKTOP
:
938 self
.treeview
.set_enable_tree_lines(True)
941 ncol
= gtk
.TreeViewColumn(_('Name'))
942 ncell
= gtk
.CellRendererText()
943 ncell
.set_property('ellipsize', pango
.ELLIPSIZE_END
)
944 ncell
.set_property('editable', True)
945 ncell
.connect('edited', self
.label_edited
)
946 ncol
.set_expand(True)
947 ncol
.pack_start(ncell
)
948 ncol
.add_attribute(ncell
, 'text', 1)
950 tcol
= gtk
.TreeViewColumn(_('Position'))
951 tcell
= gtk
.CellRendererText()
952 tcol
.pack_start(tcell
)
953 tcol
.add_attribute(tcell
, 'text', 2)
955 self
.treeview
.append_column(ncol
)
956 self
.treeview
.append_column(tcol
)
957 self
.treeview
.connect('drag-data-received', self
.drag_data_recieved
)
958 self
.treeview
.connect('drag_data_get', self
.drag_data_get_data
)
961 ( 'playlist_row_data', gtk
.TARGET_SAME_WIDGET
, 0 ) ]
963 self
.treeview
.enable_model_drag_source(
964 gtk
.gdk
.BUTTON1_MASK
, treeview_targets
, gtk
.gdk
.ACTION_COPY
)
966 self
.treeview
.enable_model_drag_dest(
967 treeview_targets
, gtk
.gdk
.ACTION_COPY
)
969 sw
= gtk
.ScrolledWindow()
970 sw
.set_policy(gtk
.POLICY_AUTOMATIC
, gtk
.POLICY_AUTOMATIC
)
971 sw
.set_shadow_type(gtk
.SHADOW_IN
)
972 sw
.add(self
.treeview
)
975 self
.hbox
= gtk
.HBox()
977 self
.add_button
= gtk
.Button(gtk
.STOCK_NEW
)
978 self
.add_button
.set_use_stock(True)
979 set_stock_button_text( self
.add_button
, _('Add File') )
980 self
.add_button
.connect('clicked', self
.add_file
)
981 self
.hbox
.pack_start(self
.add_button
, True, True)
983 self
.dir_button
= gtk
.Button(gtk
.STOCK_OPEN
)
984 self
.dir_button
.set_use_stock(True)
985 set_stock_button_text( self
.dir_button
, _('Add Directory') )
986 self
.dir_button
.connect('clicked', self
.add_directory
)
987 self
.hbox
.pack_start(self
.dir_button
, True, True)
989 self
.remove_button
= widgets
.DualActionButton(
990 generate_image(gtk
.STOCK_REMOVE
, True),
991 self
.remove_bookmark
,
992 generate_image(gtk
.STOCK_CANCEL
, True),
993 lambda *a
: player
.playlist
.reset_playlist() )
994 #self.remove_button.set_use_stock(True)
995 #self.remove_button.connect('clicked', self.remove_bookmark)
996 self
.hbox
.pack_start(self
.remove_button
, True, True)
998 self
.jump_button
= gtk
.Button(gtk
.STOCK_JUMP_TO
)
999 self
.jump_button
.set_use_stock(True)
1000 self
.jump_button
.connect('clicked', self
.jump_bookmark
)
1001 self
.hbox
.pack_start(self
.jump_button
, True, True)
1003 self
.info_button
= gtk
.Button()
1004 self
.info_button
.add(
1005 gtk
.image_new_from_stock(gtk
.STOCK_INFO
, gtk
.ICON_SIZE_BUTTON
))
1006 self
.info_button
.connect('clicked', self
.show_playlist_item_details
)
1007 self
.hbox
.pack_start(self
.info_button
, True, True)
1009 self
.pack_start(self
.hbox
, False, True)
1011 player
.playlist
.register( 'file_queued',
1012 lambda x
,y
,z
: self
.update_model() )
1013 player
.playlist
.register( 'bookmark_added', self
.on_bookmark_added
)
1017 def tree_selection_changed(self
, treeselection
):
1018 count
= treeselection
.count_selected_rows()
1019 self
.remove_button
.set_sensitive(count
> 0)
1020 self
.jump_button
.set_sensitive(count
== 1)
1021 self
.info_button
.set_sensitive(count
== 1)
1023 def drag_data_get_data(
1024 self
, treeview
, context
, selection
, target_id
, timestamp
):
1026 treeselection
= treeview
.get_selection()
1027 model
, iter = treeselection
.get_selected()
1028 # only allow moving around top-level parents
1029 if model
.iter_parent(iter) is None:
1030 # send the path of the selected row
1031 data
= model
.get_string_from_iter(iter)
1032 selection
.set(selection
.target
, 8, data
)
1034 self
.__log
.debug("Can't move children...")
1036 def drag_data_recieved(
1037 self
, treeview
, context
, x
, y
, selection
, info
, timestamp
):
1039 drop_info
= treeview
.get_dest_row_at_pos(x
, y
)
1041 # TODO: If user drags the row past the last row, drop_info is None
1042 # I'm not sure if it's safe to simply assume that None is
1043 # euqivalent to the last row...
1044 if None not in [ drop_info
and selection
.data
]:
1045 model
= treeview
.get_model()
1046 path
, position
= drop_info
1048 from_iter
= model
.get_iter_from_string(selection
.data
)
1050 # make sure the to_iter doesn't have a parent
1051 to_iter
= model
.get_iter(path
)
1052 if model
.iter_parent(to_iter
) is not None:
1053 to_iter
= model
.iter_parent(to_iter
)
1055 from_row
= model
.get_path(from_iter
)[0]
1058 if ( position
== gtk
.TREE_VIEW_DROP_BEFORE
or
1059 position
== gtk
.TREE_VIEW_DROP_INTO_OR_BEFORE
):
1060 model
.move_before( from_iter
, to_iter
)
1061 to_row
= to_row
- 1 if from_row
< to_row
else to_row
1062 elif ( position
== gtk
.TREE_VIEW_DROP_AFTER
or
1063 position
== gtk
.TREE_VIEW_DROP_INTO_OR_AFTER
):
1064 model
.move_after( from_iter
, to_iter
)
1065 to_row
= to_row
+ 1 if from_row
> to_row
else to_row
1067 self
.__log
.debug('Drop not supported: %s', position
)
1069 # don't do anything if we're not actually moving rows around
1070 if from_row
!= to_row
:
1071 player
.playlist
.move_item( from_row
, to_row
)
1074 self
.__log
.debug('No drop_data or selection.data available')
1076 def update_model(self
):
1077 plist
= player
.playlist
1078 path_info
= self
.treeview
.get_path_at_pos(0,0)
1079 path
= path_info
[0] if path_info
is not None else None
1081 self
.__model
.clear()
1084 for item
, data
in plist
.get_playlist_item_ids():
1085 parent
= self
.__model
.append(None, (item
, data
.get('title'), None))
1087 for bid
, bname
, bpos
in plist
.get_bookmarks_from_item_id( item
):
1088 nice_bpos
= util
.convert_ns(bpos
)
1089 self
.__model
.append( parent
, (bid
, bname
, nice_bpos
) )
1091 self
.treeview
.expand_all()
1093 if path
is not None:
1094 self
.treeview
.scroll_to_cell(path
)
1096 def label_edited(self
, cellrenderer
, path
, new_text
):
1097 iter = self
.__model
.get_iter(path
)
1098 old_text
= self
.__model
.get_value(iter, 1)
1100 if new_text
.strip() and old_text
!= new_text
:
1101 # this loop will only run once, because only one cell can be
1102 # edited at a time, we use it to get the item and bookmark ids
1103 for m
, bkmk_id
, biter
, item_id
, iiter
in self
.__cur
_selection
():
1104 self
.__model
.set_value(iter, 1, new_text
)
1105 player
.playlist
.update_bookmark(
1106 item_id
, bkmk_id
, name
=new_text
)
1108 self
.__model
.set_value(iter, 1, old_text
)
1110 def on_bookmark_added(self
, parent_id
, bookmark_name
, position
):
1111 util
.notify(_('Bookmark added: %s') % bookmark_name
)
1114 def add_file(self
, widget
):
1115 filename
= get_file_from_filechooser(self
.main
.main_window
)
1116 if filename
is not None:
1117 player
.playlist
.append(filename
)
1119 def add_directory(self
, widget
):
1120 directory
= get_file_from_filechooser(
1121 self
.main
.main_window
, folder
=True )
1122 if directory
is not None:
1123 player
.playlist
.load_directory(directory
, append
=True)
1125 def __cur_selection(self
):
1126 selection
= self
.treeview
.get_selection()
1127 model
, bookmark_paths
= selection
.get_selected_rows()
1129 # Convert the paths to gtk.TreeRowReference objects, because we
1130 # might modify the model while this generator is running
1131 bookmark_refs
= [gtk
.TreeRowReference(model
, p
) for p
in bookmark_paths
]
1133 for reference
in bookmark_refs
:
1134 bookmark_iter
= model
.get_iter(reference
.get_path())
1135 item_iter
= model
.iter_parent(bookmark_iter
)
1137 # bookmark_iter is actually an item_iter
1138 if item_iter
is None:
1139 item_iter
= bookmark_iter
1140 item_id
= model
.get_value(item_iter
, 0)
1141 bookmark_id
, bookmark_iter
= None, None
1143 bookmark_id
= model
.get_value(bookmark_iter
, 0)
1144 item_id
= model
.get_value(item_iter
, 0)
1146 yield model
, bookmark_id
, bookmark_iter
, item_id
, item_iter
1148 def remove_bookmark(self
, w
=None):
1149 for model
, bkmk_id
, bkmk_iter
, item_id
, item_iter
in self
.__cur
_selection
():
1150 player
.playlist
.remove_bookmark( item_id
, bkmk_id
)
1151 if bkmk_iter
is not None:
1152 model
.remove(bkmk_iter
)
1153 elif item_iter
is not None:
1154 model
.remove(item_iter
)
1156 def select_current_item(self
):
1157 model
= self
.treeview
.get_model()
1158 selection
= self
.treeview
.get_selection()
1159 current_item_id
= str(player
.playlist
.get_current_item())
1160 for row
in iter(model
):
1161 if model
.get_value(row
.iter, 0) == current_item_id
:
1162 selection
.unselect_all()
1163 self
.treeview
.set_cursor(row
.path
)
1164 self
.treeview
.scroll_to_cell(row
.path
, use_align
=True)
1167 def show_playlist_item_details(self
, w
):
1168 selection
= self
.treeview
.get_selection()
1169 if selection
.count_selected_rows() == 1:
1170 selected
= self
.__cur
_selection
().next()
1171 model
, bkmk_id
, bkmk_iter
, item_id
, item_iter
= selected
1172 playlist_item
= player
.playlist
.get_item_by_id(item_id
)
1173 PlaylistItemDetails(self
.main
, playlist_item
)
1175 def jump_bookmark(self
, w
):
1176 selected
= list(self
.__cur
_selection
())
1177 if len(selected
) == 1:
1178 # It should be guranteed by the fact that we only enable the
1179 # "Jump to" button when the selection count equals 1.
1180 model
, bkmk_id
, bkmk_iter
, item_id
, item_iter
= selected
.pop(0)
1181 player
.playlist
.load_from_bookmark_id(item_id
, bkmk_id
)
1183 # FIXME: The player/playlist should be able to take care of this
1184 if not player
.playing
:
1188 ##################################################
1189 # PlaylistItemDetails
1190 ##################################################
1191 class PlaylistItemDetails(gtk
.Dialog
):
1192 def __init__(self
, main
, playlist_item
):
1193 gtk
.Dialog
.__init
__( self
, _('Playlist item details'),
1194 main
.main_window
, gtk
.DIALOG_MODAL
,
1195 (gtk
.STOCK_CLOSE
, gtk
.RESPONSE_OK
))
1198 self
.fill(playlist_item
)
1199 self
.set_has_separator(False)
1200 self
.set_resizable(False)
1205 def fill(self
, playlist_item
):
1206 t
= gtk
.Table(10, 2)
1207 self
.vbox
.pack_start(t
, expand
=False)
1209 metadata
= playlist_item
.metadata
1211 t
.attach(gtk
.Label(_('Custom title:')), 0, 1, 0, 1)
1212 t
.attach(gtk
.Label(_('ID:')), 0, 1, 1, 2)
1213 t
.attach(gtk
.Label(_('Playlist ID:')), 0, 1, 2, 3)
1214 t
.attach(gtk
.Label(_('Filepath:')), 0, 1, 3, 4)
1217 for key
in metadata
:
1218 if metadata
[key
] is not None:
1219 t
.attach( gtk
.Label(key
.capitalize()+':'),
1220 0, 1, row_num
, row_num
+1 )
1223 t
.foreach(lambda x
, y
: x
.set_alignment(1, 0.5), None)
1224 t
.foreach(lambda x
, y
: x
.set_markup('<b>%s</b>' % x
.get_label()), None)
1226 t
.attach(gtk
.Label(playlist_item
.title
or _('<not modified>')),1,2,0,1)
1227 t
.attach(gtk
.Label(str(playlist_item
)), 1, 2, 1, 2)
1228 t
.attach(gtk
.Label(playlist_item
.playlist_id
), 1, 2, 2, 3)
1229 t
.attach(gtk
.Label(playlist_item
.filepath
), 1, 2, 3, 4)
1232 for key
in metadata
:
1233 value
= metadata
[key
]
1235 value
= util
.convert_ns(value
)
1236 if metadata
[key
] is not None:
1237 t
.attach( gtk
.Label( str(value
) or _('<not set>')),
1238 1, 2, row_num
, row_num
+1)
1241 t
.foreach(lambda x
, y
: x
.get_alignment() == (0.5, 0.5) and \
1242 x
.set_alignment(0, 0.5), None)
1244 t
.set_border_width(8)
1245 t
.set_row_spacings(4)
1246 t
.set_col_spacings(8)
1248 l
= gtk
.ListStore(str, str)
1250 cr
= gtk
.CellRendererText()
1251 cr
.set_property('ellipsize', pango
.ELLIPSIZE_END
)
1252 c
= gtk
.TreeViewColumn(_('Title'), cr
, text
=0)
1255 c
= gtk
.TreeViewColumn(_('Time'), gtk
.CellRendererText(), text
=1)
1257 playlist_item
.load_bookmarks()
1258 for bookmark
in playlist_item
.bookmarks
:
1259 l
.append([bookmark
.bookmark_name
, \
1260 util
.convert_ns(bookmark
.seek_position
)])
1262 sw
= gtk
.ScrolledWindow()
1263 sw
.set_shadow_type(gtk
.SHADOW_IN
)
1265 sw
.set_policy(gtk
.POLICY_AUTOMATIC
, gtk
.POLICY_AUTOMATIC
)
1266 e
= gtk
.Expander(_('Bookmarks'))
1268 self
.vbox
.pack_start(e
)
1271 def run(filename
=None):
1272 ossohelper
.application_init('org.panucci', '0.4')
1273 PanucciGUI( filename
)
1275 ossohelper
.application_exit()
1277 if __name__
== '__main__':
1278 log
.error( 'Use the "panucci" executable to run this program.' )
1279 log
.error( 'Exiting...' )