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
41 from panucci
import widgets
42 from panucci
import util
43 from panucci
import platform
45 log
= logging
.getLogger('panucci.panucci')
49 pynotify
.init('Panucci')
58 log
.critical( 'Using GTK widgets, install "python2.5-hildon" '
59 'for this to work properly.' )
61 from panucci
.settings
import settings
62 from panucci
.player
import player
63 from panucci
.dbusinterface
import interface
64 from panucci
.services
import ObservableService
66 about_name
= 'Panucci'
67 about_text
= _('Resuming audiobook and podcast player')
68 about_authors
= ['Thomas Perl', 'Nick (nikosapi)', 'Matthew Taylor']
69 about_website
= 'http://panucci.garage.maemo.org/'
70 about_bugtracker
= 'http://bugs.maemo.org/enter_bug.cgi?product=Panucci'
71 about_donate
= 'http://gpodder.org/donate'
76 'maemo fullscreen' : 275,
79 gtk
.icon_size_register('panucci-button', 32, 32)
81 def find_image(filename
):
82 bin_dir
= os
.path
.dirname(sys
.argv
[0])
84 os
.path
.join(bin_dir
, '..', 'share', 'panucci'),
85 os
.path
.join(bin_dir
, '..', 'icons'),
89 for location
in locations
:
90 fn
= os
.path
.abspath(os
.path
.join(location
, filename
))
91 if os
.path
.exists(fn
):
94 def generate_image(filename
, is_stock
=False):
97 image
= gtk
.image_new_from_stock(
98 filename
, gtk
.icon_size_from_name('panucci-button') )
100 filename
= find_image(filename
)
101 if filename
is not None:
102 image
= gtk
.image_new_from_file(filename
)
103 if image
is not None:
105 image
.set_padding(20, 20)
107 image
.set_padding(5, 5)
111 def image(widget
, filename
, is_stock
=False):
112 child
= widget
.get_child()
113 if child
is not None:
115 image
= generate_image(filename
, is_stock
)
116 if image
is not None:
119 def dialog( toplevel_window
, title
, question
, description
,
120 affirmative_button
=gtk
.STOCK_YES
, negative_button
=gtk
.STOCK_NO
,
121 abortion_button
=gtk
.STOCK_CANCEL
):
123 """Present the user with a yes/no/cancel dialog.
124 The return value is either True, False or None, depending on which
125 button has been pressed in the dialog:
127 affirmative button (default: Yes) => True
128 negative button (defaut: No) => False
129 abortion button (default: Cancel) => None
131 When the dialog is closed with the "X" button in the window manager
132 decoration, the return value is always None (same as abortion button).
134 You can set any of the affirmative_button, negative_button or
135 abortion_button values to "None" to hide the corresponding action.
137 dlg
= gtk
.MessageDialog( toplevel_window
, gtk
.DIALOG_MODAL
,
138 gtk
.MESSAGE_QUESTION
, message_format
=question
)
142 if abortion_button
is not None:
143 dlg
.add_button(abortion_button
, gtk
.RESPONSE_CANCEL
)
144 if negative_button
is not None:
145 dlg
.add_button(negative_button
, gtk
.RESPONSE_NO
)
146 if affirmative_button
is not None:
147 dlg
.add_button(affirmative_button
, gtk
.RESPONSE_YES
)
149 dlg
.format_secondary_text(description
)
154 if response
== gtk
.RESPONSE_YES
:
156 elif response
== gtk
.RESPONSE_NO
:
158 elif response
in [gtk
.RESPONSE_CANCEL
, gtk
.RESPONSE_DELETE_EVENT
]:
161 def get_file_from_filechooser(
162 toplevel_window
, folder
=False, save_file
=False, save_to
=None):
165 open_action
= gtk
.FILE_CHOOSER_ACTION_SELECT_FOLDER
167 open_action
= gtk
.FILE_CHOOSER_ACTION_OPEN
169 if platform
.FREMANTLE
:
171 dlg
= gobject
.new(hildon
.FileChooserDialog
, \
172 action
=gtk
.FILE_CHOOSER_ACTION_SAVE
)
174 dlg
= gobject
.new(hildon
.FileChooserDialog
, \
178 args
= ( toplevel_window
, gtk
.FILE_CHOOSER_ACTION_SAVE
)
180 args
= ( toplevel_window
, open_action
)
182 dlg
= hildon
.FileChooserDialog( *args
)
185 args
= ( _('Select file to save playlist to'), None,
186 gtk
.FILE_CHOOSER_ACTION_SAVE
,
187 (( gtk
.STOCK_CANCEL
, gtk
.RESPONSE_REJECT
,
188 gtk
.STOCK_SAVE
, gtk
.RESPONSE_OK
)) )
190 args
= ( _('Select podcast or audiobook'), None, open_action
,
191 (( gtk
.STOCK_CANCEL
, gtk
.RESPONSE_REJECT
,
192 gtk
.STOCK_OPEN
, gtk
.RESPONSE_OK
)) )
194 dlg
= gtk
.FileChooserDialog(*args
)
196 current_folder
= os
.path
.expanduser(settings
.last_folder
)
198 if current_folder
is not None and os
.path
.isdir(current_folder
):
199 dlg
.set_current_folder(current_folder
)
201 if save_file
and save_to
is not None:
202 dlg
.set_current_name(save_to
)
204 if dlg
.run() == gtk
.RESPONSE_OK
:
205 filename
= dlg
.get_filename()
206 settings
.last_folder
= dlg
.get_current_folder()
213 def set_stock_button_text( button
, text
):
214 alignment
= button
.get_child()
215 hbox
= alignment
.get_child()
216 image
, label
= hbox
.get_children()
219 ##################################################
221 ##################################################
222 class PanucciGUI(object):
223 """ The object that holds the entire panucci gui """
225 def __init__(self
, filename
=None):
226 self
.__log
= logging
.getLogger('panucci.panucci.PanucciGUI')
227 interface
.register_gui(self
)
229 # Build the base ui (window and menubar)
231 self
.app
= hildon
.Program()
232 if platform
.FREMANTLE
:
233 window
= hildon
.StackableWindow()
235 window
= hildon
.Window()
236 self
.app
.add_window(window
)
238 window
= gtk
.Window(gtk
.WINDOW_TOPLEVEL
)
240 self
.main_window
= window
241 window
.set_title('Panucci')
242 self
.window_icon
= find_image('panucci.png')
243 if self
.window_icon
is not None:
244 window
.set_icon_from_file( self
.window_icon
)
245 window
.set_default_size(400, -1)
246 window
.set_border_width(0)
247 window
.connect("destroy", self
.destroy
)
249 # Add the tabs (they are private to prevent us from trying to do
250 # something like gui_root.player_tab.some_function() from inside
251 # playlist_tab or vice-versa)
252 self
.__player
_tab
= PlayerTab(self
)
253 self
.__playlist
_tab
= PlaylistTab(self
)
255 if platform
.FREMANTLE
:
256 self
.playlist_window
= hildon
.StackableWindow()
258 self
.playlist_window
= gtk
.Window(gtk
.WINDOW_TOPLEVEL
)
259 self
.playlist_window
.connect('delete-event', gtk
.Widget
.hide_on_delete
)
260 self
.playlist_window
.set_title(_('Playlist'))
261 self
.playlist_window
.set_transient_for(self
.main_window
)
262 self
.playlist_window
.add(self
.__playlist
_tab
)
264 self
.create_actions()
267 if platform
.FREMANTLE
:
268 window
.set_app_menu(self
.create_app_menu())
270 window
.set_menu(self
.create_menu())
271 window
.add(self
.__player
_tab
)
273 menu_vbox
= gtk
.VBox()
274 menu_vbox
.set_spacing(0)
275 window
.add(menu_vbox
)
276 menu_bar
= gtk
.MenuBar()
277 self
.create_desktop_menu(menu_bar
)
278 menu_vbox
.pack_start(menu_bar
, False, False, 0)
280 menu_vbox
.pack_end(self
.__player
_tab
, True, True, 6)
282 # Tie it all together!
283 self
.__ignore
_queue
_check
= False
284 self
.__window
_fullscreen
= False
286 if platform
.MAEMO
and interface
.headset_device
is not None:
287 # Enable play/pause with headset button
288 interface
.headset_device
.connect_to_signal(
289 'Condition', self
.handle_headset_button
)
291 self
.main_window
.connect('key-press-event', self
.on_key_press
)
292 player
.playlist
.register( 'file_queued', self
.on_file_queued
)
294 player
.playlist
.register( 'playlist-to-be-overwritten',
296 self
.__player
_tab
.register( 'select-current-item-request',
297 self
.__select
_current
_item
)
299 self
.main_window
.show_all()
301 # this should be done when the gui is ready
302 player
.init(filepath
=filename
)
304 pos_int
, dur_int
= player
.get_position_duration()
305 # This prevents bogus values from being set while seeking
306 if (pos_int
> 10**9) and (dur_int
> 10**9):
307 self
.set_progress_callback(pos_int
, dur_int
)
309 def create_actions(self
):
310 self
.action_open
= gtk
.Action('open', _('Open'), _('Open a file or playlist'), gtk
.STOCK_OPEN
)
311 self
.action_open
.connect('activate', self
.open_file_callback
)
312 self
.action_save
= gtk
.Action('save', _('Save playlist'), _('Save current playlist to file'), gtk
.STOCK_SAVE_AS
)
313 self
.action_save
.connect('activate', self
.save_to_playlist_callback
)
314 self
.action_playlist
= gtk
.Action('playlist', _('Playlist'), _('Open the current playlist'), None)
315 self
.action_playlist
.connect('activate', lambda a
: self
.playlist_window
.show())
316 self
.action_about
= gtk
.Action('about', _('About Panucci'), _('Show application version'), gtk
.STOCK_ABOUT
)
317 self
.action_about
.connect('activate', self
.about_callback
)
318 self
.action_quit
= gtk
.Action('quit', _('Quit'), _('Close Panucci'), gtk
.STOCK_QUIT
)
319 self
.action_quit
.connect('activate', self
.destroy
)
321 def create_desktop_menu(self
, menu_bar
):
322 file_menu_item
= gtk
.MenuItem(_('File'))
323 file_menu
= gtk
.Menu()
324 file_menu
.append(self
.action_open
.create_menu_item())
325 file_menu
.append(self
.action_save
.create_menu_item())
326 file_menu
.append(gtk
.SeparatorMenuItem())
327 file_menu
.append(self
.action_quit
.create_menu_item())
328 file_menu_item
.set_submenu(file_menu
)
329 menu_bar
.append(file_menu_item
)
331 tools_menu_item
= gtk
.MenuItem(_('Tools'))
332 tools_menu
= gtk
.Menu()
333 tools_menu
.append(self
.action_playlist
.create_menu_item())
334 tools_menu_item
.set_submenu(tools_menu
)
335 menu_bar
.append(tools_menu_item
)
337 help_menu_item
= gtk
.MenuItem(_('Help'))
338 help_menu
= gtk
.Menu()
339 help_menu
.append(self
.action_about
.create_menu_item())
340 help_menu_item
.set_submenu(help_menu
)
341 menu_bar
.append(help_menu_item
)
343 def create_app_menu(self
):
344 menu
= hildon
.AppMenu()
346 b
= gtk
.Button(_('Playlist'))
347 b
.connect('clicked', lambda b
: self
.__player
_tab
.notify('select-current-item-request'))
350 b
= gtk
.Button(_('About'))
351 b
.connect('clicked', self
.about_callback
)
357 def create_menu(self
):
361 menu_open
= gtk
.ImageMenuItem(_('Open playlist'))
363 gtk
.image_new_from_stock(gtk
.STOCK_OPEN
, gtk
.ICON_SIZE_MENU
))
364 menu_open
.connect("activate", self
.open_file_callback
)
365 menu
.append(menu_open
)
367 # the recent files menu
368 self
.menu_recent
= gtk
.MenuItem(_('Open recent playlist'))
369 menu
.append(self
.menu_recent
)
370 self
.create_recent_files_menu()
372 menu
.append(gtk
.SeparatorMenuItem())
374 menu_save
= gtk
.ImageMenuItem(_('Save current playlist'))
376 gtk
.image_new_from_stock(gtk
.STOCK_SAVE_AS
, gtk
.ICON_SIZE_MENU
))
377 menu_save
.connect("activate", self
.save_to_playlist_callback
)
378 menu
.append(menu_save
)
380 menu
.append(gtk
.SeparatorMenuItem())
382 # the settings sub-menu
383 menu_settings
= gtk
.MenuItem(_('Settings'))
384 menu
.append(menu_settings
)
386 menu_settings_sub
= gtk
.Menu()
387 menu_settings
.set_submenu(menu_settings_sub
)
389 menu_settings_enable_dual_action
= gtk
.CheckMenuItem(
390 _('Enable dual-action buttons') )
391 settings
.attach_checkbutton( menu_settings_enable_dual_action
,
392 'enable_dual_action_btn' )
393 menu_settings_sub
.append(menu_settings_enable_dual_action
)
395 menu_settings_lock_progress
= gtk
.CheckMenuItem(_('Lock Progress Bar'))
396 settings
.attach_checkbutton( menu_settings_lock_progress
,
398 menu_settings_sub
.append(menu_settings_lock_progress
)
400 menu_about
= gtk
.ImageMenuItem(gtk
.STOCK_ABOUT
)
401 menu_about
.connect("activate", self
.about_callback
)
402 menu
.append(menu_about
)
404 menu
.append(gtk
.SeparatorMenuItem())
406 menu_quit
= gtk
.ImageMenuItem(gtk
.STOCK_QUIT
)
407 menu_quit
.connect("activate", self
.destroy
)
408 menu
.append(menu_quit
)
412 def create_recent_files_menu( self
):
413 max_files
= settings
.max_recent_files
414 self
.recent_files
= player
.playlist
.get_recent_files(max_files
)
415 menu_recent_sub
= gtk
.Menu()
417 if len(self
.recent_files
) > 0:
418 for f
in self
.recent_files
:
419 # don't include the temporary playlist in the file list
420 if f
== panucci
.PLAYLIST_FILE
: continue
421 # don't include non-existant files
422 if not os
.path
.exists( f
): continue
423 filename
, extension
= os
.path
.splitext(os
.path
.basename(f
))
424 menu_item
= gtk
.MenuItem( filename
.replace('_', ' '))
425 menu_item
.connect('activate', self
.on_recent_file_activate
, f
)
426 menu_recent_sub
.append(menu_item
)
428 menu_item
= gtk
.MenuItem(_('No recent files available.'))
429 menu_item
.set_sensitive(False)
430 menu_recent_sub
.append(menu_item
)
432 self
.menu_recent
.set_submenu(menu_recent_sub
)
434 def notify(self
, message
):
435 """ Sends a notification using pynotify, returns message """
436 if platform
.DESKTOP
and have_pynotify
:
437 icon
= find_image('panucci_64x64.png')
438 notification
= pynotify
.Notification(self
.main_window
.get_title(), message
, icon
)
440 elif platform
.FREMANTLE
:
441 hildon
.hildon_banner_show_information(self
.main_window
, \
444 # Note: This won't work if we're not in the gtk main loop
445 markup
= '<b>%s</b>\n<small>%s</small>' % (self
.main_window
.get_title(), message
)
446 hildon
.hildon_banner_show_information_with_markup(self
.main_window
, None, markup
)
448 def destroy(self
, widget
):
452 def set_progress_indicator(self
, loading_title
=False):
453 if platform
.FREMANTLE
:
455 self
.main_window
.set_title(_('Loading...'))
456 hildon
.hildon_gtk_window_set_progress_indicator(self
.main_window
, \
458 while gtk
.events_pending():
459 gtk
.main_iteration(False)
461 def show_main_window(self
):
462 self
.main_window
.present()
464 def check_queue(self
):
465 """ Makes sure the queue is saved if it has been modified
466 True means a new file can be opened
467 False means the user does not want to continue """
469 if not self
.__ignore
_queue
_check
and player
.playlist
.queue_modified
:
471 self
.main_window
, _('Save current playlist'),
472 _('Current playlist has been modified'),
473 _('Opening a new file will replace the current playlist. ') +
474 _('Do you want to save it before creating a new one?'),
475 affirmative_button
=gtk
.STOCK_SAVE
,
476 negative_button
=_('Discard changes'))
478 self
.__log
.debug('Response to "Save Queue?": %s', response
)
483 return self
.save_to_playlist_callback()
491 def open_file_callback(self
, widget
=None):
492 if self
.check_queue():
493 # set __ingnore__queue_check because we already did the check
494 self
.__ignore
_queue
_check
= True
495 filename
= get_file_from_filechooser(self
.main_window
)
496 if filename
is not None:
497 self
._play
_file
(filename
)
499 self
.__ignore
_queue
_check
= False
501 def save_to_playlist_callback(self
, widget
=None):
502 filename
= get_file_from_filechooser(
503 self
.main_window
, save_file
=True, save_to
='playlist.m3u' )
508 if os
.path
.isfile(filename
):
509 response
= dialog( self
.main_window
, _('File already exists'),
510 _('File already exists'),
511 _('The file %s already exists. You can choose another name or '
512 'overwrite the existing file.') % os
.path
.basename(filename
),
513 affirmative_button
=gtk
.STOCK_SAVE
,
514 negative_button
=_('Rename file'))
522 return self
.save_to_playlist_callback()
524 ext
= util
.detect_filetype(filename
)
525 if not player
.playlist
.save_to_new_playlist(filename
, ext
):
526 self
.notify(_('Error saving playlist...'))
531 def __get_fullscreen(self
):
532 return self
.__window
_fullscreen
534 def __set_fullscreen(self
, value
):
535 if value
!= self
.__window
_fullscreen
:
537 self
.main_window
.fullscreen()
539 self
.main_window
.unfullscreen()
541 self
.__window
_fullscreen
= value
542 player
.playlist
.send_metadata()
544 fullscreen
= property( __get_fullscreen
, __set_fullscreen
)
546 def on_key_press(self
, widget
, event
):
548 if event
.keyval
== gtk
.keysyms
.F6
:
549 self
.fullscreen
= not self
.fullscreen
551 def on_recent_file_activate(self
, widget
, filepath
):
552 self
._play
_file
(filepath
)
554 def on_file_queued(self
, filepath
, success
, notify
):
556 filename
= os
.path
.basename(filepath
)
559 self
.notify( '%s added successfully.' % filename
))
562 self
.notify( 'Error adding %s to the queue.' % filename
))
564 def about_callback(self
, widget
):
565 from panucci
.aboutdialog
import HeAboutDialog
567 authors
= ', '.join(about_authors
[:-1])
568 authors
+= ' and ' + about_authors
[-1]
570 HeAboutDialog
.present(self
.main_window
,
575 '(c) 2008-2010 ' + authors
,
580 def _play_file(self
, filename
, pause_on_load
=False):
581 player
.playlist
.load( os
.path
.abspath(filename
) )
583 if player
.playlist
.is_empty
:
586 def handle_headset_button(self
, event
, button
):
587 if event
== 'ButtonPressed' and button
== 'phone':
588 player
.play_pause_toggle()
590 def __select_current_item( self
):
591 # Select the currently playing track in the playlist tab
592 # and switch to it (so we can edit bookmarks, etc.. there)
593 self
.__playlist
_tab
.select_current_item()
594 self
.playlist_window
.show()
596 ##################################################
598 ##################################################
599 class PlayerTab(ObservableService
, gtk
.HBox
):
600 """ The tab that holds the player elements """
602 signals
= [ 'select-current-item-request', ]
604 def __init__(self
, gui_root
):
605 self
.__log
= logging
.getLogger('panucci.panucci.PlayerTab')
606 self
.__gui
_root
= gui_root
608 gtk
.HBox
.__init
__(self
)
609 ObservableService
.__init
__(self
, self
.signals
, self
.__log
)
612 self
.progress_timer_id
= None
614 self
.recent_files
= []
615 self
.make_player_tab()
616 self
.has_coverart
= False
618 #settings.register( 'enable_dual_action_btn_changed',
619 # self.on_dual_action_setting_changed )
620 #settings.register( 'dual_action_button_delay_changed',
621 # self.on_dual_action_setting_changed )
622 #settings.register( 'scrolling_labels_changed', lambda v:
623 # setattr( self.title_label, 'scrolling', v ) )
625 player
.register( 'stopped', self
.on_player_stopped
)
626 player
.register( 'playing', self
.on_player_playing
)
627 player
.register( 'paused', self
.on_player_paused
)
628 player
.playlist
.register( 'end-of-playlist',
629 self
.on_player_end_of_playlist
)
630 player
.playlist
.register( 'new-track-loaded',
631 self
.on_player_new_track
)
632 player
.playlist
.register( 'new-metadata-available',
633 self
.on_player_new_metadata
)
635 def make_player_tab(self
):
636 main_vbox
= gtk
.VBox()
637 main_vbox
.set_spacing(6)
639 self
.pack_start(main_vbox
, True, True)
641 # a hbox to hold the cover art and metadata vbox
642 metadata_hbox
= gtk
.HBox()
643 metadata_hbox
.set_spacing(6)
644 main_vbox
.pack_start(metadata_hbox
, True, False)
646 self
.cover_art
= gtk
.Image()
647 metadata_hbox
.pack_start( self
.cover_art
, False, False )
649 # vbox to hold metadata
650 metadata_vbox
= gtk
.VBox()
651 metadata_vbox
.pack_start(gtk
.Image(), True, True)
652 self
.artist_label
= gtk
.Label('')
653 self
.artist_label
.set_ellipsize(pango
.ELLIPSIZE_END
)
654 metadata_vbox
.pack_start(self
.artist_label
, False, False)
655 self
.album_label
= gtk
.Label('')
656 if platform
.FREMANTLE
:
657 hildon
.hildon_helper_set_logical_font(self
.album_label
, 'SmallSystemFont')
658 hildon
.hildon_helper_set_logical_color(self
.album_label
, gtk
.RC_FG
, gtk
.STATE_NORMAL
, 'SecondaryTextColor')
660 self
.album_label
.modify_font(pango
.FontDescription('normal 8'))
661 self
.album_label
.set_ellipsize(pango
.ELLIPSIZE_END
)
662 metadata_vbox
.pack_start(self
.album_label
, False, False)
663 self
.title_label
= widgets
.ScrollingLabel('',
666 delay_btwn_scrolls
=5000,
668 self
.title_label
.scrolling
= settings
.scrolling_labels
669 metadata_vbox
.pack_start(self
.title_label
, False, False)
670 metadata_vbox
.pack_start(gtk
.Image(), True, True)
671 metadata_hbox
.pack_start( metadata_vbox
, True, True )
673 progress_eventbox
= gtk
.EventBox()
674 progress_eventbox
.set_events(gtk
.gdk
.BUTTON_PRESS_MASK
)
675 progress_eventbox
.connect(
676 'button-press-event', self
.on_progressbar_changed
)
677 self
.progress
= gtk
.ProgressBar()
678 # make the progress bar more "finger-friendly"
679 if platform
.FREMANTLE
:
680 self
.progress
.set_size_request(-1, 100)
682 self
.progress
.set_size_request(-1, 50)
683 progress_eventbox
.add(self
.progress
)
684 main_vbox
.pack_start( progress_eventbox
, False, False )
686 # make the button box
687 buttonbox
= gtk
.HBox()
689 # A wrapper to help create DualActionButtons with the right settings
690 def create_da(widget
, action
, widget2
=None, action2
=None):
691 if platform
.FREMANTLE
:
695 return widgets
.DualActionButton(widget
, action
, \
697 settings
.dual_action_button_delay
, \
698 settings
.enable_dual_action_btn
)
700 self
.rrewind_button
= create_da(
701 generate_image('media-skip-backward.png'),
702 lambda: self
.do_seek(-1*settings
.seek_long
),
703 generate_image(gtk
.STOCK_GOTO_FIRST
, True),
704 player
.playlist
.prev
)
705 buttonbox
.add(self
.rrewind_button
)
707 self
.rewind_button
= create_da(
708 generate_image('media-seek-backward.png'),
709 lambda: self
.do_seek(-1*settings
.seek_short
))
710 buttonbox
.add(self
.rewind_button
)
712 self
.play_pause_button
= gtk
.Button('')
713 image(self
.play_pause_button
, 'media-playback-start.png')
714 self
.play_pause_button
.connect( 'clicked',
715 self
.on_btn_play_pause_clicked
)
716 self
.play_pause_button
.set_sensitive(False)
717 buttonbox
.add(self
.play_pause_button
)
719 self
.forward_button
= create_da(
720 generate_image('media-seek-forward.png'),
721 lambda: self
.do_seek(settings
.seek_short
))
722 buttonbox
.add(self
.forward_button
)
724 self
.fforward_button
= create_da(
725 generate_image('media-skip-forward.png'),
726 lambda: self
.do_seek(settings
.seek_long
),
727 generate_image(gtk
.STOCK_GOTO_LAST
, True),
728 player
.playlist
.next
)
729 buttonbox
.add(self
.fforward_button
)
731 self
.bookmarks_button
= create_da(
732 generate_image('bookmark-new.png'),
733 player
.add_bookmark_at_current_position
,
734 generate_image(gtk
.STOCK_JUMP_TO
, True),
735 lambda *args
: self
.notify('select-current-item-request'))
736 buttonbox
.add(self
.bookmarks_button
)
737 self
.set_controls_sensitivity(False)
739 if platform
.FREMANTLE
:
740 for child
in buttonbox
.get_children():
741 if isinstance(child
, gtk
.Button
):
742 child
.set_name('HildonButton-thumb')
743 buttonbox
.set_size_request(-1, 105)
745 main_vbox
.pack_start(buttonbox
, False, False)
748 self
.__gui
_root
.main_window
.connect( 'key-press-event',
751 # Disable focus for all widgets, so we can use the cursor
752 # keys + enter to directly control our media player, which
753 # is handled by "key-press-event"
755 self
.rrewind_button
, self
.rewind_button
,
756 self
.play_pause_button
, self
.forward_button
,
757 self
.fforward_button
, self
.progress
,
758 self
.bookmarks_button
, ):
759 w
.unset_flags(gtk
.CAN_FOCUS
)
761 def set_controls_sensitivity(self
, sensitive
):
762 for button
in self
.forward_button
, self
.rewind_button
, \
763 self
.fforward_button
, self
.rrewind_button
:
765 button
.set_sensitive(sensitive
)
767 # the play/pause button should always be available except
768 # for when the player starts without a file
769 self
.play_pause_button
.set_sensitive(True)
771 def on_dual_action_setting_changed( self
, *args
):
772 for button
in self
.forward_button
, self
.rewind_button
, \
773 self
.fforward_button
, self
.rrewind_button
, \
774 self
.bookmarks_button
:
776 button
.set_longpress_enabled( settings
.enable_dual_action_btn
)
777 button
.set_duration( settings
.dual_action_button_delay
)
779 def on_key_press(self
, widget
, event
):
781 if event
.keyval
== gtk
.keysyms
.Left
: # seek back
782 self
.do_seek( -1 * settings
.seek_long
)
783 elif event
.keyval
== gtk
.keysyms
.Right
: # seek forward
784 self
.do_seek( settings
.seek_long
)
785 elif event
.keyval
== gtk
.keysyms
.Return
: # play/pause
786 self
.on_btn_play_pause_clicked()
788 def on_player_stopped(self
):
789 self
.stop_progress_timer()
790 self
.set_controls_sensitivity(False)
791 image(self
.play_pause_button
, 'media-playback-start.png')
793 def on_player_playing(self
):
794 self
.start_progress_timer()
795 image(self
.play_pause_button
, 'media-playback-pause.png')
796 self
.set_controls_sensitivity(True)
797 if platform
.FREMANTLE
:
798 hildon
.hildon_gtk_window_set_progress_indicator(\
799 self
.__gui
_root
.main_window
, False)
801 def on_player_new_track(self
):
802 for widget
in [self
.title_label
,self
.artist_label
,self
.album_label
]:
803 widget
.set_markup('')
806 self
.cover_art
.hide()
807 self
.has_coverart
= False
809 def on_player_new_metadata(self
):
810 metadata
= player
.playlist
.get_file_metadata()
811 self
.set_metadata(metadata
)
813 if not player
.playing
:
814 position
= player
.playlist
.get_current_position()
815 estimated_length
= metadata
.get('length', 0)
816 self
.set_progress_callback( position
, estimated_length
)
818 def on_player_paused( self
, position
, duration
):
819 self
.stop_progress_timer() # This should save some power
820 self
.set_progress_callback( position
, duration
)
821 image(self
.play_pause_button
, 'media-playback-start.png')
823 def on_player_end_of_playlist(self
, loop
):
826 def reset_progress(self
):
827 self
.progress
.set_fraction(0)
828 self
.set_progress_callback(0,0)
830 def set_progress_callback(self
, time_elapsed
, total_time
):
831 """ times must be in nanoseconds """
832 time_string
= "%s / %s" % ( util
.convert_ns(time_elapsed
),
833 util
.convert_ns(total_time
) )
834 self
.progress
.set_text( time_string
)
835 fraction
= float(time_elapsed
) / float(total_time
) if total_time
else 0
836 self
.progress
.set_fraction( fraction
)
838 def on_progressbar_changed(self
, widget
, event
):
839 if ( not settings
.progress_locked
and
840 event
.type == gtk
.gdk
.BUTTON_PRESS
and event
.button
== 1 ):
841 new_fraction
= event
.x
/float(widget
.get_allocation().width
)
842 resp
= player
.do_seek(percent
=new_fraction
)
844 # Preemptively update the progressbar to make seeking smoother
845 self
.set_progress_callback( *resp
)
847 def on_btn_play_pause_clicked(self
, widget
=None):
848 player
.play_pause_toggle()
850 def progress_timer_callback( self
):
851 if player
.playing
and not player
.seeking
:
852 pos_int
, dur_int
= player
.get_position_duration()
853 # This prevents bogus values from being set while seeking
854 if ( pos_int
> 10**9 ) and ( dur_int
> 10**9 ):
855 self
.set_progress_callback( pos_int
, dur_int
)
858 def start_progress_timer( self
):
859 if self
.progress_timer_id
is not None:
860 self
.stop_progress_timer()
862 self
.progress_timer_id
= gobject
.timeout_add(
863 1000, self
.progress_timer_callback
)
865 def stop_progress_timer( self
):
866 if self
.progress_timer_id
is not None:
867 gobject
.source_remove( self
.progress_timer_id
)
868 self
.progress_timer_id
= None
870 def get_coverart_size( self
):
872 if self
.__gui
_root
.fullscreen
:
873 size
= coverart_sizes
['maemo fullscreen']
875 size
= coverart_sizes
['maemo']
877 size
= coverart_sizes
['normal']
881 def set_coverart( self
, pixbuf
):
882 self
.cover_art
.set_from_pixbuf(pixbuf
)
883 self
.cover_art
.show()
884 self
.has_coverart
= True
886 def set_metadata( self
, tag_message
):
887 tags
= { 'title': self
.title_label
, 'artist': self
.artist_label
,
888 'album': self
.album_label
}
891 if tag_message
.has_key('image') and tag_message
['image'] is not None:
892 value
= tag_message
['image']
894 pbl
= gtk
.gdk
.PixbufLoader()
899 x
, y
= self
.get_coverart_size()
900 pixbuf
= pbl
.get_pixbuf()
901 pixbuf
= pixbuf
.scale_simple( x
, y
, gtk
.gdk
.INTERP_BILINEAR
)
902 self
.set_coverart(pixbuf
)
904 self
.__log
.exception('Error setting coverart...')
906 # set the text metadata
907 for tag
,value
in tag_message
.iteritems():
908 if tags
.has_key(tag
) and value
is not None and value
.strip():
910 tags
[tag
].set_markup('<big>'+cgi
.escape(value
)+'</big>')
912 self
.__log
.exception(str(e
))
913 tags
[tag
].set_alignment( 0.5*int(not self
.has_coverart
), 0.5)
917 # make the title bold
918 tags
[tag
].set_markup('<b><big>'+cgi
.escape(value
)+'</big></b>')
920 if not platform
.MAEMO
:
921 value
+= ' - Panucci'
923 if platform
.FREMANTLE
and len(value
) > 25:
924 value
= value
[:24] + '...'
926 self
.__gui
_root
.main_window
.set_title( value
)
928 def do_seek(self
, seek_amount
):
929 resp
= player
.do_seek(from_current
=seek_amount
*10**9)
931 # Preemptively update the progressbar to make seeking smoother
932 self
.set_progress_callback( *resp
)
936 ##################################################
938 ##################################################
939 class PlaylistTab(gtk
.VBox
):
940 def __init__(self
, main_window
):
941 gtk
.VBox
.__init
__(self
)
942 self
.__log
= logging
.getLogger('panucci.panucci.BookmarksWindow')
943 self
.main
= main_window
945 self
.__model
= gtk
.TreeStore(
946 # uid, name, position
947 gobject
.TYPE_STRING
, gobject
.TYPE_STRING
, gobject
.TYPE_STRING
)
950 self
.treeview
= gtk
.TreeView()
951 self
.treeview
.set_model(self
.__model
)
952 self
.treeview
.set_headers_visible(True)
953 tree_selection
= self
.treeview
.get_selection()
954 # This breaks drag and drop, only use single selection for now
955 # tree_selection.set_mode(gtk.SELECTION_MULTIPLE)
956 tree_selection
.connect('changed', self
.tree_selection_changed
)
958 # The tree lines look nasty on maemo
960 self
.treeview
.set_enable_tree_lines(True)
963 ncol
= gtk
.TreeViewColumn(_('Name'))
964 ncell
= gtk
.CellRendererText()
965 ncell
.set_property('ellipsize', pango
.ELLIPSIZE_END
)
966 ncell
.set_property('editable', True)
967 ncell
.connect('edited', self
.label_edited
)
968 ncol
.set_expand(True)
969 ncol
.pack_start(ncell
)
970 ncol
.add_attribute(ncell
, 'text', 1)
972 tcol
= gtk
.TreeViewColumn(_('Position'))
973 tcell
= gtk
.CellRendererText()
974 tcol
.pack_start(tcell
)
975 tcol
.add_attribute(tcell
, 'text', 2)
977 self
.treeview
.append_column(ncol
)
978 self
.treeview
.append_column(tcol
)
979 self
.treeview
.connect('drag-data-received', self
.drag_data_recieved
)
980 self
.treeview
.connect('drag_data_get', self
.drag_data_get_data
)
983 ( 'playlist_row_data', gtk
.TARGET_SAME_WIDGET
, 0 ) ]
985 self
.treeview
.enable_model_drag_source(
986 gtk
.gdk
.BUTTON1_MASK
, treeview_targets
, gtk
.gdk
.ACTION_COPY
)
988 self
.treeview
.enable_model_drag_dest(
989 treeview_targets
, gtk
.gdk
.ACTION_COPY
)
991 sw
= gtk
.ScrolledWindow()
992 sw
.set_policy(gtk
.POLICY_AUTOMATIC
, gtk
.POLICY_AUTOMATIC
)
993 sw
.set_shadow_type(gtk
.SHADOW_IN
)
994 sw
.add(self
.treeview
)
997 self
.hbox
= gtk
.HBox()
999 self
.add_button
= gtk
.Button(gtk
.STOCK_NEW
)
1000 self
.add_button
.set_use_stock(True)
1001 set_stock_button_text( self
.add_button
, _('Add File') )
1002 self
.add_button
.connect('clicked', self
.add_file
)
1003 self
.hbox
.pack_start(self
.add_button
, True, True)
1005 self
.dir_button
= gtk
.Button(gtk
.STOCK_OPEN
)
1006 self
.dir_button
.set_use_stock(True)
1007 set_stock_button_text( self
.dir_button
, _('Add Directory') )
1008 self
.dir_button
.connect('clicked', self
.add_directory
)
1009 self
.hbox
.pack_start(self
.dir_button
, True, True)
1011 self
.remove_button
= gtk
.Button(gtk
.STOCK_REMOVE
)
1012 self
.remove_button
.set_use_stock(True)
1013 self
.remove_button
.connect('clicked', self
.remove_bookmark
)
1014 self
.hbox
.pack_start(self
.remove_button
, True, True)
1016 self
.jump_button
= gtk
.Button(gtk
.STOCK_JUMP_TO
)
1017 self
.jump_button
.set_use_stock(True)
1018 self
.jump_button
.connect('clicked', self
.jump_bookmark
)
1019 self
.hbox
.pack_start(self
.jump_button
, True, True)
1021 self
.info_button
= gtk
.Button()
1022 self
.info_button
.add(
1023 gtk
.image_new_from_stock(gtk
.STOCK_INFO
, gtk
.ICON_SIZE_BUTTON
))
1024 self
.info_button
.connect('clicked', self
.show_playlist_item_details
)
1025 self
.hbox
.pack_start(self
.info_button
, True, True)
1027 if platform
.FREMANTLE
:
1028 for child
in self
.hbox
.get_children():
1029 if isinstance(child
, gtk
.Button
):
1030 child
.set_name('HildonButton-thumb')
1031 self
.hbox
.set_size_request(-1, 105)
1033 self
.pack_start(self
.hbox
, False, True)
1035 player
.playlist
.register( 'file_queued',
1036 lambda x
,y
,z
: self
.update_model() )
1037 player
.playlist
.register( 'bookmark_added', self
.on_bookmark_added
)
1041 def tree_selection_changed(self
, treeselection
):
1042 count
= treeselection
.count_selected_rows()
1043 self
.remove_button
.set_sensitive(count
> 0)
1044 self
.jump_button
.set_sensitive(count
== 1)
1045 self
.info_button
.set_sensitive(count
== 1)
1047 def drag_data_get_data(
1048 self
, treeview
, context
, selection
, target_id
, timestamp
):
1050 treeselection
= treeview
.get_selection()
1051 model
, iter = treeselection
.get_selected()
1052 # only allow moving around top-level parents
1053 if model
.iter_parent(iter) is None:
1054 # send the path of the selected row
1055 data
= model
.get_string_from_iter(iter)
1056 selection
.set(selection
.target
, 8, data
)
1058 self
.__log
.debug("Can't move children...")
1060 def drag_data_recieved(
1061 self
, treeview
, context
, x
, y
, selection
, info
, timestamp
):
1063 drop_info
= treeview
.get_dest_row_at_pos(x
, y
)
1065 # TODO: If user drags the row past the last row, drop_info is None
1066 # I'm not sure if it's safe to simply assume that None is
1067 # euqivalent to the last row...
1068 if None not in [ drop_info
and selection
.data
]:
1069 model
= treeview
.get_model()
1070 path
, position
= drop_info
1072 from_iter
= model
.get_iter_from_string(selection
.data
)
1074 # make sure the to_iter doesn't have a parent
1075 to_iter
= model
.get_iter(path
)
1076 if model
.iter_parent(to_iter
) is not None:
1077 to_iter
= model
.iter_parent(to_iter
)
1079 from_row
= model
.get_path(from_iter
)[0]
1082 if ( position
== gtk
.TREE_VIEW_DROP_BEFORE
or
1083 position
== gtk
.TREE_VIEW_DROP_INTO_OR_BEFORE
):
1084 model
.move_before( from_iter
, to_iter
)
1085 to_row
= to_row
- 1 if from_row
< to_row
else to_row
1086 elif ( position
== gtk
.TREE_VIEW_DROP_AFTER
or
1087 position
== gtk
.TREE_VIEW_DROP_INTO_OR_AFTER
):
1088 model
.move_after( from_iter
, to_iter
)
1089 to_row
= to_row
+ 1 if from_row
> to_row
else to_row
1091 self
.__log
.debug('Drop not supported: %s', position
)
1093 # don't do anything if we're not actually moving rows around
1094 if from_row
!= to_row
:
1095 player
.playlist
.move_item( from_row
, to_row
)
1098 self
.__log
.debug('No drop_data or selection.data available')
1100 def update_model(self
):
1101 plist
= player
.playlist
1102 path_info
= self
.treeview
.get_path_at_pos(0,0)
1103 path
= path_info
[0] if path_info
is not None else None
1105 self
.__model
.clear()
1108 for item
, data
in plist
.get_playlist_item_ids():
1109 parent
= self
.__model
.append(None, (item
, data
.get('title'), None))
1111 for bid
, bname
, bpos
in plist
.get_bookmarks_from_item_id( item
):
1112 nice_bpos
= util
.convert_ns(bpos
)
1113 self
.__model
.append( parent
, (bid
, bname
, nice_bpos
) )
1115 self
.treeview
.expand_all()
1117 if path
is not None:
1118 self
.treeview
.scroll_to_cell(path
)
1120 def label_edited(self
, cellrenderer
, path
, new_text
):
1121 iter = self
.__model
.get_iter(path
)
1122 old_text
= self
.__model
.get_value(iter, 1)
1124 if new_text
.strip() and old_text
!= new_text
:
1125 # this loop will only run once, because only one cell can be
1126 # edited at a time, we use it to get the item and bookmark ids
1127 for m
, bkmk_id
, biter
, item_id
, iiter
in self
.__cur
_selection
():
1128 self
.__model
.set_value(iter, 1, new_text
)
1129 player
.playlist
.update_bookmark(
1130 item_id
, bkmk_id
, name
=new_text
)
1132 self
.__model
.set_value(iter, 1, old_text
)
1134 def on_bookmark_added(self
, parent_id
, bookmark_name
, position
):
1135 self
.main
.notify(_('Bookmark added: %s') % bookmark_name
)
1138 def add_file(self
, widget
):
1139 filename
= get_file_from_filechooser(self
.main
.main_window
)
1140 if filename
is not None:
1141 player
.playlist
.append(filename
)
1143 def add_directory(self
, widget
):
1144 directory
= get_file_from_filechooser(
1145 self
.main
.main_window
, folder
=True )
1146 if directory
is not None:
1147 player
.playlist
.load_directory(directory
, append
=True)
1149 def __cur_selection(self
):
1150 selection
= self
.treeview
.get_selection()
1151 model
, bookmark_paths
= selection
.get_selected_rows()
1153 # Convert the paths to gtk.TreeRowReference objects, because we
1154 # might modify the model while this generator is running
1155 bookmark_refs
= [gtk
.TreeRowReference(model
, p
) for p
in bookmark_paths
]
1157 for reference
in bookmark_refs
:
1158 bookmark_iter
= model
.get_iter(reference
.get_path())
1159 item_iter
= model
.iter_parent(bookmark_iter
)
1161 # bookmark_iter is actually an item_iter
1162 if item_iter
is None:
1163 item_iter
= bookmark_iter
1164 item_id
= model
.get_value(item_iter
, 0)
1165 bookmark_id
, bookmark_iter
= None, None
1167 bookmark_id
= model
.get_value(bookmark_iter
, 0)
1168 item_id
= model
.get_value(item_iter
, 0)
1170 yield model
, bookmark_id
, bookmark_iter
, item_id
, item_iter
1172 def remove_bookmark(self
, w
=None):
1173 for model
, bkmk_id
, bkmk_iter
, item_id
, item_iter
in self
.__cur
_selection
():
1174 player
.playlist
.remove_bookmark( item_id
, bkmk_id
)
1175 if bkmk_iter
is not None:
1176 model
.remove(bkmk_iter
)
1177 elif item_iter
is not None:
1178 model
.remove(item_iter
)
1180 def select_current_item(self
):
1181 model
= self
.treeview
.get_model()
1182 selection
= self
.treeview
.get_selection()
1183 current_item_id
= str(player
.playlist
.get_current_item())
1184 for row
in iter(model
):
1185 if model
.get_value(row
.iter, 0) == current_item_id
:
1186 selection
.unselect_all()
1187 self
.treeview
.set_cursor(row
.path
)
1188 self
.treeview
.scroll_to_cell(row
.path
, use_align
=True)
1191 def show_playlist_item_details(self
, w
):
1192 selection
= self
.treeview
.get_selection()
1193 if selection
.count_selected_rows() == 1:
1194 selected
= self
.__cur
_selection
().next()
1195 model
, bkmk_id
, bkmk_iter
, item_id
, item_iter
= selected
1196 playlist_item
= player
.playlist
.get_item_by_id(item_id
)
1197 PlaylistItemDetails(self
.main
, playlist_item
)
1199 def jump_bookmark(self
, w
):
1200 selected
= list(self
.__cur
_selection
())
1201 if len(selected
) == 1:
1202 # It should be guranteed by the fact that we only enable the
1203 # "Jump to" button when the selection count equals 1.
1204 model
, bkmk_id
, bkmk_iter
, item_id
, item_iter
= selected
.pop(0)
1205 player
.playlist
.load_from_bookmark_id(item_id
, bkmk_id
)
1207 # FIXME: The player/playlist should be able to take care of this
1208 if not player
.playing
:
1212 ##################################################
1213 # PlaylistItemDetails
1214 ##################################################
1215 class PlaylistItemDetails(gtk
.Dialog
):
1216 def __init__(self
, main
, playlist_item
):
1217 gtk
.Dialog
.__init
__(self
, _('Playlist item details'),
1218 main
.main_window
, gtk
.DIALOG_MODAL
)
1220 if not platform
.FREMANTLE
:
1221 self
.add_button(gtk
.STOCK_CLOSE
, gtk
.RESPONSE_OK
)
1224 self
.fill(playlist_item
)
1225 self
.set_has_separator(False)
1226 self
.set_resizable(False)
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)
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 )
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)
1258 for key
in metadata
:
1259 value
= metadata
[key
]
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)
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)
1276 cr
= gtk
.CellRendererText()
1277 cr
.set_property('ellipsize', pango
.ELLIPSIZE_END
)
1278 c
= gtk
.TreeViewColumn(_('Title'), cr
, text
=0)
1281 c
= gtk
.TreeViewColumn(_('Time'), gtk
.CellRendererText(), text
=1)
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
)
1291 sw
.set_policy(gtk
.POLICY_AUTOMATIC
, gtk
.POLICY_AUTOMATIC
)
1292 e
= gtk
.Expander(_('Bookmarks'))
1294 if not platform
.MAEMO
:
1295 self
.vbox
.pack_start(e
)
1298 def run(filename
=None):
1299 PanucciGUI(filename
)
1302 if __name__
== '__main__':
1303 log
.error( 'Use the "panucci" executable to run this program.' )
1304 log
.error( 'Exiting...' )