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
39 from panucci
import widgets
40 from panucci
import util
42 log
= logging
.getLogger('panucci.panucci')
48 if util
.platform
.MAEMO
:
49 log
.critical( 'Using GTK widgets, install "python2.5-hildon" '
50 'for this to work properly.' )
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/'
65 'maemo fullscreen' : 275,
68 gtk
.about_dialog_set_url_hook(util
.open_link
, None)
69 gtk
.icon_size_register('panucci-button', 32, 32)
71 def generate_image(filename
, is_stock
=False):
74 image
= gtk
.image_new_from_stock(
75 filename
, gtk
.icon_size_from_name('panucci-button') )
77 filename
= util
.find_image(filename
)
78 if filename
is not None:
79 image
= gtk
.image_new_from_file(filename
)
81 if util
.platform
.MAEMO
:
82 image
.set_padding(20, 20)
84 image
.set_padding(5, 5)
88 def image(widget
, filename
, is_stock
=False):
89 child
= widget
.get_child()
92 image
= generate_image(filename
, is_stock
)
96 def dialog( toplevel_window
, title
, question
, description
,
97 affirmative_button
=gtk
.STOCK_YES
, negative_button
=gtk
.STOCK_NO
,
98 abortion_button
=gtk
.STOCK_CANCEL
):
100 """Present the user with a yes/no/cancel dialog.
101 The return value is either True, False or None, depending on which
102 button has been pressed in the dialog:
104 affirmative button (default: Yes) => True
105 negative button (defaut: No) => False
106 abortion button (default: Cancel) => None
108 When the dialog is closed with the "X" button in the window manager
109 decoration, the return value is always None (same as abortion button).
111 You can set any of the affirmative_button, negative_button or
112 abortion_button values to "None" to hide the corresponding action.
114 dlg
= gtk
.MessageDialog( toplevel_window
, gtk
.DIALOG_MODAL
,
115 gtk
.MESSAGE_QUESTION
, message_format
=question
)
119 if abortion_button
is not None:
120 dlg
.add_button(abortion_button
, gtk
.RESPONSE_CANCEL
)
121 if negative_button
is not None:
122 dlg
.add_button(negative_button
, gtk
.RESPONSE_NO
)
123 if affirmative_button
is not None:
124 dlg
.add_button(affirmative_button
, gtk
.RESPONSE_YES
)
126 dlg
.format_secondary_text(description
)
131 if response
== gtk
.RESPONSE_YES
:
133 elif response
== gtk
.RESPONSE_NO
:
135 elif response
in [gtk
.RESPONSE_CANCEL
, gtk
.RESPONSE_DELETE_EVENT
]:
138 def get_file_from_filechooser(
139 toplevel_window
, folder
=False, save_file
=False, save_to
=None):
142 open_action
= gtk
.FILE_CHOOSER_ACTION_SELECT_FOLDER
144 open_action
= gtk
.FILE_CHOOSER_ACTION_OPEN
146 if util
.platform
.MAEMO
:
148 args
= ( toplevel_window
, gtk
.FILE_CHOOSER_ACTION_SAVE
)
150 args
= ( toplevel_window
, open_action
)
152 dlg
= hildon
.FileChooserDialog( *args
)
155 args
= ( _('Select file to save playlist to'), None,
156 gtk
.FILE_CHOOSER_ACTION_SAVE
,
157 (( gtk
.STOCK_CANCEL
, gtk
.RESPONSE_REJECT
,
158 gtk
.STOCK_SAVE
, gtk
.RESPONSE_OK
)) )
160 args
= ( _('Select podcast or audiobook'), None, open_action
,
161 (( gtk
.STOCK_CANCEL
, gtk
.RESPONSE_REJECT
,
162 gtk
.STOCK_OPEN
, gtk
.RESPONSE_OK
)) )
164 dlg
= gtk
.FileChooserDialog(*args
)
166 current_folder
= os
.path
.expanduser(settings
.last_folder
)
168 if current_folder
is not None and os
.path
.isdir(current_folder
):
169 dlg
.set_current_folder(current_folder
)
171 if save_file
and save_to
is not None:
172 dlg
.set_current_name(save_to
)
174 if dlg
.run() == gtk
.RESPONSE_OK
:
175 filename
= dlg
.get_filename()
176 settings
.last_folder
= dlg
.get_current_folder()
183 def set_stock_button_text( button
, text
):
184 alignment
= button
.get_child()
185 hbox
= alignment
.get_child()
186 image
, label
= hbox
.get_children()
189 ##################################################
191 ##################################################
192 class PanucciGUI(object):
193 """ The object that holds the entire panucci gui """
195 def __init__(self
, filename
=None):
196 self
.__log
= logging
.getLogger('panucci.panucci.PanucciGUI')
197 interface
.register_gui(self
)
199 # Build the base ui (window and menubar)
200 if util
.platform
.MAEMO
:
201 self
.app
= hildon
.Program()
202 window
= hildon
.Window()
203 self
.app
.add_window(window
)
205 window
= gtk
.Window(gtk
.WINDOW_TOPLEVEL
)
207 self
.main_window
= window
208 window
.set_title('Panucci')
209 self
.window_icon
= util
.find_image('panucci.png')
210 if self
.window_icon
is not None:
211 window
.set_icon_from_file( self
.window_icon
)
212 window
.set_default_size(400, -1)
213 window
.set_border_width(0)
214 window
.connect("destroy", self
.destroy
)
216 # Add the tabs (they are private to prevent us from trying to do
217 # something like gui_root.player_tab.some_function() from inside
218 # playlist_tab or vice-versa)
219 self
.__player
_tab
= PlayerTab(self
)
220 self
.__playlist
_tab
= PlaylistTab(self
)
222 self
.playlist_window
= gtk
.Window(gtk
.WINDOW_TOPLEVEL
)
223 self
.playlist_window
.connect('delete-event', gtk
.Widget
.hide_on_delete
)
224 self
.playlist_window
.set_title(_('Panucci playlist'))
225 self
.playlist_window
.set_transient_for(self
.main_window
)
226 self
.playlist_window
.add(self
.__playlist
_tab
)
228 self
.create_actions()
230 if util
.platform
.MAEMO
:
231 window
.set_menu(self
.create_menu())
232 window
.add(self
.__player
_tab
)
234 menu_vbox
= gtk
.VBox()
235 menu_vbox
.set_spacing(0)
236 window
.add(menu_vbox
)
237 menu_bar
= gtk
.MenuBar()
238 self
.create_desktop_menu(menu_bar
)
239 menu_vbox
.pack_start(menu_bar
, False, False, 0)
241 menu_vbox
.pack_end(self
.__player
_tab
, True, True, 6)
243 # Tie it all together!
244 self
.__ignore
_queue
_check
= False
245 self
.__window
_fullscreen
= False
247 if util
.platform
.MAEMO
and interface
.headset_device
is not None:
248 # Enable play/pause with headset button
249 interface
.headset_device
.connect_to_signal(
250 'Condition', self
.handle_headset_button
)
252 self
.main_window
.connect('key-press-event', self
.on_key_press
)
253 player
.playlist
.register( 'file_queued', self
.on_file_queued
)
255 player
.playlist
.register( 'playlist-to-be-overwritten',
257 self
.__player
_tab
.register( 'select-current-item-request',
258 self
.__select
_current
_item
)
260 self
.main_window
.show_all()
262 # this should be done when the gui is ready
263 player
.init(filepath
=filename
)
265 def create_actions(self
):
266 self
.action_open
= gtk
.Action('open', _('Open'), _('Open a file or playlist'), gtk
.STOCK_OPEN
)
267 self
.action_open
.connect('activate', self
.open_file_callback
)
268 self
.action_save
= gtk
.Action('save', _('Save playlist'), _('Save current playlist to file'), gtk
.STOCK_SAVE_AS
)
269 self
.action_save
.connect('activate', self
.save_to_playlist_callback
)
270 self
.action_playlist
= gtk
.Action('playlist', _('Playlist'), _('Open the current playlist'), None)
271 self
.action_playlist
.connect('activate', lambda a
: self
.playlist_window
.show())
272 self
.action_about
= gtk
.Action('about', _('About Panucci'), _('Show application version'), gtk
.STOCK_ABOUT
)
273 self
.action_about
.connect('activate', self
.about_callback
)
274 self
.action_quit
= gtk
.Action('quit', _('Quit'), _('Close Panucci'), gtk
.STOCK_QUIT
)
275 self
.action_quit
.connect('activate', self
.destroy
)
277 def create_desktop_menu(self
, menu_bar
):
278 file_menu_item
= gtk
.MenuItem(_('File'))
279 file_menu
= gtk
.Menu()
280 file_menu
.append(self
.action_open
.create_menu_item())
281 file_menu
.append(self
.action_save
.create_menu_item())
282 file_menu
.append(gtk
.SeparatorMenuItem())
283 file_menu
.append(self
.action_quit
.create_menu_item())
284 file_menu_item
.set_submenu(file_menu
)
285 menu_bar
.append(file_menu_item
)
287 tools_menu_item
= gtk
.MenuItem(_('Tools'))
288 tools_menu
= gtk
.Menu()
289 tools_menu
.append(self
.action_playlist
.create_menu_item())
290 tools_menu_item
.set_submenu(tools_menu
)
291 menu_bar
.append(tools_menu_item
)
293 help_menu_item
= gtk
.MenuItem(_('Help'))
294 help_menu
= gtk
.Menu()
295 help_menu
.append(self
.action_about
.create_menu_item())
296 help_menu_item
.set_submenu(help_menu
)
297 menu_bar
.append(help_menu_item
)
299 def create_menu(self
):
303 menu_open
= gtk
.ImageMenuItem(_('Open playlist'))
305 gtk
.image_new_from_stock(gtk
.STOCK_OPEN
, gtk
.ICON_SIZE_MENU
))
306 menu_open
.connect("activate", self
.open_file_callback
)
307 menu
.append(menu_open
)
309 # the recent files menu
310 self
.menu_recent
= gtk
.MenuItem(_('Open recent playlist'))
311 menu
.append(self
.menu_recent
)
312 self
.create_recent_files_menu()
314 menu
.append(gtk
.SeparatorMenuItem())
316 menu_save
= gtk
.ImageMenuItem(_('Save current playlist'))
318 gtk
.image_new_from_stock(gtk
.STOCK_SAVE_AS
, gtk
.ICON_SIZE_MENU
))
319 menu_save
.connect("activate", self
.save_to_playlist_callback
)
320 menu
.append(menu_save
)
322 menu
.append(gtk
.SeparatorMenuItem())
324 # the settings sub-menu
325 menu_settings
= gtk
.MenuItem(_('Settings'))
326 menu
.append(menu_settings
)
328 menu_settings_sub
= gtk
.Menu()
329 menu_settings
.set_submenu(menu_settings_sub
)
331 menu_settings_enable_dual_action
= gtk
.CheckMenuItem(
332 _('Enable dual-action buttons') )
333 settings
.attach_checkbutton( menu_settings_enable_dual_action
,
334 'enable_dual_action_btn' )
335 menu_settings_sub
.append(menu_settings_enable_dual_action
)
337 menu_settings_lock_progress
= gtk
.CheckMenuItem(_('Lock Progress Bar'))
338 settings
.attach_checkbutton( menu_settings_lock_progress
,
340 menu_settings_sub
.append(menu_settings_lock_progress
)
342 menu_about
= gtk
.ImageMenuItem(gtk
.STOCK_ABOUT
)
343 menu_about
.connect("activate", self
.about_callback
)
344 menu
.append(menu_about
)
346 menu
.append(gtk
.SeparatorMenuItem())
348 menu_quit
= gtk
.ImageMenuItem(gtk
.STOCK_QUIT
)
349 menu_quit
.connect("activate", self
.destroy
)
350 menu
.append(menu_quit
)
354 def create_recent_files_menu( self
):
355 max_files
= settings
.max_recent_files
356 self
.recent_files
= player
.playlist
.get_recent_files(max_files
)
357 menu_recent_sub
= gtk
.Menu()
359 if len(self
.recent_files
) > 0:
360 for f
in self
.recent_files
:
361 # don't include the temporary playlist in the file list
362 if f
== panucci
.PLAYLIST_FILE
: continue
363 # don't include non-existant files
364 if not os
.path
.exists( f
): continue
365 filename
, extension
= os
.path
.splitext(os
.path
.basename(f
))
366 menu_item
= gtk
.MenuItem( filename
.replace('_', ' '))
367 menu_item
.connect('activate', self
.on_recent_file_activate
, f
)
368 menu_recent_sub
.append(menu_item
)
370 menu_item
= gtk
.MenuItem(_('No recent files available.'))
371 menu_item
.set_sensitive(False)
372 menu_recent_sub
.append(menu_item
)
374 self
.menu_recent
.set_submenu(menu_recent_sub
)
376 def destroy(self
, widget
):
380 def show_main_window(self
):
381 self
.main_window
.present()
383 def check_queue(self
):
384 """ Makes sure the queue is saved if it has been modified
385 True means a new file can be opened
386 False means the user does not want to continue """
388 if not self
.__ignore
_queue
_check
and player
.playlist
.queue_modified
:
390 self
.main_window
, _('Save current playlist'),
391 _('Current playlist has been modified'),
392 _('Opening a new file will replace the current playlist. ') +
393 _('Do you want to save it before creating a new one?'),
394 affirmative_button
=gtk
.STOCK_SAVE
,
395 negative_button
=_('Discard changes'))
397 self
.__log
.debug('Response to "Save Queue?": %s', response
)
402 return self
.save_to_playlist_callback()
410 def open_file_callback(self
, widget
=None):
411 if self
.check_queue():
412 # set __ingnore__queue_check because we already did the check
413 self
.__ignore
_queue
_check
= True
414 filename
= get_file_from_filechooser(self
.main_window
)
415 if filename
is not None:
416 self
._play
_file
(filename
)
418 self
.__ignore
_queue
_check
= False
420 def save_to_playlist_callback(self
, widget
=None):
421 filename
= get_file_from_filechooser(
422 self
.main_window
, save_file
=True, save_to
='playlist.m3u' )
427 if os
.path
.isfile(filename
):
428 response
= dialog( self
.main_window
, _('File already exists'),
429 _('File already exists'),
430 _('The file %s already exists. You can choose another name or '
431 'overwrite the existing file.') % os
.path
.basename(filename
),
432 affirmative_button
=gtk
.STOCK_SAVE
,
433 negative_button
=_('Rename file'))
441 return self
.save_to_playlist_callback()
443 ext
= util
.detect_filetype(filename
)
444 if not player
.playlist
.save_to_new_playlist(filename
, ext
):
445 util
.notify(_('Error saving playlist...'))
450 def __get_fullscreen(self
):
451 return self
.__window
_fullscreen
453 def __set_fullscreen(self
, value
):
454 if value
!= self
.__window
_fullscreen
:
456 self
.main_window
.fullscreen()
458 self
.main_window
.unfullscreen()
460 self
.__window
_fullscreen
= value
461 player
.playlist
.send_metadata()
463 fullscreen
= property( __get_fullscreen
, __set_fullscreen
)
465 def on_key_press(self
, widget
, event
):
466 if util
.platform
.MAEMO
:
467 if event
.keyval
== gtk
.keysyms
.F6
:
468 self
.fullscreen
= not self
.fullscreen
470 def on_recent_file_activate(self
, widget
, filepath
):
471 self
._play
_file
(filepath
)
473 def on_file_queued(self
, filepath
, success
, notify
):
475 filename
= os
.path
.basename(filepath
)
478 util
.notify( '%s added successfully.' % filename
))
481 util
.notify( 'Error adding %s to the queue.' % filename
))
483 def about_callback(self
, widget
):
484 dialog
= gtk
.AboutDialog()
485 dialog
.set_website(about_website
)
486 dialog
.set_website_label(about_website
)
487 dialog
.set_name(about_name
)
488 dialog
.set_authors(about_authors
)
489 dialog
.set_comments(about_text
)
490 dialog
.set_version(panucci
.__version
__)
494 def _play_file(self
, filename
, pause_on_load
=False):
495 player
.playlist
.load( os
.path
.abspath(filename
) )
497 if player
.playlist
.is_empty
:
500 def handle_headset_button(self
, event
, button
):
501 if event
== 'ButtonPressed' and button
== 'phone':
502 player
.play_pause_toggle()
504 def __select_current_item( self
):
505 # Select the currently playing track in the playlist tab
506 # and switch to it (so we can edit bookmarks, etc.. there)
507 self
.__playlist
_tab
.select_current_item()
508 self
.playlist_window
.show()
510 ##################################################
512 ##################################################
513 class PlayerTab(ObservableService
, gtk
.HBox
):
514 """ The tab that holds the player elements """
516 signals
= [ 'select-current-item-request', ]
518 def __init__(self
, gui_root
):
519 self
.__log
= logging
.getLogger('panucci.panucci.PlayerTab')
520 self
.__gui
_root
= gui_root
522 gtk
.HBox
.__init
__(self
)
523 ObservableService
.__init
__(self
, self
.signals
, self
.__log
)
526 self
.progress_timer_id
= None
527 self
.volume_timer_id
= None
529 self
.recent_files
= []
530 self
.make_player_tab()
531 self
.has_coverart
= False
532 self
.set_volume(settings
.volume
)
534 #settings.register( 'enable_dual_action_btn_changed',
535 # self.on_dual_action_setting_changed )
536 #settings.register( 'dual_action_button_delay_changed',
537 # self.on_dual_action_setting_changed )
538 #settings.register( 'volume_changed', self.set_volume )
539 #settings.register( 'scrolling_labels_changed', lambda v:
540 # setattr( self.title_label, 'scrolling', v ) )
542 player
.register( 'stopped', self
.on_player_stopped
)
543 player
.register( 'playing', self
.on_player_playing
)
544 player
.register( 'paused', self
.on_player_paused
)
545 player
.playlist
.register( 'end-of-playlist',
546 self
.on_player_end_of_playlist
)
547 player
.playlist
.register( 'new-track-loaded',
548 self
.on_player_new_track
)
549 player
.playlist
.register( 'new-metadata-available',
550 self
.on_player_new_metadata
)
552 def make_player_tab(self
):
553 main_vbox
= gtk
.VBox()
554 main_vbox
.set_spacing(6)
556 self
.pack_start(main_vbox
, True, True)
558 # a hbox to hold the cover art and metadata vbox
559 metadata_hbox
= gtk
.HBox()
560 metadata_hbox
.set_spacing(6)
561 main_vbox
.pack_start(metadata_hbox
, True, False)
563 self
.cover_art
= gtk
.Image()
564 metadata_hbox
.pack_start( self
.cover_art
, False, False )
566 # vbox to hold metadata
567 metadata_vbox
= gtk
.VBox()
568 metadata_vbox
.set_spacing(8)
569 empty_label
= gtk
.Label()
570 metadata_vbox
.pack_start(empty_label
, True, True)
571 self
.artist_label
= gtk
.Label('')
572 self
.artist_label
.set_ellipsize(pango
.ELLIPSIZE_END
)
573 metadata_vbox
.pack_start(self
.artist_label
, False, False)
574 self
.album_label
= gtk
.Label('')
575 self
.album_label
.set_ellipsize(pango
.ELLIPSIZE_END
)
576 metadata_vbox
.pack_start(self
.album_label
, False, False)
577 self
.title_label
= widgets
.ScrollingLabel( '',
580 delay_btwn_scrolls
=5000,
582 self
.title_label
.scrolling
= settings
.scrolling_labels
583 metadata_vbox
.pack_start(self
.title_label
, False, False)
584 empty_label
= gtk
.Label()
585 metadata_vbox
.pack_start(empty_label
, True, True)
586 metadata_hbox
.pack_start( metadata_vbox
, True, True )
588 progress_eventbox
= gtk
.EventBox()
589 progress_eventbox
.set_events(gtk
.gdk
.BUTTON_PRESS_MASK
)
590 progress_eventbox
.connect(
591 'button-press-event', self
.on_progressbar_changed
)
592 self
.progress
= gtk
.ProgressBar()
593 # make the progress bar more "finger-friendly"
594 if util
.platform
.FREMANTLE
:
595 self
.progress
.set_size_request(-1, 100)
596 elif util
.platform
.MAEMO
:
597 self
.progress
.set_size_request(-1, 50)
598 progress_eventbox
.add(self
.progress
)
599 main_vbox
.pack_start( progress_eventbox
, False, False )
601 # make the button box
602 buttonbox
= gtk
.HBox()
604 # A wrapper to help create DualActionButtons with the right settings
605 create_da
= lambda a
, b
, c
=None, d
=None: widgets
.DualActionButton(
606 a
, b
, c
, d
, settings
.dual_action_button_delay
,
607 settings
.enable_dual_action_btn
)
609 self
.rrewind_button
= create_da(
610 generate_image('media-skip-backward.png'),
611 lambda: self
.do_seek(-1*settings
.seek_long
),
612 generate_image(gtk
.STOCK_GOTO_FIRST
, True),
613 player
.playlist
.prev
)
614 buttonbox
.add(self
.rrewind_button
)
616 self
.rewind_button
= create_da(
617 generate_image('media-seek-backward.png'),
618 lambda: self
.do_seek(-1*settings
.seek_short
))
619 buttonbox
.add(self
.rewind_button
)
621 self
.play_pause_button
= gtk
.Button('')
622 image(self
.play_pause_button
, 'media-playback-start.png')
623 self
.play_pause_button
.connect( 'clicked',
624 self
.on_btn_play_pause_clicked
)
625 self
.play_pause_button
.set_sensitive(False)
626 buttonbox
.add(self
.play_pause_button
)
628 self
.forward_button
= create_da(
629 generate_image('media-seek-forward.png'),
630 lambda: self
.do_seek(settings
.seek_short
))
631 buttonbox
.add(self
.forward_button
)
633 self
.fforward_button
= create_da(
634 generate_image('media-skip-forward.png'),
635 lambda: self
.do_seek(settings
.seek_long
),
636 generate_image(gtk
.STOCK_GOTO_LAST
, True),
637 player
.playlist
.next
)
638 buttonbox
.add(self
.fforward_button
)
640 self
.bookmarks_button
= create_da(
641 generate_image('bookmark-new.png'),
642 player
.add_bookmark_at_current_position
,
643 generate_image(gtk
.STOCK_JUMP_TO
, True),
644 lambda *args
: self
.notify('select-current-item-request'))
645 buttonbox
.add(self
.bookmarks_button
)
646 self
.set_controls_sensitivity(False)
647 main_vbox
.pack_start(buttonbox
, False, False)
649 if util
.platform
.MAEMO
:
650 self
.volume
= hildon
.VVolumebar()
651 self
.volume
.set_property('can-focus', False)
652 self
.volume
.connect('level_changed', self
.volume_changed_hildon
)
653 self
.volume
.connect('mute_toggled', self
.mute_toggled
)
654 self
.__gui
_root
.main_window
.connect( 'key-press-event',
656 if not util
.platform
.FREMANTLE
:
657 self
.pack_start(self
.volume
, False, True)
659 # Add a button to pop out the volume bar
660 self
.volume_button
= gtk
.ToggleButton('')
661 image(self
.volume_button
, 'media-speaker.png')
662 self
.volume_button
.connect('clicked', self
.toggle_volumebar
)
664 'show', lambda x
: self
.volume_button
.set_active(True))
666 'hide', lambda x
: self
.volume_button
.set_active(False))
667 if not util
.platform
.FREMANTLE
:
668 buttonbox
.add(self
.volume_button
)
669 self
.volume_button
.show()
671 # Disable focus for all widgets, so we can use the cursor
672 # keys + enter to directly control our media player, which
673 # is handled by "key-press-event"
675 self
.rrewind_button
, self
.rewind_button
,
676 self
.play_pause_button
, self
.forward_button
,
677 self
.fforward_button
, self
.progress
,
678 self
.bookmarks_button
, self
.volume_button
, ):
679 w
.unset_flags(gtk
.CAN_FOCUS
)
681 self
.volume
= gtk
.VolumeButton()
682 self
.volume
.connect('value-changed', self
.volume_changed_gtk
)
683 buttonbox
.add(self
.volume
)
686 self
.set_volume(settings
.volume
)
688 def set_controls_sensitivity(self
, sensitive
):
689 for button
in self
.forward_button
, self
.rewind_button
, \
690 self
.fforward_button
, self
.rrewind_button
:
692 button
.set_sensitive(sensitive
)
694 # the play/pause button should always be available except
695 # for when the player starts without a file
696 self
.play_pause_button
.set_sensitive(True)
698 def on_dual_action_setting_changed( self
, *args
):
699 for button
in self
.forward_button
, self
.rewind_button
, \
700 self
.fforward_button
, self
.rrewind_button
, \
701 self
.bookmarks_button
:
703 button
.set_longpress_enabled( settings
.enable_dual_action_btn
)
704 button
.set_duration( settings
.dual_action_button_delay
)
706 def on_key_press(self
, widget
, event
):
707 if util
.platform
.MAEMO
:
708 if event
.keyval
== gtk
.keysyms
.F7
: #plus
709 self
.set_volume( min( 1, self
.get_volume() + 0.10 ))
710 elif event
.keyval
== gtk
.keysyms
.F8
: #minus
711 self
.set_volume( max( 0, self
.get_volume() - 0.10 ))
712 elif event
.keyval
== gtk
.keysyms
.Left
: # seek back
713 self
.do_seek( -1 * settings
.seek_long
)
714 elif event
.keyval
== gtk
.keysyms
.Right
: # seek forward
715 self
.do_seek( settings
.seek_long
)
716 elif event
.keyval
== gtk
.keysyms
.Return
: # play/pause
717 self
.on_btn_play_pause_clicked()
719 # The following two functions get and set the
720 # volume from the volume control widgets.
721 def get_volume(self
):
722 if util
.platform
.MAEMO
:
723 return self
.volume
.get_level()/100.0
725 return self
.volume
.get_value()
727 def set_volume(self
, vol
):
728 """ vol is a float from 0 to 1 """
731 if util
.platform
.FREMANTLE
:
732 # No volume setting on Maemo 5
735 if util
.platform
.MAEMO
:
736 self
.volume
.set_level(vol
*100.0)
738 self
.volume
.set_value(vol
)
740 def __set_volume_hide_timer(self
, timeout
, force_show
=False):
741 if force_show
or self
.volume_button
.get_active():
743 if self
.volume_timer_id
is not None:
744 gobject
.source_remove(self
.volume_timer_id
)
745 self
.volume_timer_id
= None
747 self
.volume_timer_id
= gobject
.timeout_add(
748 1000 * timeout
, self
.__volume
_hide
_callback
)
750 def __volume_hide_callback(self
):
751 self
.volume_timer_id
= None
755 def toggle_volumebar(self
, widget
=None):
756 if self
.volume_timer_id
is None:
757 self
.__set
_volume
_hide
_timer
(5)
759 self
.__volume
_hide
_callback
()
761 def volume_changed_gtk(self
, widget
, new_value
=0.5):
762 settings
.volume
= new_value
764 def volume_changed_hildon(self
, widget
):
765 self
.__set
_volume
_hide
_timer
( 4, force_show
=True )
766 settings
.volume
= widget
.get_level()/100.0
768 def mute_toggled(self
, widget
):
769 if widget
.get_mute():
772 settings
.volume
= widget
.get_level()/100.0
774 def on_player_stopped(self
):
775 self
.stop_progress_timer()
776 self
.set_controls_sensitivity(False)
777 image(self
.play_pause_button
, 'media-playback-start.png')
779 def on_player_playing(self
):
780 self
.start_progress_timer()
781 image(self
.play_pause_button
, 'media-playback-pause.png')
782 self
.set_controls_sensitivity(True)
784 def on_player_new_track(self
):
785 for widget
in [self
.title_label
,self
.artist_label
,self
.album_label
]:
786 widget
.set_markup('')
789 self
.cover_art
.hide()
790 self
.has_coverart
= False
792 def on_player_new_metadata(self
):
793 metadata
= player
.playlist
.get_file_metadata()
794 self
.set_metadata(metadata
)
796 if not player
.playing
:
797 position
= player
.playlist
.get_current_position()
798 estimated_length
= metadata
.get('length', 0)
799 self
.set_progress_callback( position
, estimated_length
)
801 def on_player_paused( self
, position
, duration
):
802 self
.stop_progress_timer() # This should save some power
803 self
.set_progress_callback( position
, duration
)
804 image(self
.play_pause_button
, 'media-playback-start.png')
806 def on_player_end_of_playlist(self
, loop
):
809 def reset_progress(self
):
810 self
.progress
.set_fraction(0)
811 self
.set_progress_callback(0,0)
813 def set_progress_callback(self
, time_elapsed
, total_time
):
814 """ times must be in nanoseconds """
815 time_string
= "%s / %s" % ( util
.convert_ns(time_elapsed
),
816 util
.convert_ns(total_time
) )
817 self
.progress
.set_text( time_string
)
818 fraction
= float(time_elapsed
) / float(total_time
) if total_time
else 0
819 self
.progress
.set_fraction( fraction
)
821 def on_progressbar_changed(self
, widget
, event
):
822 if ( not settings
.progress_locked
and
823 event
.type == gtk
.gdk
.BUTTON_PRESS
and event
.button
== 1 ):
824 new_fraction
= event
.x
/float(widget
.get_allocation().width
)
825 resp
= player
.do_seek(percent
=new_fraction
)
827 # Preemptively update the progressbar to make seeking smoother
828 self
.set_progress_callback( *resp
)
830 def on_btn_play_pause_clicked(self
, widget
=None):
831 player
.play_pause_toggle()
833 def progress_timer_callback( self
):
834 if player
.playing
and not player
.seeking
:
835 pos_int
, dur_int
= player
.get_position_duration()
836 # This prevents bogus values from being set while seeking
837 if ( pos_int
> 10**9 ) and ( dur_int
> 10**9 ):
838 self
.set_progress_callback( pos_int
, dur_int
)
841 def start_progress_timer( self
):
842 if self
.progress_timer_id
is not None:
843 self
.stop_progress_timer()
845 self
.progress_timer_id
= gobject
.timeout_add(
846 1000, self
.progress_timer_callback
)
848 def stop_progress_timer( self
):
849 if self
.progress_timer_id
is not None:
850 gobject
.source_remove( self
.progress_timer_id
)
851 self
.progress_timer_id
= None
853 def get_coverart_size( self
):
854 if util
.platform
.MAEMO
:
855 if self
.__gui
_root
.fullscreen
:
856 size
= coverart_sizes
['maemo fullscreen']
858 size
= coverart_sizes
['maemo']
860 size
= coverart_sizes
['normal']
864 def set_coverart( self
, pixbuf
):
865 self
.cover_art
.set_from_pixbuf(pixbuf
)
866 self
.cover_art
.show()
867 self
.has_coverart
= True
869 def set_metadata( self
, tag_message
):
870 tags
= { 'title': self
.title_label
, 'artist': self
.artist_label
,
871 'album': self
.album_label
}
874 if tag_message
.has_key('image') and tag_message
['image'] is not None:
875 value
= tag_message
['image']
877 pbl
= gtk
.gdk
.PixbufLoader()
882 x
, y
= self
.get_coverart_size()
883 pixbuf
= pbl
.get_pixbuf()
884 pixbuf
= pixbuf
.scale_simple( x
, y
, gtk
.gdk
.INTERP_BILINEAR
)
885 self
.set_coverart(pixbuf
)
887 self
.__log
.exception('Error setting coverart...')
889 # set the text metadata
890 for tag
,value
in tag_message
.iteritems():
891 if tags
.has_key(tag
) and value
is not None and value
.strip():
892 tags
[tag
].set_markup('<big>'+value
+'</big>')
893 tags
[tag
].set_alignment( 0.5*int(not self
.has_coverart
), 0.5)
897 # make the title bold
898 tags
[tag
].set_markup('<b><big>'+value
+'</big></b>')
900 if not util
.platform
.MAEMO
:
901 value
+= ' - Panucci'
903 self
.__gui
_root
.main_window
.set_title( value
)
905 def do_seek(self
, seek_amount
):
906 resp
= player
.do_seek(from_current
=seek_amount
*10**9)
908 # Preemptively update the progressbar to make seeking smoother
909 self
.set_progress_callback( *resp
)
913 ##################################################
915 ##################################################
916 class PlaylistTab(gtk
.VBox
):
917 def __init__(self
, main_window
):
918 gtk
.VBox
.__init
__(self
)
919 self
.__log
= logging
.getLogger('panucci.panucci.BookmarksWindow')
920 self
.main
= main_window
922 self
.__model
= gtk
.TreeStore(
923 # uid, name, position
924 gobject
.TYPE_STRING
, gobject
.TYPE_STRING
, gobject
.TYPE_STRING
)
927 self
.treeview
= gtk
.TreeView()
928 self
.treeview
.set_model(self
.__model
)
929 self
.treeview
.set_headers_visible(True)
930 tree_selection
= self
.treeview
.get_selection()
931 # This breaks drag and drop, only use single selection for now
932 # tree_selection.set_mode(gtk.SELECTION_MULTIPLE)
933 tree_selection
.connect('changed', self
.tree_selection_changed
)
935 # The tree lines look nasty on maemo
936 if util
.platform
.DESKTOP
:
937 self
.treeview
.set_enable_tree_lines(True)
940 ncol
= gtk
.TreeViewColumn(_('Name'))
941 ncell
= gtk
.CellRendererText()
942 ncell
.set_property('ellipsize', pango
.ELLIPSIZE_END
)
943 ncell
.set_property('editable', True)
944 ncell
.connect('edited', self
.label_edited
)
945 ncol
.set_expand(True)
946 ncol
.pack_start(ncell
)
947 ncol
.add_attribute(ncell
, 'text', 1)
949 tcol
= gtk
.TreeViewColumn(_('Position'))
950 tcell
= gtk
.CellRendererText()
951 tcol
.pack_start(tcell
)
952 tcol
.add_attribute(tcell
, 'text', 2)
954 self
.treeview
.append_column(ncol
)
955 self
.treeview
.append_column(tcol
)
956 self
.treeview
.connect('drag-data-received', self
.drag_data_recieved
)
957 self
.treeview
.connect('drag_data_get', self
.drag_data_get_data
)
960 ( 'playlist_row_data', gtk
.TARGET_SAME_WIDGET
, 0 ) ]
962 self
.treeview
.enable_model_drag_source(
963 gtk
.gdk
.BUTTON1_MASK
, treeview_targets
, gtk
.gdk
.ACTION_COPY
)
965 self
.treeview
.enable_model_drag_dest(
966 treeview_targets
, gtk
.gdk
.ACTION_COPY
)
968 sw
= gtk
.ScrolledWindow()
969 sw
.set_policy(gtk
.POLICY_AUTOMATIC
, gtk
.POLICY_AUTOMATIC
)
970 sw
.set_shadow_type(gtk
.SHADOW_IN
)
971 sw
.add(self
.treeview
)
974 self
.hbox
= gtk
.HBox()
976 self
.add_button
= gtk
.Button(gtk
.STOCK_NEW
)
977 self
.add_button
.set_use_stock(True)
978 set_stock_button_text( self
.add_button
, _('Add File') )
979 self
.add_button
.connect('clicked', self
.add_file
)
980 self
.hbox
.pack_start(self
.add_button
, True, True)
982 self
.dir_button
= gtk
.Button(gtk
.STOCK_OPEN
)
983 self
.dir_button
.set_use_stock(True)
984 set_stock_button_text( self
.dir_button
, _('Add Directory') )
985 self
.dir_button
.connect('clicked', self
.add_directory
)
986 self
.hbox
.pack_start(self
.dir_button
, True, True)
988 self
.remove_button
= widgets
.DualActionButton(
989 generate_image(gtk
.STOCK_REMOVE
, True),
990 self
.remove_bookmark
,
991 generate_image(gtk
.STOCK_CANCEL
, True),
992 lambda *a
: player
.playlist
.reset_playlist() )
993 #self.remove_button.set_use_stock(True)
994 #self.remove_button.connect('clicked', self.remove_bookmark)
995 self
.hbox
.pack_start(self
.remove_button
, True, True)
997 self
.jump_button
= gtk
.Button(gtk
.STOCK_JUMP_TO
)
998 self
.jump_button
.set_use_stock(True)
999 self
.jump_button
.connect('clicked', self
.jump_bookmark
)
1000 self
.hbox
.pack_start(self
.jump_button
, True, True)
1002 self
.info_button
= gtk
.Button()
1003 self
.info_button
.add(
1004 gtk
.image_new_from_stock(gtk
.STOCK_INFO
, gtk
.ICON_SIZE_BUTTON
))
1005 self
.info_button
.connect('clicked', self
.show_playlist_item_details
)
1006 self
.hbox
.pack_start(self
.info_button
, True, True)
1008 self
.pack_start(self
.hbox
, False, True)
1010 player
.playlist
.register( 'file_queued',
1011 lambda x
,y
,z
: self
.update_model() )
1012 player
.playlist
.register( 'bookmark_added', self
.on_bookmark_added
)
1016 def tree_selection_changed(self
, treeselection
):
1017 count
= treeselection
.count_selected_rows()
1018 self
.remove_button
.set_sensitive(count
> 0)
1019 self
.jump_button
.set_sensitive(count
== 1)
1020 self
.info_button
.set_sensitive(count
== 1)
1022 def drag_data_get_data(
1023 self
, treeview
, context
, selection
, target_id
, timestamp
):
1025 treeselection
= treeview
.get_selection()
1026 model
, iter = treeselection
.get_selected()
1027 # only allow moving around top-level parents
1028 if model
.iter_parent(iter) is None:
1029 # send the path of the selected row
1030 data
= model
.get_string_from_iter(iter)
1031 selection
.set(selection
.target
, 8, data
)
1033 self
.__log
.debug("Can't move children...")
1035 def drag_data_recieved(
1036 self
, treeview
, context
, x
, y
, selection
, info
, timestamp
):
1038 drop_info
= treeview
.get_dest_row_at_pos(x
, y
)
1040 # TODO: If user drags the row past the last row, drop_info is None
1041 # I'm not sure if it's safe to simply assume that None is
1042 # euqivalent to the last row...
1043 if None not in [ drop_info
and selection
.data
]:
1044 model
= treeview
.get_model()
1045 path
, position
= drop_info
1047 from_iter
= model
.get_iter_from_string(selection
.data
)
1049 # make sure the to_iter doesn't have a parent
1050 to_iter
= model
.get_iter(path
)
1051 if model
.iter_parent(to_iter
) is not None:
1052 to_iter
= model
.iter_parent(to_iter
)
1054 from_row
= model
.get_path(from_iter
)[0]
1057 if ( position
== gtk
.TREE_VIEW_DROP_BEFORE
or
1058 position
== gtk
.TREE_VIEW_DROP_INTO_OR_BEFORE
):
1059 model
.move_before( from_iter
, to_iter
)
1060 to_row
= to_row
- 1 if from_row
< to_row
else to_row
1061 elif ( position
== gtk
.TREE_VIEW_DROP_AFTER
or
1062 position
== gtk
.TREE_VIEW_DROP_INTO_OR_AFTER
):
1063 model
.move_after( from_iter
, to_iter
)
1064 to_row
= to_row
+ 1 if from_row
> to_row
else to_row
1066 self
.__log
.debug('Drop not supported: %s', position
)
1068 # don't do anything if we're not actually moving rows around
1069 if from_row
!= to_row
:
1070 player
.playlist
.move_item( from_row
, to_row
)
1073 self
.__log
.debug('No drop_data or selection.data available')
1075 def update_model(self
):
1076 plist
= player
.playlist
1077 path_info
= self
.treeview
.get_path_at_pos(0,0)
1078 path
= path_info
[0] if path_info
is not None else None
1080 self
.__model
.clear()
1083 for item
, data
in plist
.get_playlist_item_ids():
1084 parent
= self
.__model
.append(None, (item
, data
.get('title'), None))
1086 for bid
, bname
, bpos
in plist
.get_bookmarks_from_item_id( item
):
1087 nice_bpos
= util
.convert_ns(bpos
)
1088 self
.__model
.append( parent
, (bid
, bname
, nice_bpos
) )
1090 self
.treeview
.expand_all()
1092 if path
is not None:
1093 self
.treeview
.scroll_to_cell(path
)
1095 def label_edited(self
, cellrenderer
, path
, new_text
):
1096 iter = self
.__model
.get_iter(path
)
1097 old_text
= self
.__model
.get_value(iter, 1)
1099 if new_text
.strip() and old_text
!= new_text
:
1100 # this loop will only run once, because only one cell can be
1101 # edited at a time, we use it to get the item and bookmark ids
1102 for m
, bkmk_id
, biter
, item_id
, iiter
in self
.__cur
_selection
():
1103 self
.__model
.set_value(iter, 1, new_text
)
1104 player
.playlist
.update_bookmark(
1105 item_id
, bkmk_id
, name
=new_text
)
1107 self
.__model
.set_value(iter, 1, old_text
)
1109 def on_bookmark_added(self
, parent_id
, bookmark_name
, position
):
1110 util
.notify(_('Bookmark added: %s') % bookmark_name
)
1113 def add_file(self
, widget
):
1114 filename
= get_file_from_filechooser(self
.main
.main_window
)
1115 if filename
is not None:
1116 player
.playlist
.append(filename
)
1118 def add_directory(self
, widget
):
1119 directory
= get_file_from_filechooser(
1120 self
.main
.main_window
, folder
=True )
1121 if directory
is not None:
1122 player
.playlist
.load_directory(directory
, append
=True)
1124 def __cur_selection(self
):
1125 selection
= self
.treeview
.get_selection()
1126 model
, bookmark_paths
= selection
.get_selected_rows()
1128 # Convert the paths to gtk.TreeRowReference objects, because we
1129 # might modify the model while this generator is running
1130 bookmark_refs
= [gtk
.TreeRowReference(model
, p
) for p
in bookmark_paths
]
1132 for reference
in bookmark_refs
:
1133 bookmark_iter
= model
.get_iter(reference
.get_path())
1134 item_iter
= model
.iter_parent(bookmark_iter
)
1136 # bookmark_iter is actually an item_iter
1137 if item_iter
is None:
1138 item_iter
= bookmark_iter
1139 item_id
= model
.get_value(item_iter
, 0)
1140 bookmark_id
, bookmark_iter
= None, None
1142 bookmark_id
= model
.get_value(bookmark_iter
, 0)
1143 item_id
= model
.get_value(item_iter
, 0)
1145 yield model
, bookmark_id
, bookmark_iter
, item_id
, item_iter
1147 def remove_bookmark(self
, w
=None):
1148 for model
, bkmk_id
, bkmk_iter
, item_id
, item_iter
in self
.__cur
_selection
():
1149 player
.playlist
.remove_bookmark( item_id
, bkmk_id
)
1150 if bkmk_iter
is not None:
1151 model
.remove(bkmk_iter
)
1152 elif item_iter
is not None:
1153 model
.remove(item_iter
)
1155 def select_current_item(self
):
1156 model
= self
.treeview
.get_model()
1157 selection
= self
.treeview
.get_selection()
1158 current_item_id
= str(player
.playlist
.get_current_item())
1159 for row
in iter(model
):
1160 if model
.get_value(row
.iter, 0) == current_item_id
:
1161 selection
.unselect_all()
1162 self
.treeview
.set_cursor(row
.path
)
1163 self
.treeview
.scroll_to_cell(row
.path
, use_align
=True)
1166 def show_playlist_item_details(self
, w
):
1167 selection
= self
.treeview
.get_selection()
1168 if selection
.count_selected_rows() == 1:
1169 selected
= self
.__cur
_selection
().next()
1170 model
, bkmk_id
, bkmk_iter
, item_id
, item_iter
= selected
1171 playlist_item
= player
.playlist
.get_item_by_id(item_id
)
1172 PlaylistItemDetails(self
.main
, playlist_item
)
1174 def jump_bookmark(self
, w
):
1175 selected
= list(self
.__cur
_selection
())
1176 if len(selected
) == 1:
1177 # It should be guranteed by the fact that we only enable the
1178 # "Jump to" button when the selection count equals 1.
1179 model
, bkmk_id
, bkmk_iter
, item_id
, item_iter
= selected
.pop(0)
1180 player
.playlist
.load_from_bookmark_id(item_id
, bkmk_id
)
1182 # FIXME: The player/playlist should be able to take care of this
1183 if not player
.playing
:
1187 ##################################################
1188 # PlaylistItemDetails
1189 ##################################################
1190 class PlaylistItemDetails(gtk
.Dialog
):
1191 def __init__(self
, main
, playlist_item
):
1192 gtk
.Dialog
.__init
__( self
, _('Playlist item details'),
1193 main
.main_window
, gtk
.DIALOG_MODAL
,
1194 (gtk
.STOCK_CLOSE
, gtk
.RESPONSE_OK
))
1197 self
.fill(playlist_item
)
1198 self
.set_has_separator(False)
1199 self
.set_resizable(False)
1204 def fill(self
, playlist_item
):
1205 t
= gtk
.Table(10, 2)
1206 self
.vbox
.pack_start(t
, expand
=False)
1208 metadata
= playlist_item
.metadata
1210 t
.attach(gtk
.Label(_('Custom title:')), 0, 1, 0, 1)
1211 t
.attach(gtk
.Label(_('ID:')), 0, 1, 1, 2)
1212 t
.attach(gtk
.Label(_('Playlist ID:')), 0, 1, 2, 3)
1213 t
.attach(gtk
.Label(_('Filepath:')), 0, 1, 3, 4)
1216 for key
in metadata
:
1217 if metadata
[key
] is not None:
1218 t
.attach( gtk
.Label(key
.capitalize()+':'),
1219 0, 1, row_num
, row_num
+1 )
1222 t
.foreach(lambda x
, y
: x
.set_alignment(1, 0.5), None)
1223 t
.foreach(lambda x
, y
: x
.set_markup('<b>%s</b>' % x
.get_label()), None)
1225 t
.attach(gtk
.Label(playlist_item
.title
or _('<not modified>')),1,2,0,1)
1226 t
.attach(gtk
.Label(str(playlist_item
)), 1, 2, 1, 2)
1227 t
.attach(gtk
.Label(playlist_item
.playlist_id
), 1, 2, 2, 3)
1228 t
.attach(gtk
.Label(playlist_item
.filepath
), 1, 2, 3, 4)
1231 for key
in metadata
:
1232 value
= metadata
[key
]
1234 value
= util
.convert_ns(value
)
1235 if metadata
[key
] is not None:
1236 t
.attach( gtk
.Label( str(value
) or _('<not set>')),
1237 1, 2, row_num
, row_num
+1)
1240 t
.foreach(lambda x
, y
: x
.get_alignment() == (0.5, 0.5) and \
1241 x
.set_alignment(0, 0.5), None)
1243 t
.set_border_width(8)
1244 t
.set_row_spacings(4)
1245 t
.set_col_spacings(8)
1247 l
= gtk
.ListStore(str, str)
1249 cr
= gtk
.CellRendererText()
1250 cr
.set_property('ellipsize', pango
.ELLIPSIZE_END
)
1251 c
= gtk
.TreeViewColumn(_('Title'), cr
, text
=0)
1254 c
= gtk
.TreeViewColumn(_('Time'), gtk
.CellRendererText(), text
=1)
1256 playlist_item
.load_bookmarks()
1257 for bookmark
in playlist_item
.bookmarks
:
1258 l
.append([bookmark
.bookmark_name
, \
1259 util
.convert_ns(bookmark
.seek_position
)])
1261 sw
= gtk
.ScrolledWindow()
1262 sw
.set_shadow_type(gtk
.SHADOW_IN
)
1264 sw
.set_policy(gtk
.POLICY_AUTOMATIC
, gtk
.POLICY_AUTOMATIC
)
1265 e
= gtk
.Expander(_('Bookmarks'))
1267 self
.vbox
.pack_start(e
)
1270 def run(filename
=None):
1271 PanucciGUI( filename
)
1274 if __name__
== '__main__':
1275 log
.error( 'Use the "panucci" executable to run this program.' )
1276 log
.error( 'Exiting...' )