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)
39 log
= logging
.getLogger('panucci.panucci')
45 if util
.platform
.MAEMO
:
46 log
.critical( 'Using GTK widgets, install "python2.5-hildon" '
47 'for this to work properly.' )
49 from simplegconf
import gconf
50 from settings
import settings
51 from player
import player
52 from dbusinterface
import interface
53 from services
import ObservableService
55 about_name
= 'Panucci'
56 about_text
= _('Resuming audiobook and podcast player')
57 about_authors
= ['Thomas Perl', 'Nick (nikosapi)', 'Matthew Taylor']
58 about_website
= 'http://panucci.garage.maemo.org/'
64 'maemo fullscreen' : 275,
67 gtk
.about_dialog_set_url_hook(util
.open_link
, None)
68 gtk
.icon_size_register('panucci-button', 32, 32)
70 def generate_image(filename
, is_stock
=False):
73 image
= gtk
.image_new_from_stock(
74 filename
, gtk
.icon_size_from_name('panucci-button') )
76 filename
= util
.find_image(filename
)
77 if filename
is not None:
78 image
= gtk
.image_new_from_file(filename
)
80 if util
.platform
.MAEMO
:
81 image
.set_padding(20, 20)
83 image
.set_padding(5, 5)
87 def image(widget
, filename
, is_stock
=False):
88 child
= widget
.get_child()
91 image
= generate_image(filename
, is_stock
)
95 def dialog( toplevel_window
, title
, question
, description
,
96 affirmative_button
=gtk
.STOCK_YES
, negative_button
=gtk
.STOCK_NO
,
97 abortion_button
=gtk
.STOCK_CANCEL
):
99 """Present the user with a yes/no/cancel dialog.
100 The return value is either True, False or None, depending on which
101 button has been pressed in the dialog:
103 affirmative button (default: Yes) => True
104 negative button (defaut: No) => False
105 abortion button (default: Cancel) => None
107 When the dialog is closed with the "X" button in the window manager
108 decoration, the return value is always None (same as abortion button).
110 You can set any of the affirmative_button, negative_button or
111 abortion_button values to "None" to hide the corresponding action.
113 dlg
= gtk
.MessageDialog( toplevel_window
, gtk
.DIALOG_MODAL
,
114 gtk
.MESSAGE_QUESTION
, message_format
=question
)
118 if abortion_button
is not None:
119 dlg
.add_button(abortion_button
, gtk
.RESPONSE_CANCEL
)
120 if negative_button
is not None:
121 dlg
.add_button(negative_button
, gtk
.RESPONSE_NO
)
122 if affirmative_button
is not None:
123 dlg
.add_button(affirmative_button
, gtk
.RESPONSE_YES
)
125 dlg
.format_secondary_text(description
)
130 if response
== gtk
.RESPONSE_YES
:
132 elif response
== gtk
.RESPONSE_NO
:
134 elif response
in [gtk
.RESPONSE_CANCEL
, gtk
.RESPONSE_DELETE_EVENT
]:
137 def get_file_from_filechooser(
138 toplevel_window
, folder
=False, save_file
=False, save_to
=None):
141 open_action
= gtk
.FILE_CHOOSER_ACTION_SELECT_FOLDER
143 open_action
= gtk
.FILE_CHOOSER_ACTION_OPEN
145 if util
.platform
.MAEMO
:
147 args
= ( toplevel_window
, gtk
.FILE_CHOOSER_ACTION_SAVE
)
149 args
= ( toplevel_window
, open_action
)
151 dlg
= hildon
.FileChooserDialog( *args
)
154 args
= ( _('Select file to save playlist to'), None,
155 gtk
.FILE_CHOOSER_ACTION_SAVE
,
156 (( gtk
.STOCK_CANCEL
, gtk
.RESPONSE_REJECT
,
157 gtk
.STOCK_SAVE
, gtk
.RESPONSE_OK
)) )
159 args
= ( _('Select podcast or audiobook'), None, open_action
,
160 (( gtk
.STOCK_CANCEL
, gtk
.RESPONSE_REJECT
,
161 gtk
.STOCK_OPEN
, gtk
.RESPONSE_OK
)) )
163 dlg
= gtk
.FileChooserDialog(*args
)
165 current_folder
= os
.path
.expanduser(settings
.last_folder
)
167 if current_folder
is not None and os
.path
.isdir(current_folder
):
168 dlg
.set_current_folder(current_folder
)
170 if save_file
and save_to
is not None:
171 dlg
.set_current_name(save_to
)
173 if dlg
.run() == gtk
.RESPONSE_OK
:
174 filename
= dlg
.get_filename()
175 settings
.last_folder
= dlg
.get_current_folder()
182 def set_stock_button_text( button
, text
):
183 alignment
= button
.get_child()
184 hbox
= alignment
.get_child()
185 image
, label
= hbox
.get_children()
188 ##################################################
190 ##################################################
191 class PanucciGUI(object):
192 """ The object that holds the entire panucci gui """
194 def __init__(self
, filename
=None):
195 self
.__log
= logging
.getLogger('panucci.panucci.PanucciGUI')
196 interface
.register_gui(self
)
198 # Build the base ui (window and menubar)
199 if util
.platform
.MAEMO
:
200 self
.app
= hildon
.Program()
201 window
= hildon
.Window()
202 self
.app
.add_window(window
)
204 window
= gtk
.Window(gtk
.WINDOW_TOPLEVEL
)
206 self
.main_window
= window
207 window
.set_title('Panucci')
208 self
.window_icon
= util
.find_image('panucci.png')
209 if self
.window_icon
is not None:
210 window
.set_icon_from_file( self
.window_icon
)
211 window
.set_default_size(400, -1)
212 window
.set_border_width(0)
213 window
.connect("destroy", self
.destroy
)
215 # Add the tabs (they are private to prevent us from trying to do
216 # something like gui_root.player_tab.some_function() from inside
217 # playlist_tab or vice-versa)
218 self
.__player
_tab
= PlayerTab(self
)
219 self
.__playlist
_tab
= PlaylistTab(self
)
221 self
.playlist_window
= gtk
.Window(gtk
.WINDOW_TOPLEVEL
)
222 self
.playlist_window
.set_title(_('Panucci playlist'))
223 self
.playlist_window
.set_transient_for(self
.main_window
)
224 self
.playlist_window
.add(self
.__playlist
_tab
)
226 if util
.platform
.MAEMO
:
227 window
.set_menu(self
.create_menu())
228 window
.add(self
.__player
_tab
)
230 menu_vbox
= gtk
.VBox()
231 menu_vbox
.set_spacing(0)
232 window
.add(menu_vbox
)
233 menu_bar
= gtk
.MenuBar()
234 root_menu
= gtk
.MenuItem('Panucci')
235 root_menu
.set_submenu(self
.create_menu())
236 menu_bar
.append(root_menu
)
237 menu_vbox
.pack_start(menu_bar
, False, False, 0)
239 menu_vbox
.pack_end(self
.__player
_tab
, True, True, 6)
241 # Tie it all together!
242 self
.__ignore
_queue
_check
= False
243 self
.__window
_fullscreen
= False
245 if util
.platform
.MAEMO
and interface
.headset_device
is not None:
246 # Enable play/pause with headset button
247 interface
.headset_device
.connect_to_signal(
248 'Condition', self
.handle_headset_button
)
250 self
.main_window
.connect('key-press-event', self
.on_key_press
)
251 player
.playlist
.register( 'file_queued', self
.on_file_queued
)
253 self
.__anti
_blank
_timer
= None
254 settings
.register('allow_blanking_changed',self
.__set
_anti
_blank
_timer
)
255 self
.__set
_anti
_blank
_timer
( settings
.allow_blanking
)
257 player
.playlist
.register( 'playlist-to-be-overwritten',
259 self
.__player
_tab
.register( 'select-current-item-request',
260 self
.__select
_current
_item
)
262 self
.main_window
.show_all()
264 # this should be done when the gui is ready
265 self
.pickle_file_conversion()
266 player
.init(filepath
=filename
)
268 def create_menu(self
):
272 menu_open
= gtk
.ImageMenuItem(_('Open playlist'))
274 gtk
.image_new_from_stock(gtk
.STOCK_OPEN
, gtk
.ICON_SIZE_MENU
))
275 menu_open
.connect("activate", self
.open_file_callback
)
276 menu
.append(menu_open
)
278 # the recent files menu
279 self
.menu_recent
= gtk
.MenuItem(_('Open recent playlist'))
280 menu
.append(self
.menu_recent
)
281 self
.create_recent_files_menu()
283 menu
.append(gtk
.SeparatorMenuItem())
285 menu_save
= gtk
.ImageMenuItem(_('Save current playlist'))
287 gtk
.image_new_from_stock(gtk
.STOCK_SAVE_AS
, gtk
.ICON_SIZE_MENU
))
288 menu_save
.connect("activate", self
.save_to_playlist_callback
)
289 menu
.append(menu_save
)
291 menu
.append(gtk
.SeparatorMenuItem())
293 # the settings sub-menu
294 menu_settings
= gtk
.MenuItem(_('Settings'))
295 menu
.append(menu_settings
)
297 menu_settings_sub
= gtk
.Menu()
298 menu_settings
.set_submenu(menu_settings_sub
)
300 menu_settings_enable_dual_action
= gtk
.CheckMenuItem(
301 _('Enable dual-action buttons') )
302 settings
.attach_checkbutton( menu_settings_enable_dual_action
,
303 'enable_dual_action_btn' )
304 menu_settings_sub
.append(menu_settings_enable_dual_action
)
306 if util
.platform
.MAEMO
:
307 menu_settings_enable_hw_decoding
= gtk
.CheckMenuItem(
308 _('Enable hardware decoding') )
309 settings
.attach_checkbutton( menu_settings_enable_hw_decoding
,
310 'enable_hardware_decoding' )
311 menu_settings_sub
.append(menu_settings_enable_hw_decoding
)
313 menu_settings_lock_progress
= gtk
.CheckMenuItem(_('Lock Progress Bar'))
314 settings
.attach_checkbutton( menu_settings_lock_progress
,
316 menu_settings_sub
.append(menu_settings_lock_progress
)
318 menu_about
= gtk
.ImageMenuItem(gtk
.STOCK_ABOUT
)
319 menu_about
.connect("activate", self
.show_about
, self
.main_window
)
320 menu
.append(menu_about
)
322 menu
.append(gtk
.SeparatorMenuItem())
324 menu_quit
= gtk
.ImageMenuItem(gtk
.STOCK_QUIT
)
325 menu_quit
.connect("activate", self
.destroy
)
326 menu
.append(menu_quit
)
330 def create_recent_files_menu( self
):
331 max_files
= settings
.max_recent_files
332 self
.recent_files
= player
.playlist
.get_recent_files(max_files
)
333 menu_recent_sub
= gtk
.Menu()
335 temp_playlist
= os
.path
.expanduser(settings
.temp_playlist
)
337 if len(self
.recent_files
) > 0:
338 for f
in self
.recent_files
:
339 # don't include the temporary playlist in the file list
340 if f
== temp_playlist
: continue
341 # don't include non-existant files
342 if not os
.path
.exists( f
): continue
343 filename
, extension
= os
.path
.splitext(os
.path
.basename(f
))
344 menu_item
= gtk
.MenuItem( filename
.replace('_', ' '))
345 menu_item
.connect('activate', self
.on_recent_file_activate
, f
)
346 menu_recent_sub
.append(menu_item
)
348 menu_item
= gtk
.MenuItem(_('No recent files available.'))
349 menu_item
.set_sensitive(False)
350 menu_recent_sub
.append(menu_item
)
352 self
.menu_recent
.set_submenu(menu_recent_sub
)
354 def destroy(self
, widget
):
358 def show_main_window(self
):
359 self
.main_window
.present()
361 def check_queue(self
):
362 """ Makes sure the queue is saved if it has been modified
363 True means a new file can be opened
364 False means the user does not want to continue """
366 if not self
.__ignore
_queue
_check
and player
.playlist
.queue_modified
:
368 self
.main_window
, _('Save current playlist'),
369 _('Current playlist has been modified'),
370 _('Opening a new file will replace the current playlist. ') +
371 _('Do you want to save it before creating a new one?'),
372 affirmative_button
=gtk
.STOCK_SAVE
,
373 negative_button
=_('Discard changes'))
375 self
.__log
.debug('Response to "Save Queue?": %s', response
)
380 return self
.save_to_playlist_callback()
388 def open_file_callback(self
, widget
=None):
389 if self
.check_queue():
390 # set __ingnore__queue_check because we already did the check
391 self
.__ignore
_queue
_check
= True
392 filename
= get_file_from_filechooser(self
.main_window
)
393 if filename
is not None:
394 self
._play
_file
(filename
)
396 self
.__ignore
_queue
_check
= False
398 def save_to_playlist_callback(self
, widget
=None):
399 filename
= get_file_from_filechooser(
400 self
.main_window
, save_file
=True, save_to
='playlist.m3u' )
405 if os
.path
.isfile(filename
):
406 response
= dialog( self
.main_window
, _('File already exists'),
407 _('File already exists'),
408 _('The file %s already exists. You can choose another name or '
409 'overwrite the existing file.') % os
.path
.basename(filename
),
410 affirmative_button
=gtk
.STOCK_SAVE
,
411 negative_button
=_('Rename file'))
419 return self
.save_to_playlist_callback()
421 ext
= util
.detect_filetype(filename
)
422 if not player
.playlist
.save_to_new_playlist(filename
, ext
):
423 util
.notify(_('Error saving playlist...'))
428 def __get_fullscreen(self
):
429 return self
.__window
_fullscreen
431 def __set_fullscreen(self
, value
):
432 if value
!= self
.__window
_fullscreen
:
434 self
.main_window
.fullscreen()
436 self
.main_window
.unfullscreen()
438 self
.__window
_fullscreen
= value
439 player
.playlist
.send_metadata()
441 fullscreen
= property( __get_fullscreen
, __set_fullscreen
)
443 def on_key_press(self
, widget
, event
):
444 if util
.platform
.MAEMO
:
445 if event
.keyval
== gtk
.keysyms
.F6
:
446 self
.fullscreen
= not self
.fullscreen
448 def on_recent_file_activate(self
, widget
, filepath
):
449 self
._play
_file
(filepath
)
451 def on_file_queued(self
, filepath
, success
, notify
):
453 filename
= os
.path
.basename(filepath
)
456 util
.notify( '%s added successfully.' % filename
))
459 util
.notify( 'Error adding %s to the queue.' % filename
))
461 def show_about(self
, w
, win
):
462 dialog
= gtk
.AboutDialog()
463 dialog
.set_website(about_website
)
464 dialog
.set_website_label(about_website
)
465 dialog
.set_name(about_name
)
466 dialog
.set_authors(about_authors
)
467 dialog
.set_comments(about_text
)
468 dialog
.set_version(app_version
)
472 def _play_file(self
, filename
, pause_on_load
=False):
473 player
.playlist
.load( os
.path
.abspath(filename
) )
475 if player
.playlist
.is_empty
:
478 def handle_headset_button(self
, event
, button
):
479 if event
== 'ButtonPressed' and button
== 'phone':
480 player
.play_pause_toggle()
482 def __set_anti_blank_timer(self
, allow_blanking
):
483 if util
.platform
.MAEMO
:
484 if allow_blanking
and self
.__anti
_blank
_timer
is not None:
485 self
.__log
.info('Screen blanking enabled.')
486 gobject
.source_remove(self
.__anti
_blank
_timer
)
487 self
.__anti
_blank
_timer
= None
488 elif not allow_blanking
and self
.__anti
_blank
_timer
is None:
489 self
.__log
.info('Attempting to disable screen blanking.')
490 self
.__anti
_blank
_timer
= gobject
.timeout_add(
491 1000 * 59, util
.poke_backlight
)
493 self
.__log
.info('Blanking controls are for Maemo only.')
495 def __select_current_item( self
):
496 # Select the currently playing track in the playlist tab
497 # and switch to it (so we can edit bookmarks, etc.. there)
498 self
.__playlist
_tab
.select_current_item()
499 self
.playlist_window
.show()
501 def pickle_file_conversion(self
):
502 pickle_file
= os
.path
.expanduser('~/.rmp-bookmarks')
503 if os
.path
.isfile(pickle_file
):
504 import pickle_converter
507 util
.notify( _('Converting old pickle format to SQLite.') ))
508 self
.__log
.info( util
.notify( _('This may take a while...') ))
510 if pickle_converter
.load_pickle_file(pickle_file
):
512 util
.notify( _('Pickle file converted successfully.') ))
514 self
.__log
.error( util
.notify(
515 _('Error converting pickle file, check your log...') ))
517 ##################################################
519 ##################################################
520 class PlayerTab(ObservableService
, gtk
.HBox
):
521 """ The tab that holds the player elements """
523 signals
= [ 'select-current-item-request', ]
525 def __init__(self
, gui_root
):
526 self
.__log
= logging
.getLogger('panucci.panucci.PlayerTab')
527 self
.__gui
_root
= gui_root
529 gtk
.HBox
.__init
__(self
)
530 ObservableService
.__init
__(self
, self
.signals
, self
.__log
)
533 self
.progress_timer_id
= None
534 self
.volume_timer_id
= None
536 self
.recent_files
= []
537 self
.make_player_tab()
538 self
.has_coverart
= False
539 self
.set_volume(settings
.volume
)
541 settings
.register( 'enable_dual_action_btn_changed',
542 self
.on_dual_action_setting_changed
)
543 settings
.register( 'dual_action_button_delay_changed',
544 self
.on_dual_action_setting_changed
)
545 settings
.register( 'volume_changed', self
.set_volume
)
546 settings
.register( 'scrolling_labels_changed', lambda v
:
547 setattr( self
.title_label
, 'scrolling', v
) )
549 player
.register( 'stopped', self
.on_player_stopped
)
550 player
.register( 'playing', self
.on_player_playing
)
551 player
.register( 'paused', self
.on_player_paused
)
552 player
.playlist
.register( 'end-of-playlist',
553 self
.on_player_end_of_playlist
)
554 player
.playlist
.register( 'new-track-loaded',
555 self
.on_player_new_track
)
556 player
.playlist
.register( 'new-metadata-available',
557 self
.on_player_new_metadata
)
559 def make_player_tab(self
):
560 main_vbox
= gtk
.VBox()
561 main_vbox
.set_spacing(6)
563 self
.pack_start(main_vbox
, True, True)
565 # a hbox to hold the cover art and metadata vbox
566 metadata_hbox
= gtk
.HBox()
567 metadata_hbox
.set_spacing(6)
568 main_vbox
.pack_start(metadata_hbox
, True, False)
570 self
.cover_art
= gtk
.Image()
571 metadata_hbox
.pack_start( self
.cover_art
, False, False )
573 # vbox to hold metadata
574 metadata_vbox
= gtk
.VBox()
575 metadata_vbox
.set_spacing(8)
576 empty_label
= gtk
.Label()
577 metadata_vbox
.pack_start(empty_label
, True, True)
578 self
.artist_label
= gtk
.Label('')
579 self
.artist_label
.set_ellipsize(pango
.ELLIPSIZE_END
)
580 metadata_vbox
.pack_start(self
.artist_label
, False, False)
581 self
.album_label
= gtk
.Label('')
582 self
.album_label
.set_ellipsize(pango
.ELLIPSIZE_END
)
583 metadata_vbox
.pack_start(self
.album_label
, False, False)
584 self
.title_label
= widgets
.ScrollingLabel( '',
587 delay_btwn_scrolls
=5000,
589 self
.title_label
.scrolling
= settings
.scrolling_labels
590 metadata_vbox
.pack_start(self
.title_label
, False, False)
591 empty_label
= gtk
.Label()
592 metadata_vbox
.pack_start(empty_label
, True, True)
593 metadata_hbox
.pack_start( metadata_vbox
, True, True )
595 progress_eventbox
= gtk
.EventBox()
596 progress_eventbox
.set_events(gtk
.gdk
.BUTTON_PRESS_MASK
)
597 progress_eventbox
.connect(
598 'button-press-event', self
.on_progressbar_changed
)
599 self
.progress
= gtk
.ProgressBar()
600 # make the progress bar more "finger-friendly"
601 if util
.platform
.MAEMO
:
602 self
.progress
.set_size_request( -1, 50 )
603 progress_eventbox
.add(self
.progress
)
604 main_vbox
.pack_start( progress_eventbox
, False, False )
606 # make the button box
607 buttonbox
= gtk
.HBox()
609 # A wrapper to help create DualActionButtons with the right settings
610 create_da
= lambda a
, b
, c
=None, d
=None: widgets
.DualActionButton(
611 a
, b
, c
, d
, settings
.dual_action_button_delay
,
612 settings
.enable_dual_action_btn
)
614 self
.rrewind_button
= create_da(
615 generate_image('media-skip-backward.png'),
616 lambda: self
.do_seek(-1*settings
.seek_long
),
617 generate_image(gtk
.STOCK_GOTO_FIRST
, True),
618 player
.playlist
.prev
)
619 buttonbox
.add(self
.rrewind_button
)
621 self
.rewind_button
= create_da(
622 generate_image('media-seek-backward.png'),
623 lambda: self
.do_seek(-1*settings
.seek_short
))
624 buttonbox
.add(self
.rewind_button
)
626 self
.play_pause_button
= gtk
.Button('')
627 image(self
.play_pause_button
, 'media-playback-start.png')
628 self
.play_pause_button
.connect( 'clicked',
629 self
.on_btn_play_pause_clicked
)
630 self
.play_pause_button
.set_sensitive(False)
631 buttonbox
.add(self
.play_pause_button
)
633 self
.forward_button
= create_da(
634 generate_image('media-seek-forward.png'),
635 lambda: self
.do_seek(settings
.seek_short
))
636 buttonbox
.add(self
.forward_button
)
638 self
.fforward_button
= create_da(
639 generate_image('media-skip-forward.png'),
640 lambda: self
.do_seek(settings
.seek_long
),
641 generate_image(gtk
.STOCK_GOTO_LAST
, True),
642 player
.playlist
.next
)
643 buttonbox
.add(self
.fforward_button
)
645 self
.bookmarks_button
= create_da(
646 generate_image('bookmark-new.png'),
647 player
.add_bookmark_at_current_position
,
648 generate_image(gtk
.STOCK_JUMP_TO
, True),
649 lambda *args
: self
.notify('select-current-item-request'))
650 buttonbox
.add(self
.bookmarks_button
)
651 self
.set_controls_sensitivity(False)
652 main_vbox
.pack_start(buttonbox
, False, False)
654 if util
.platform
.MAEMO
:
655 self
.volume
= hildon
.VVolumebar()
656 self
.volume
.set_property('can-focus', False)
657 self
.volume
.connect('level_changed', self
.volume_changed_hildon
)
658 self
.volume
.connect('mute_toggled', self
.mute_toggled
)
659 self
.__gui
_root
.main_window
.connect( 'key-press-event',
661 self
.pack_start(self
.volume
, False, True)
663 # Add a button to pop out the volume bar
664 self
.volume_button
= gtk
.ToggleButton('')
665 image(self
.volume_button
, 'media-speaker.png')
666 self
.volume_button
.connect('clicked', self
.toggle_volumebar
)
668 'show', lambda x
: self
.volume_button
.set_active(True))
670 'hide', lambda x
: self
.volume_button
.set_active(False))
671 buttonbox
.add(self
.volume_button
)
672 self
.volume_button
.show()
674 # Disable focus for all widgets, so we can use the cursor
675 # keys + enter to directly control our media player, which
676 # is handled by "key-press-event"
678 self
.rrewind_button
, self
.rewind_button
,
679 self
.play_pause_button
, self
.forward_button
,
680 self
.fforward_button
, self
.progress
,
681 self
.bookmarks_button
, self
.volume_button
, ):
682 w
.unset_flags(gtk
.CAN_FOCUS
)
684 self
.volume
= gtk
.VolumeButton()
685 self
.volume
.connect('value-changed', self
.volume_changed_gtk
)
686 buttonbox
.add(self
.volume
)
689 self
.set_volume(settings
.volume
)
691 def set_controls_sensitivity(self
, sensitive
):
692 for button
in self
.forward_button
, self
.rewind_button
, \
693 self
.fforward_button
, self
.rrewind_button
:
695 button
.set_sensitive(sensitive
)
697 # the play/pause button should always be available except
698 # for when the player starts without a file
699 self
.play_pause_button
.set_sensitive(True)
701 def on_dual_action_setting_changed( self
, *args
):
702 for button
in self
.forward_button
, self
.rewind_button
, \
703 self
.fforward_button
, self
.rrewind_button
, \
704 self
.bookmarks_button
:
706 button
.set_longpress_enabled( settings
.enable_dual_action_btn
)
707 button
.set_duration( settings
.dual_action_button_delay
)
709 def on_key_press(self
, widget
, event
):
710 if util
.platform
.MAEMO
:
711 if event
.keyval
== gtk
.keysyms
.F7
: #plus
712 self
.set_volume( min( 1, self
.get_volume() + 0.10 ))
713 elif event
.keyval
== gtk
.keysyms
.F8
: #minus
714 self
.set_volume( max( 0, self
.get_volume() - 0.10 ))
715 elif event
.keyval
== gtk
.keysyms
.Left
: # seek back
716 self
.do_seek( -1 * settings
.seek_long
)
717 elif event
.keyval
== gtk
.keysyms
.Right
: # seek forward
718 self
.do_seek( settings
.seek_long
)
719 elif event
.keyval
== gtk
.keysyms
.Return
: # play/pause
720 self
.on_btn_play_pause_clicked()
722 # The following two functions get and set the
723 # volume from the volume control widgets.
724 def get_volume(self
):
725 if util
.platform
.MAEMO
:
726 return self
.volume
.get_level()/100.0
728 return self
.volume
.get_value()
730 def set_volume(self
, vol
):
731 """ vol is a float from 0 to 1 """
733 if util
.platform
.MAEMO
:
734 self
.volume
.set_level(vol
*100.0)
736 self
.volume
.set_value(vol
)
738 def __set_volume_hide_timer(self
, timeout
, force_show
=False):
739 if force_show
or self
.volume_button
.get_active():
741 if self
.volume_timer_id
is not None:
742 gobject
.source_remove(self
.volume_timer_id
)
743 self
.volume_timer_id
= None
745 self
.volume_timer_id
= gobject
.timeout_add(
746 1000 * timeout
, self
.__volume
_hide
_callback
)
748 def __volume_hide_callback(self
):
749 self
.volume_timer_id
= None
753 def toggle_volumebar(self
, widget
=None):
754 if self
.volume_timer_id
is None:
755 self
.__set
_volume
_hide
_timer
(5)
757 self
.__volume
_hide
_callback
()
759 def volume_changed_gtk(self
, widget
, new_value
=0.5):
760 settings
.volume
= new_value
762 def volume_changed_hildon(self
, widget
):
763 self
.__set
_volume
_hide
_timer
( 4, force_show
=True )
764 settings
.volume
= widget
.get_level()/100.0
766 def mute_toggled(self
, widget
):
767 if widget
.get_mute():
770 settings
.volume
= widget
.get_level()/100.0
772 def on_player_stopped(self
):
773 self
.stop_progress_timer()
774 self
.set_controls_sensitivity(False)
775 image(self
.play_pause_button
, 'media-playback-start.png')
777 def on_player_playing(self
):
778 self
.start_progress_timer()
779 image(self
.play_pause_button
, 'media-playback-pause.png')
780 self
.set_controls_sensitivity(True)
782 def on_player_new_track(self
):
783 for widget
in [self
.title_label
,self
.artist_label
,self
.album_label
]:
784 widget
.set_markup('')
787 self
.cover_art
.hide()
788 self
.has_coverart
= False
790 def on_player_new_metadata(self
):
791 metadata
= player
.playlist
.get_file_metadata()
792 self
.set_metadata(metadata
)
794 if not player
.playing
:
795 position
= player
.playlist
.get_current_position()
796 estimated_length
= metadata
.get('length', 0)
797 self
.set_progress_callback( position
, estimated_length
)
799 def on_player_paused( self
, position
, duration
):
800 self
.stop_progress_timer() # This should save some power
801 self
.set_progress_callback( position
, duration
)
802 image(self
.play_pause_button
, 'media-playback-start.png')
804 def on_player_end_of_playlist(self
, loop
):
807 def reset_progress(self
):
808 self
.progress
.set_fraction(0)
809 self
.set_progress_callback(0,0)
811 def set_progress_callback(self
, time_elapsed
, total_time
):
812 """ times must be in nanoseconds """
813 time_string
= "%s / %s" % ( util
.convert_ns(time_elapsed
),
814 util
.convert_ns(total_time
) )
815 self
.progress
.set_text( time_string
)
816 fraction
= float(time_elapsed
) / float(total_time
) if total_time
else 0
817 self
.progress
.set_fraction( fraction
)
819 def on_progressbar_changed(self
, widget
, event
):
820 if ( not settings
.progress_locked
and
821 event
.type == gtk
.gdk
.BUTTON_PRESS
and event
.button
== 1 ):
822 new_fraction
= event
.x
/float(widget
.get_allocation().width
)
823 resp
= player
.do_seek(percent
=new_fraction
)
825 # Preemptively update the progressbar to make seeking smoother
826 self
.set_progress_callback( *resp
)
828 def on_btn_play_pause_clicked(self
, widget
=None):
829 player
.play_pause_toggle()
831 def progress_timer_callback( self
):
832 if player
.playing
and not player
.seeking
:
833 pos_int
, dur_int
= player
.get_position_duration()
834 # This prevents bogus values from being set while seeking
835 if ( pos_int
> 10**9 ) and ( dur_int
> 10**9 ):
836 self
.set_progress_callback( pos_int
, dur_int
)
839 def start_progress_timer( self
):
840 if self
.progress_timer_id
is not None:
841 self
.stop_progress_timer()
843 self
.progress_timer_id
= gobject
.timeout_add(
844 1000, self
.progress_timer_callback
)
846 def stop_progress_timer( self
):
847 if self
.progress_timer_id
is not None:
848 gobject
.source_remove( self
.progress_timer_id
)
849 self
.progress_timer_id
= None
851 def get_coverart_size( self
):
852 if util
.platform
.MAEMO
:
853 if self
.__gui
_root
.fullscreen
:
854 size
= coverart_sizes
['maemo fullscreen']
856 size
= coverart_sizes
['maemo']
858 size
= coverart_sizes
['normal']
862 def set_coverart( self
, pixbuf
):
863 self
.cover_art
.set_from_pixbuf(pixbuf
)
864 self
.cover_art
.show()
865 self
.has_coverart
= True
867 def set_metadata( self
, tag_message
):
868 tags
= { 'title': self
.title_label
, 'artist': self
.artist_label
,
869 'album': self
.album_label
}
872 if tag_message
.has_key('image') and tag_message
['image'] is not None:
873 value
= tag_message
['image']
875 pbl
= gtk
.gdk
.PixbufLoader()
880 x
, y
= self
.get_coverart_size()
881 pixbuf
= pbl
.get_pixbuf()
882 pixbuf
= pixbuf
.scale_simple( x
, y
, gtk
.gdk
.INTERP_BILINEAR
)
883 self
.set_coverart(pixbuf
)
885 self
.__log
.exception('Error setting coverart...')
887 # set the text metadata
888 for tag
,value
in tag_message
.iteritems():
889 if tags
.has_key(tag
) and value
is not None and value
.strip():
890 tags
[tag
].set_markup('<big>'+value
+'</big>')
891 tags
[tag
].set_alignment( 0.5*int(not self
.has_coverart
), 0.5)
895 # make the title bold
896 tags
[tag
].set_markup('<b><big>'+value
+'</big></b>')
898 if not util
.platform
.MAEMO
:
899 value
+= ' - Panucci'
901 self
.__gui
_root
.main_window
.set_title( value
)
903 def do_seek(self
, seek_amount
):
904 resp
= player
.do_seek(from_current
=seek_amount
*10**9)
906 # Preemptively update the progressbar to make seeking smoother
907 self
.set_progress_callback( *resp
)
911 ##################################################
913 ##################################################
914 class PlaylistTab(gtk
.VBox
):
915 def __init__(self
, main_window
):
916 gtk
.VBox
.__init
__(self
)
917 self
.__log
= logging
.getLogger('panucci.panucci.BookmarksWindow')
918 self
.main
= main_window
920 self
.__model
= gtk
.TreeStore(
921 # uid, name, position
922 gobject
.TYPE_STRING
, gobject
.TYPE_STRING
, gobject
.TYPE_STRING
)
925 self
.treeview
= gtk
.TreeView()
926 self
.treeview
.set_model(self
.__model
)
927 self
.treeview
.set_headers_visible(True)
928 tree_selection
= self
.treeview
.get_selection()
929 # This breaks drag and drop, only use single selection for now
930 # tree_selection.set_mode(gtk.SELECTION_MULTIPLE)
931 tree_selection
.connect('changed', self
.tree_selection_changed
)
933 # The tree lines look nasty on maemo
934 if util
.platform
.DESKTOP
:
935 self
.treeview
.set_enable_tree_lines(True)
938 ncol
= gtk
.TreeViewColumn(_('Name'))
939 ncell
= gtk
.CellRendererText()
940 ncell
.set_property('ellipsize', pango
.ELLIPSIZE_END
)
941 ncell
.set_property('editable', True)
942 ncell
.connect('edited', self
.label_edited
)
943 ncol
.set_expand(True)
944 ncol
.pack_start(ncell
)
945 ncol
.add_attribute(ncell
, 'text', 1)
947 tcol
= gtk
.TreeViewColumn(_('Position'))
948 tcell
= gtk
.CellRendererText()
949 tcol
.pack_start(tcell
)
950 tcol
.add_attribute(tcell
, 'text', 2)
952 self
.treeview
.append_column(ncol
)
953 self
.treeview
.append_column(tcol
)
954 self
.treeview
.connect('drag-data-received', self
.drag_data_recieved
)
955 self
.treeview
.connect('drag_data_get', self
.drag_data_get_data
)
958 ( 'playlist_row_data', gtk
.TARGET_SAME_WIDGET
, 0 ) ]
960 self
.treeview
.enable_model_drag_source(
961 gtk
.gdk
.BUTTON1_MASK
, treeview_targets
, gtk
.gdk
.ACTION_COPY
)
963 self
.treeview
.enable_model_drag_dest(
964 treeview_targets
, gtk
.gdk
.ACTION_COPY
)
966 sw
= gtk
.ScrolledWindow()
967 sw
.set_policy(gtk
.POLICY_AUTOMATIC
, gtk
.POLICY_AUTOMATIC
)
968 sw
.set_shadow_type(gtk
.SHADOW_IN
)
969 sw
.add(self
.treeview
)
972 self
.hbox
= gtk
.HBox()
974 self
.add_button
= gtk
.Button(gtk
.STOCK_NEW
)
975 self
.add_button
.set_use_stock(True)
976 set_stock_button_text( self
.add_button
, _('Add File') )
977 self
.add_button
.connect('clicked', self
.add_file
)
978 self
.hbox
.pack_start(self
.add_button
, True, True)
980 self
.dir_button
= gtk
.Button(gtk
.STOCK_OPEN
)
981 self
.dir_button
.set_use_stock(True)
982 set_stock_button_text( self
.dir_button
, _('Add Directory') )
983 self
.dir_button
.connect('clicked', self
.add_directory
)
984 self
.hbox
.pack_start(self
.dir_button
, True, True)
986 self
.remove_button
= widgets
.DualActionButton(
987 generate_image(gtk
.STOCK_REMOVE
, True),
988 self
.remove_bookmark
,
989 generate_image(gtk
.STOCK_CANCEL
, True),
990 lambda *a
: player
.playlist
.reset_playlist() )
991 #self.remove_button.set_use_stock(True)
992 #self.remove_button.connect('clicked', self.remove_bookmark)
993 self
.hbox
.pack_start(self
.remove_button
, True, True)
995 self
.jump_button
= gtk
.Button(gtk
.STOCK_JUMP_TO
)
996 self
.jump_button
.set_use_stock(True)
997 self
.jump_button
.connect('clicked', self
.jump_bookmark
)
998 self
.hbox
.pack_start(self
.jump_button
, True, True)
1000 self
.info_button
= gtk
.Button()
1001 self
.info_button
.add(
1002 gtk
.image_new_from_stock(gtk
.STOCK_INFO
, gtk
.ICON_SIZE_BUTTON
))
1003 self
.info_button
.connect('clicked', self
.show_playlist_item_details
)
1004 self
.hbox
.pack_start(self
.info_button
, True, True)
1006 self
.pack_start(self
.hbox
, False, True)
1008 player
.playlist
.register( 'file_queued',
1009 lambda x
,y
,z
: self
.update_model() )
1010 player
.playlist
.register( 'bookmark_added', self
.on_bookmark_added
)
1014 def tree_selection_changed(self
, treeselection
):
1015 count
= treeselection
.count_selected_rows()
1016 self
.remove_button
.set_sensitive(count
> 0)
1017 self
.jump_button
.set_sensitive(count
== 1)
1018 self
.info_button
.set_sensitive(count
== 1)
1020 def drag_data_get_data(
1021 self
, treeview
, context
, selection
, target_id
, timestamp
):
1023 treeselection
= treeview
.get_selection()
1024 model
, iter = treeselection
.get_selected()
1025 # only allow moving around top-level parents
1026 if model
.iter_parent(iter) is None:
1027 # send the path of the selected row
1028 data
= model
.get_string_from_iter(iter)
1029 selection
.set(selection
.target
, 8, data
)
1031 self
.__log
.debug("Can't move children...")
1033 def drag_data_recieved(
1034 self
, treeview
, context
, x
, y
, selection
, info
, timestamp
):
1036 drop_info
= treeview
.get_dest_row_at_pos(x
, y
)
1038 # TODO: If user drags the row past the last row, drop_info is None
1039 # I'm not sure if it's safe to simply assume that None is
1040 # euqivalent to the last row...
1041 if None not in [ drop_info
and selection
.data
]:
1042 model
= treeview
.get_model()
1043 path
, position
= drop_info
1045 from_iter
= model
.get_iter_from_string(selection
.data
)
1047 # make sure the to_iter doesn't have a parent
1048 to_iter
= model
.get_iter(path
)
1049 if model
.iter_parent(to_iter
) is not None:
1050 to_iter
= model
.iter_parent(to_iter
)
1052 from_row
= model
.get_path(from_iter
)[0]
1055 if ( position
== gtk
.TREE_VIEW_DROP_BEFORE
or
1056 position
== gtk
.TREE_VIEW_DROP_INTO_OR_BEFORE
):
1057 model
.move_before( from_iter
, to_iter
)
1058 to_row
= to_row
- 1 if from_row
< to_row
else to_row
1059 elif ( position
== gtk
.TREE_VIEW_DROP_AFTER
or
1060 position
== gtk
.TREE_VIEW_DROP_INTO_OR_AFTER
):
1061 model
.move_after( from_iter
, to_iter
)
1062 to_row
= to_row
+ 1 if from_row
> to_row
else to_row
1064 self
.__log
.debug('Drop not supported: %s', position
)
1066 # don't do anything if we're not actually moving rows around
1067 if from_row
!= to_row
:
1068 player
.playlist
.move_item( from_row
, to_row
)
1071 self
.__log
.debug('No drop_data or selection.data available')
1073 def update_model(self
):
1074 plist
= player
.playlist
1075 path_info
= self
.treeview
.get_path_at_pos(0,0)
1076 path
= path_info
[0] if path_info
is not None else None
1078 self
.__model
.clear()
1081 for item
, data
in plist
.get_playlist_item_ids():
1082 parent
= self
.__model
.append(None, (item
, data
.get('title'), None))
1084 for bid
, bname
, bpos
in plist
.get_bookmarks_from_item_id( item
):
1085 nice_bpos
= util
.convert_ns(bpos
)
1086 self
.__model
.append( parent
, (bid
, bname
, nice_bpos
) )
1088 self
.treeview
.expand_all()
1090 if path
is not None:
1091 self
.treeview
.scroll_to_cell(path
)
1093 def label_edited(self
, cellrenderer
, path
, new_text
):
1094 iter = self
.__model
.get_iter(path
)
1095 old_text
= self
.__model
.get_value(iter, 1)
1097 if new_text
.strip() and old_text
!= new_text
:
1098 # this loop will only run once, because only one cell can be
1099 # edited at a time, we use it to get the item and bookmark ids
1100 for m
, bkmk_id
, biter
, item_id
, iiter
in self
.__cur
_selection
():
1101 self
.__model
.set_value(iter, 1, new_text
)
1102 player
.playlist
.update_bookmark(
1103 item_id
, bkmk_id
, name
=new_text
)
1105 self
.__model
.set_value(iter, 1, old_text
)
1107 def on_bookmark_added(self
, parent_id
, bookmark_name
, position
):
1108 util
.notify(_('Bookmark added: %s') % bookmark_name
)
1111 def add_file(self
, widget
):
1112 filename
= get_file_from_filechooser(self
.main
.main_window
)
1113 if filename
is not None:
1114 player
.playlist
.append(filename
)
1116 def add_directory(self
, widget
):
1117 directory
= get_file_from_filechooser(
1118 self
.main
.main_window
, folder
=True )
1119 if directory
is not None:
1120 player
.playlist
.load_directory(directory
, append
=True)
1122 def __cur_selection(self
):
1123 selection
= self
.treeview
.get_selection()
1124 model
, bookmark_paths
= selection
.get_selected_rows()
1126 # Convert the paths to gtk.TreeRowReference objects, because we
1127 # might modify the model while this generator is running
1128 bookmark_refs
= [gtk
.TreeRowReference(model
, p
) for p
in bookmark_paths
]
1130 for reference
in bookmark_refs
:
1131 bookmark_iter
= model
.get_iter(reference
.get_path())
1132 item_iter
= model
.iter_parent(bookmark_iter
)
1134 # bookmark_iter is actually an item_iter
1135 if item_iter
is None:
1136 item_iter
= bookmark_iter
1137 item_id
= model
.get_value(item_iter
, 0)
1138 bookmark_id
, bookmark_iter
= None, None
1140 bookmark_id
= model
.get_value(bookmark_iter
, 0)
1141 item_id
= model
.get_value(item_iter
, 0)
1143 yield model
, bookmark_id
, bookmark_iter
, item_id
, item_iter
1145 def remove_bookmark(self
, w
=None):
1146 for model
, bkmk_id
, bkmk_iter
, item_id
, item_iter
in self
.__cur
_selection
():
1147 player
.playlist
.remove_bookmark( item_id
, bkmk_id
)
1148 if bkmk_iter
is not None:
1149 model
.remove(bkmk_iter
)
1150 elif item_iter
is not None:
1151 model
.remove(item_iter
)
1153 def select_current_item(self
):
1154 model
= self
.treeview
.get_model()
1155 selection
= self
.treeview
.get_selection()
1156 current_item_id
= str(player
.playlist
.get_current_item())
1157 for row
in iter(model
):
1158 if model
.get_value(row
.iter, 0) == current_item_id
:
1159 selection
.unselect_all()
1160 self
.treeview
.set_cursor(row
.path
)
1161 self
.treeview
.scroll_to_cell(row
.path
, use_align
=True)
1164 def show_playlist_item_details(self
, w
):
1165 selection
= self
.treeview
.get_selection()
1166 if selection
.count_selected_rows() == 1:
1167 selected
= self
.__cur
_selection
().next()
1168 model
, bkmk_id
, bkmk_iter
, item_id
, item_iter
= selected
1169 playlist_item
= player
.playlist
.get_item_by_id(item_id
)
1170 PlaylistItemDetails(self
.main
, playlist_item
)
1172 def jump_bookmark(self
, w
):
1173 selected
= list(self
.__cur
_selection
())
1174 if len(selected
) == 1:
1175 # It should be guranteed by the fact that we only enable the
1176 # "Jump to" button when the selection count equals 1.
1177 model
, bkmk_id
, bkmk_iter
, item_id
, item_iter
= selected
.pop(0)
1178 player
.playlist
.load_from_bookmark_id(item_id
, bkmk_id
)
1180 # FIXME: The player/playlist should be able to take care of this
1181 if not player
.playing
:
1185 ##################################################
1186 # PlaylistItemDetails
1187 ##################################################
1188 class PlaylistItemDetails(gtk
.Dialog
):
1189 def __init__(self
, main
, playlist_item
):
1190 gtk
.Dialog
.__init
__( self
, _('Playlist item details'),
1191 main
.main_window
, gtk
.DIALOG_MODAL
,
1192 (gtk
.STOCK_CLOSE
, gtk
.RESPONSE_OK
))
1195 self
.fill(playlist_item
)
1196 self
.set_has_separator(False)
1197 self
.set_resizable(False)
1202 def fill(self
, playlist_item
):
1203 t
= gtk
.Table(10, 2)
1204 self
.vbox
.pack_start(t
, expand
=False)
1206 metadata
= playlist_item
.metadata
1208 t
.attach(gtk
.Label(_('Custom title:')), 0, 1, 0, 1)
1209 t
.attach(gtk
.Label(_('ID:')), 0, 1, 1, 2)
1210 t
.attach(gtk
.Label(_('Playlist ID:')), 0, 1, 2, 3)
1211 t
.attach(gtk
.Label(_('Filepath:')), 0, 1, 3, 4)
1214 for key
in metadata
:
1215 if metadata
[key
] is not None:
1216 t
.attach( gtk
.Label(key
.capitalize()+':'),
1217 0, 1, row_num
, row_num
+1 )
1220 t
.foreach(lambda x
, y
: x
.set_alignment(1, 0.5), None)
1221 t
.foreach(lambda x
, y
: x
.set_markup('<b>%s</b>' % x
.get_label()), None)
1223 t
.attach(gtk
.Label(playlist_item
.title
or _('<not modified>')),1,2,0,1)
1224 t
.attach(gtk
.Label(str(playlist_item
)), 1, 2, 1, 2)
1225 t
.attach(gtk
.Label(playlist_item
.playlist_id
), 1, 2, 2, 3)
1226 t
.attach(gtk
.Label(playlist_item
.filepath
), 1, 2, 3, 4)
1229 for key
in metadata
:
1230 value
= metadata
[key
]
1232 value
= util
.convert_ns(value
)
1233 if metadata
[key
] is not None:
1234 t
.attach( gtk
.Label( str(value
) or _('<not set>')),
1235 1, 2, row_num
, row_num
+1)
1238 t
.foreach(lambda x
, y
: x
.get_alignment() == (0.5, 0.5) and \
1239 x
.set_alignment(0, 0.5), None)
1241 t
.set_border_width(8)
1242 t
.set_row_spacings(4)
1243 t
.set_col_spacings(8)
1245 l
= gtk
.ListStore(str, str)
1247 cr
= gtk
.CellRendererText()
1248 cr
.set_property('ellipsize', pango
.ELLIPSIZE_END
)
1249 c
= gtk
.TreeViewColumn(_('Title'), cr
, text
=0)
1252 c
= gtk
.TreeViewColumn(_('Time'), gtk
.CellRendererText(), text
=1)
1254 playlist_item
.load_bookmarks()
1255 for bookmark
in playlist_item
.bookmarks
:
1256 l
.append([bookmark
.bookmark_name
, \
1257 util
.convert_ns(bookmark
.seek_position
)])
1259 sw
= gtk
.ScrolledWindow()
1260 sw
.set_shadow_type(gtk
.SHADOW_IN
)
1262 sw
.set_policy(gtk
.POLICY_AUTOMATIC
, gtk
.POLICY_AUTOMATIC
)
1263 e
= gtk
.Expander(_('Bookmarks'))
1265 self
.vbox
.pack_start(e
)
1268 def run(filename
=None):
1269 ossohelper
.application_init('org.panucci', '0.4')
1270 PanucciGUI( filename
)
1272 ossohelper
.application_exit()
1274 if __name__
== '__main__':
1275 log
.error( 'Use the "panucci" executable to run this program.' )
1276 log
.error( 'Exiting...' )