3 # This file is part of Panucci.
4 # Copyright (c) 2008-2009 The Panucci Audiobook and Podcast Player Project
6 # Panucci is free software: you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation, either version 3 of the License, or
9 # (at your option) any later version.
11 # Panucci is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
16 # You should have received a copy of the GNU General Public License
17 # along with Panucci. If not, see <http://www.gnu.org/licenses/>.
19 # Based on http://thpinfo.com/2008/panucci/:
20 # A resuming media player for Podcasts and Audiobooks
21 # Copyright (c) 2008-05-26 Thomas Perl <thpinfo.com>
22 # (based on http://pygstdocs.berlios.de/pygst-tutorial/seeking.html)
34 # At the moment, we don't have gettext support, so
35 # make a dummy "_" function to passthrough the string
38 log
= logging
.getLogger('panucci.panucci')
45 if util
.platform
== util
.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
54 about_name
= 'Panucci'
55 about_text
= _('Resuming audiobook and podcast player')
56 about_authors
= ['Thomas Perl', 'Nick (nikosapi)', 'Matthew Taylor']
57 about_website
= 'http://panucci.garage.maemo.org/'
59 donate_wishlist_url
= 'http://www.amazon.de/gp/registry/2PD2MYGHE6857'
60 donate_device_url
= 'http://maemo.gpodder.org/donate.html'
65 coverart_names
= [ 'cover', 'cover.jpg', 'cover.png' ]
66 coverart_size
= [200, 200] if util
.platform
== util
.MAEMO
else [110, 110]
68 gtk
.about_dialog_set_url_hook(util
.open_link
, None)
69 gtk
.icon_size_register('panucci-button', 32, 32)
71 def image(widget
, filename
, is_stock
=False):
72 widget
.remove(widget
.get_child())
75 image
= gtk
.image_new_from_stock(
76 filename
, gtk
.icon_size_from_name('panucci-button') )
78 filename
= util
.find_image(filename
)
79 if filename
is not None:
80 image
= gtk
.image_new_from_file(filename
)
83 if util
.platform
== util
.MAEMO
:
84 image
.set_padding(20, 20)
86 image
.set_padding(5, 5)
90 def dialog( toplevel_window
, title
, question
, description
):
91 """ Present the user with a yes/no/cancel dialog
92 Reponse: Yes = True, No = False, Cancel = None """
94 dlg
= gtk
.MessageDialog( toplevel_window
, gtk
.DIALOG_MODAL
,
95 gtk
.MESSAGE_QUESTION
)
97 dlg
.add_button( gtk
.STOCK_CANCEL
, gtk
.RESPONSE_CANCEL
)
98 dlg
.add_button( gtk
.STOCK_NO
, gtk
.RESPONSE_NO
)
99 dlg
.add_button( gtk
.STOCK_YES
, gtk
.RESPONSE_YES
)
100 dlg
.set_markup( '<span weight="bold" size="larger">%s</span>\n\n%s' % (
101 question
, description
))
106 if response
== gtk
.RESPONSE_YES
:
108 elif response
== gtk
.RESPONSE_NO
:
110 elif response
in [gtk
.RESPONSE_CANCEL
, gtk
.RESPONSE_DELETE_EVENT
]:
113 def get_file_from_filechooser(
114 toplevel_window
, folder
=False, save_file
=False, save_to
=None):
117 open_action
= gtk
.FILE_CHOOSER_ACTION_SELECT_FOLDER
119 open_action
= gtk
.FILE_CHOOSER_ACTION_OPEN
121 if util
.platform
== util
.MAEMO
:
123 args
= ( toplevel_window
, gtk
.FILE_CHOOSER_ACTION_SAVE
)
125 args
= ( toplevel_window
, open_action
)
127 dlg
= hildon
.FileChooserDialog( *args
)
130 args
= ( _('Select file to save playlist to'), None,
131 gtk
.FILE_CHOOSER_ACTION_SAVE
,
132 (( gtk
.STOCK_CANCEL
, gtk
.RESPONSE_REJECT
,
133 gtk
.STOCK_SAVE
, gtk
.RESPONSE_OK
)) )
135 args
= ( _('Select podcast or audiobook'), None, open_action
,
136 (( gtk
.STOCK_CANCEL
, gtk
.RESPONSE_REJECT
,
137 gtk
.STOCK_OPEN
, gtk
.RESPONSE_OK
)) )
139 dlg
= gtk
.FileChooserDialog(*args
)
141 current_folder
= os
.path
.expanduser(settings
.last_folder
)
143 if current_folder
is not None and os
.path
.isdir(current_folder
):
144 dlg
.set_current_folder(current_folder
)
146 if save_file
and save_to
is not None:
147 dlg
.set_current_name(save_to
)
149 if dlg
.run() == gtk
.RESPONSE_OK
:
150 filename
= dlg
.get_filename()
151 settings
.last_folder
= dlg
.get_current_folder()
158 def set_stock_button_text( button
, text
):
159 alignment
= button
.get_child()
160 hbox
= alignment
.get_child()
161 image
, label
= hbox
.get_children()
164 class PlaylistTab(gtk
.VBox
):
165 def __init__(self
, main_window
):
166 gtk
.VBox
.__init
__(self
)
167 self
.__log
= logging
.getLogger('panucci.panucci.BookmarksWindow')
168 self
.main
= main_window
171 self
.treeview
= gtk
.TreeView()
172 self
.treeview
.set_headers_visible(True)
174 # The tree lines look nasty on maemo
175 if util
.platform
== util
.LINUX
:
176 self
.treeview
.set_enable_tree_lines(True)
179 ncol
= gtk
.TreeViewColumn(_('Name'))
180 ncell
= gtk
.CellRendererText()
181 ncell
.set_property('editable', True)
182 ncell
.connect('edited', self
.label_edited
)
183 ncol
.pack_start(ncell
)
184 ncol
.add_attribute(ncell
, 'text', 1)
186 tcol
= gtk
.TreeViewColumn(_('Position'))
187 tcell
= gtk
.CellRendererText()
188 tcol
.pack_start(tcell
)
189 tcol
.add_attribute(tcell
, 'text', 2)
191 self
.treeview
.append_column(ncol
)
192 self
.treeview
.append_column(tcol
)
193 self
.treeview
.connect('drag-data-received', self
.drag_data_recieved
)
194 self
.treeview
.connect('drag_data_get', self
.drag_data_get_data
)
197 ( 'playlist_row_data', gtk
.TARGET_SAME_WIDGET
, 0 ) ]
199 self
.treeview
.enable_model_drag_source(
200 gtk
.gdk
.BUTTON1_MASK
, treeview_targets
, gtk
.gdk
.ACTION_COPY
)
202 self
.treeview
.enable_model_drag_dest(
203 treeview_targets
, gtk
.gdk
.ACTION_COPY
)
205 sw
= gtk
.ScrolledWindow()
206 sw
.set_policy(gtk
.POLICY_AUTOMATIC
, gtk
.POLICY_AUTOMATIC
)
207 sw
.set_shadow_type(gtk
.SHADOW_IN
)
208 sw
.add(self
.treeview
)
211 self
.hbox
= gtk
.HBox()
213 self
.add_button
= gtk
.Button(gtk
.STOCK_NEW
)
214 self
.add_button
.set_use_stock(True)
215 set_stock_button_text( self
.add_button
, _('Add File') )
216 self
.add_button
.connect('clicked', self
.add_file
)
217 self
.hbox
.pack_start(self
.add_button
, True, False)
219 self
.dir_button
= gtk
.Button(gtk
.STOCK_OPEN
)
220 self
.dir_button
.set_use_stock(True)
221 set_stock_button_text( self
.dir_button
, _('Add Directory') )
222 self
.dir_button
.connect('clicked', self
.add_directory
)
223 self
.hbox
.pack_start(self
.dir_button
, True, False)
225 self
.remove_button
= gtk
.Button(gtk
.STOCK_REMOVE
)
226 self
.remove_button
.set_use_stock(True)
227 self
.remove_button
.connect('clicked', self
.remove_bookmark
)
228 self
.hbox
.pack_start(self
.remove_button
, True, False)
230 self
.jump_button
= gtk
.Button(gtk
.STOCK_JUMP_TO
)
231 self
.jump_button
.set_use_stock(True)
232 self
.jump_button
.connect('clicked', self
.jump_bookmark
)
233 self
.hbox
.pack_start(self
.jump_button
, True, False)
234 self
.pack_start(self
.hbox
, False, True)
236 player
.playlist
.register(
237 'file_queued', lambda x
,y
,z
: self
.update_model() )
241 def drag_data_get_data(
242 self
, treeview
, context
, selection
, target_id
, timestamp
):
244 treeselection
= treeview
.get_selection()
245 model
, iter = treeselection
.get_selected()
246 # only allow moving around top-level parents
247 if model
.iter_parent(iter) is None:
248 # send the path of the selected row
249 data
= model
.get_string_from_iter(iter)
250 selection
.set(selection
.target
, 8, data
)
252 self
.__log
.debug("Can't move children...")
254 def drag_data_recieved(
255 self
, treeview
, context
, x
, y
, selection
, info
, timestamp
):
257 drop_info
= treeview
.get_dest_row_at_pos(x
, y
)
259 # TODO: If user drags the row past the last row, drop_info is None
260 # I'm not sure if it's safe to simply assume that None is
261 # euqivalent to the last row...
262 if None not in [ drop_info
and selection
.data
]:
263 model
= treeview
.get_model()
264 path
, position
= drop_info
266 from_iter
= model
.get_iter_from_string(selection
.data
)
268 # make sure the to_iter doesn't have a parent
269 to_iter
= model
.get_iter(path
)
270 if model
.iter_parent(to_iter
) is not None:
271 to_iter
= model
.iter_parent(to_iter
)
273 from_row
= model
.get_path(from_iter
)[0]
276 if ( position
== gtk
.TREE_VIEW_DROP_BEFORE
or
277 position
== gtk
.TREE_VIEW_DROP_INTO_OR_BEFORE
):
278 model
.move_before( from_iter
, to_iter
)
279 to_row
= to_row
- 1 if from_row
< to_row
else to_row
280 elif ( position
== gtk
.TREE_VIEW_DROP_AFTER
or
281 position
== gtk
.TREE_VIEW_DROP_INTO_OR_AFTER
):
282 model
.move_after( from_iter
, to_iter
)
283 to_row
= to_row
+ 1 if from_row
> to_row
else to_row
285 self
.__log
.debug('Drop not supported: %s', position
)
287 # don't do anything if we're not actually moving rows around
288 if from_row
!= to_row
:
289 player
.playlist
.move_item( from_row
, to_row
)
292 self
.__log
.debug('No drop_data or selection.data available')
294 def update_model(self
):
295 self
.model
= player
.playlist
.get_bookmark_model()
296 self
.treeview
.set_model(self
.model
)
297 self
.treeview
.expand_all()
299 def label_edited(self
, cellrenderer
, path
, new_text
):
300 iter = self
.model
.get_iter(path
)
301 old_text
= self
.model
.get_value(iter, 1)
304 if old_text
!= new_text
:
305 self
.model
.set_value(iter, 1, new_text
)
306 m
, bkmk_id
, biter
, item_id
, iiter
= self
.__cur
_selection
()
308 player
.playlist
.update_bookmark(
309 item_id
, bkmk_id
, name
=new_text
)
311 self
.model
.set_value(iter, 1, old_text
)
313 def add_bookmark(self
, w
=None, lbl
=None, pos
=None):
314 (label
, position
) = player
.get_formatted_position(pos
)
315 label
= label
if lbl
is None else lbl
316 position
= position
if pos
is None else pos
317 player
.playlist
.save_bookmark( label
, position
)
318 util
.notify(_('Bookmark Added.'))
321 def add_file(self
, widget
):
322 filename
= get_file_from_filechooser(self
.main
.main_window
)
323 if filename
is not None:
324 player
.playlist
.append(filename
)
326 def add_directory(self
, widget
):
327 directory
= get_file_from_filechooser(
328 self
.main
.main_window
, folder
=True )
329 if directory
is not None:
330 player
.playlist
.load_directory(directory
, append
=True)
332 def __cur_selection(self
):
333 bookmark_id
, bookmark_iter
, item_id
, item_iter
= (None,)*4
335 selection
= self
.treeview
.get_selection()
336 # Assume the user selects a bookmark.
337 # bookmark_iter will get set to None if that is not the case...
338 model
, bookmark_iter
= selection
.get_selected()
340 if bookmark_iter
is not None:
341 item_iter
= model
.iter_parent(bookmark_iter
)
343 # bookmark_iter is actually an item_iter
344 if item_iter
is None:
345 item_iter
= bookmark_iter
346 item_id
= model
.get_value(item_iter
, 0)
347 bookmark_id
, bookmark_iter
= None, None
349 bookmark_id
= model
.get_value(bookmark_iter
, 0)
350 item_id
= model
.get_value(item_iter
, 0)
352 return model
, bookmark_id
, bookmark_iter
, item_id
, item_iter
354 def remove_bookmark(self
, w
):
355 model
, bkmk_id
, bkmk_iter
, item_id
, item_iter
= self
.__cur
_selection
()
356 player
.playlist
.remove_bookmark( item_id
, bkmk_id
)
357 if bkmk_iter
is not None:
358 model
.remove(bkmk_iter
)
359 elif item_iter
is not None:
360 model
.remove(item_iter
)
362 def jump_bookmark(self
, w
):
363 model
, bkmk_id
, bkmk_iter
, item_id
, item_iter
= self
.__cur
_selection
()
364 if item_iter
is not None:
365 player
.playlist
.load_from_bookmark_id( item_id
, bkmk_id
)
367 # FIXME: The player/playlist should be able to take care of this
368 if not player
.playing
:
371 class GTK_Main(object):
373 def __init__(self
, filename
=None):
374 self
.__log
= logging
.getLogger('panucci.panucci.GTK_Main')
375 interface
.register_gui(self
)
376 self
.pickle_file_conversion()
378 self
.recent_files
= []
379 self
.progress_timer_id
= None
380 self
.volume_timer_id
= None
381 self
.make_main_window()
382 self
.has_coverart
= False
383 self
.set_volume(settings
.volume
)
385 if util
.platform
==util
.MAEMO
and interface
.headset_device
is not None:
386 # Enable play/pause with headset button
387 interface
.headset_device
.connect_to_signal(
388 'Condition', self
.handle_headset_button
)
390 player
.register( 'stopped', self
.on_player_stopped
)
391 player
.register( 'playing', self
.on_player_playing
)
392 player
.register( 'paused', self
.on_player_paused
)
393 player
.register( 'end_of_playlist', self
.on_player_end_of_playlist
)
394 player
.playlist
.register('new_track_metadata',self
.on_player_new_track
)
395 player
.playlist
.register( 'file_queued', self
.on_file_queued
)
396 player
.init(filepath
=filename
)
398 def make_main_window(self
):
401 if util
.platform
== util
.MAEMO
:
402 self
.app
= hildon
.Program()
403 window
= hildon
.Window()
404 self
.app
.add_window(window
)
406 window
= gtk
.Window(gtk
.WINDOW_TOPLEVEL
)
408 window
.set_title('Panucci')
409 self
.window_icon
= util
.find_image('panucci.png')
410 if self
.window_icon
is not None:
411 window
.set_icon_from_file( self
.window_icon
)
412 window
.set_default_size(400, -1)
413 window
.set_border_width(0)
414 window
.connect("destroy", self
.destroy
)
415 self
.main_window
= window
417 if util
.platform
== util
.MAEMO
:
418 window
.set_menu(self
.create_menu())
420 menu_vbox
= gtk
.VBox()
421 menu_vbox
.set_spacing(0)
422 window
.add(menu_vbox
)
423 menu_bar
= gtk
.MenuBar()
424 root_menu
= gtk
.MenuItem('Panucci')
425 root_menu
.set_submenu(self
.create_menu())
426 menu_bar
.append(root_menu
)
427 menu_vbox
.pack_start(menu_bar
, False, False, 0)
430 self
.notebook
= gtk
.Notebook()
432 if util
.platform
== util
.MAEMO
:
433 window
.add(self
.notebook
)
435 menu_vbox
.pack_end(self
.notebook
, True, True, 6)
437 main_hbox
= gtk
.HBox()
438 self
.notebook
.append_page(main_hbox
, gtk
.Label(_('Player')))
439 self
.notebook
.set_tab_label_packing(main_hbox
,True,True,gtk
.PACK_START
)
441 main_vbox
= gtk
.VBox()
442 main_vbox
.set_spacing(6)
443 # add a vbox to the main_hbox
444 main_hbox
.pack_start(main_vbox
, True, True)
446 # a hbox to hold the cover art and metadata vbox
447 metadata_hbox
= gtk
.HBox()
448 metadata_hbox
.set_spacing(6)
449 main_vbox
.pack_start(metadata_hbox
, True, False)
451 self
.cover_art
= gtk
.Image()
452 metadata_hbox
.pack_start( self
.cover_art
, False, False )
454 # vbox to hold metadata
455 metadata_vbox
= gtk
.VBox()
456 metadata_vbox
.set_spacing(8)
457 empty_label
= gtk
.Label()
458 metadata_vbox
.pack_start(empty_label
, True, True)
459 self
.artist_label
= gtk
.Label('')
460 self
.artist_label
.set_ellipsize(pango
.ELLIPSIZE_END
)
461 metadata_vbox
.pack_start(self
.artist_label
, False, False)
462 self
.album_label
= gtk
.Label('')
463 self
.album_label
.set_ellipsize(pango
.ELLIPSIZE_END
)
464 metadata_vbox
.pack_start(self
.album_label
, False, False)
465 self
.title_label
= gtk
.Label('')
466 self
.title_label
.set_line_wrap(True)
467 metadata_vbox
.pack_start(self
.title_label
, False, False)
468 empty_label
= gtk
.Label()
469 metadata_vbox
.pack_start(empty_label
, True, True)
470 metadata_hbox
.pack_start( metadata_vbox
, True, True )
472 progress_eventbox
= gtk
.EventBox()
473 progress_eventbox
.set_events(gtk
.gdk
.BUTTON_PRESS_MASK
)
474 progress_eventbox
.connect(
475 'button-press-event', self
.on_progressbar_changed
)
476 self
.progress
= gtk
.ProgressBar()
477 # make the progress bar more "finger-friendly"
478 if util
.platform
== util
.MAEMO
:
479 self
.progress
.set_size_request( -1, 50 )
480 progress_eventbox
.add(self
.progress
)
481 main_vbox
.pack_start( progress_eventbox
, False, False )
483 # make the button box
484 buttonbox
= gtk
.HBox()
485 self
.rrewind_button
= gtk
.Button('')
486 image(self
.rrewind_button
, 'media-skip-backward.png')
487 self
.rrewind_button
.connect(
488 'clicked', self
.seekbutton_callback
, -1*long_seek
)
489 buttonbox
.add(self
.rrewind_button
)
490 self
.rewind_button
= gtk
.Button('')
491 image(self
.rewind_button
, 'media-seek-backward.png')
492 self
.rewind_button
.connect(
493 'clicked', self
.seekbutton_callback
, -1*short_seek
)
494 buttonbox
.add(self
.rewind_button
)
495 self
.play_pause_button
= gtk
.Button('')
496 image(self
.play_pause_button
, gtk
.STOCK_OPEN
, True)
497 self
.button_handler_id
= self
.play_pause_button
.connect(
498 'clicked', self
.open_file_callback
)
499 buttonbox
.add(self
.play_pause_button
)
500 self
.forward_button
= gtk
.Button('')
501 image(self
.forward_button
, 'media-seek-forward.png')
502 self
.forward_button
.connect(
503 'clicked', self
.seekbutton_callback
, short_seek
)
504 buttonbox
.add(self
.forward_button
)
505 self
.fforward_button
= gtk
.Button('')
506 image(self
.fforward_button
, 'media-skip-forward.png')
507 self
.fforward_button
.connect(
508 'clicked', self
.seekbutton_callback
, long_seek
)
509 buttonbox
.add(self
.fforward_button
)
510 self
.bookmarks_button
= gtk
.Button('')
511 image(self
.bookmarks_button
, 'bookmark-new.png')
512 buttonbox
.add(self
.bookmarks_button
)
513 self
.set_controls_sensitivity(False)
514 main_vbox
.pack_start(buttonbox
, False, False)
516 self
.playlist_tab
= PlaylistTab(self
)
517 self
.bookmarks_button
.connect('clicked',self
.playlist_tab
.add_bookmark
)
518 self
.notebook
.append_page(self
.playlist_tab
, gtk
.Label(_('Playlist')))
519 self
.notebook
.set_tab_label_packing(
520 self
.playlist_tab
, True, True, gtk
.PACK_START
)
523 self
.notebook
.set_current_page(0)
525 if util
.platform
== util
.MAEMO
:
526 self
.volume
= hildon
.VVolumebar()
527 self
.volume
.set_property('can-focus', False)
528 self
.volume
.connect('level_changed', self
.volume_changed_hildon
)
529 self
.volume
.connect('mute_toggled', self
.mute_toggled
)
530 window
.connect('key-press-event', self
.on_key_press
)
531 main_hbox
.pack_start(self
.volume
, False, True)
533 # Add a button to pop out the volume bar
534 self
.volume_button
= gtk
.ToggleButton('')
535 image(self
.volume_button
, 'media-speaker.png')
536 self
.volume_button
.connect('clicked', self
.toggle_volumebar
)
538 'show', lambda x
: self
.volume_button
.set_active(True))
540 'hide', lambda x
: self
.volume_button
.set_active(False))
541 buttonbox
.add(self
.volume_button
)
542 self
.volume_button
.show()
544 # Disable focus for all widgets, so we can use the cursor
545 # keys + enter to directly control our media player, which
546 # is handled by "key-press-event"
548 self
.rrewind_button
, self
.rewind_button
,
549 self
.play_pause_button
, self
.forward_button
,
550 self
.fforward_button
, self
.progress
,
551 self
.bookmarks_button
, self
.volume_button
, ):
552 w
.unset_flags(gtk
.CAN_FOCUS
)
554 self
.volume
= gtk
.VolumeButton()
555 self
.volume
.connect('value-changed', self
.volume_changed_gtk
)
556 buttonbox
.add(self
.volume
)
559 def create_menu(self
):
563 menu_open
= gtk
.ImageMenuItem(gtk
.STOCK_OPEN
)
564 menu_open
.connect("activate", self
.open_file_callback
)
565 menu
.append(menu_open
)
567 # the recent files menu
568 self
.menu_recent
= gtk
.MenuItem(_('Recent Files'))
569 menu
.append(self
.menu_recent
)
570 self
.create_recent_files_menu()
572 # the settings sub-menu
573 menu_settings
= gtk
.MenuItem(_('Settings'))
574 menu
.append(menu_settings
)
576 menu_settings_sub
= gtk
.Menu()
577 menu_settings
.set_submenu(menu_settings_sub
)
579 menu_settings_lock_progress
= gtk
.CheckMenuItem(_('Lock Progress Bar'))
580 menu_settings_lock_progress
.connect('toggled', lambda w
:
581 setattr( settings
, 'progress_locked', w
.get_active()))
582 menu_settings_lock_progress
.set_active(self
.lock_progress
)
583 menu_settings_sub
.append(menu_settings_lock_progress
)
585 menu
.append(gtk
.SeparatorMenuItem())
587 menu_about
= gtk
.ImageMenuItem(gtk
.STOCK_ABOUT
)
588 menu_about
.connect("activate", self
.show_about
, self
.main_window
)
589 menu
.append(menu_about
)
591 menu
.append(gtk
.SeparatorMenuItem())
593 menu_quit
= gtk
.ImageMenuItem(gtk
.STOCK_QUIT
)
594 menu_quit
.connect("activate", self
.destroy
)
595 menu
.append(menu_quit
)
599 def create_recent_files_menu( self
):
600 max_files
= settings
.max_recent_files
601 self
.recent_files
= player
.playlist
.get_recent_files(max_files
)
602 menu_recent_sub
= gtk
.Menu()
604 temp_playlist
= os
.path
.expanduser(settings
.temp_playlist
)
606 if len(self
.recent_files
) > 0:
607 for f
in self
.recent_files
:
608 # don't include the temporary playlist in the file list
609 if f
== temp_playlist
: continue
610 filename
, extension
= os
.path
.splitext(os
.path
.basename(f
))
611 menu_item
= gtk
.MenuItem( filename
.replace('_', ' '))
612 menu_item
.connect('activate', self
.on_recent_file_activate
, f
)
613 menu_recent_sub
.append(menu_item
)
615 menu_item
= gtk
.MenuItem(_('No recent files available.'))
616 menu_item
.set_sensitive(False)
617 menu_recent_sub
.append(menu_item
)
619 self
.menu_recent
.set_submenu(menu_recent_sub
)
621 def on_recent_file_activate(self
, widget
, filepath
):
622 self
.play_file(filepath
)
625 def lock_progress(self
):
626 return settings
.progress_locked
628 def show_about(self
, w
, win
):
629 dialog
= gtk
.AboutDialog()
630 dialog
.set_website(about_website
)
631 dialog
.set_website_label(about_website
)
632 dialog
.set_name(about_name
)
633 dialog
.set_authors(about_authors
)
634 dialog
.set_comments(about_text
)
635 dialog
.set_version(app_version
)
639 def destroy(self
, widget
):
643 def handle_headset_button(self
, event
, button
):
644 if event
== 'ButtonPressed' and button
== 'phone':
645 self
.on_btn_play_pause_clicked()
647 def check_queue(self
):
648 """ Makes sure the queue is saved if it has been modified
649 True means a new file can be opened
650 False means the user does not want to continue """
652 if player
.playlist
.queue_modified
:
654 self
.main_window
, _('Save queue to playlist file'),
655 _('Save Queue?'), _("The queue has been modified, "
656 "you will lose all additions if you don't save.") )
658 self
.__log
.debug('Response to "Save Queue?": %s', response
)
663 return self
.save_to_playlist_callback()
671 def open_file_callback(self
, widget
=None):
672 if self
.check_queue():
673 filename
= get_file_from_filechooser(self
.main_window
)
674 if filename
is not None:
675 self
._play
_file
(filename
)
677 def save_to_playlist_callback(self
, widget
=None):
678 filename
= get_file_from_filechooser(
679 self
.main_window
, save_file
=True, save_to
='playlist.m3u' )
684 if os
.path
.isfile(filename
):
686 self
.main_window
, _('Overwrite File Warning'),
687 _('Overwrite ') + '%s?' % os
.path
.basename(filename
),
688 _('All data in the file will be erased.') )
695 return self
.save_to_playlist_callback()
697 ext
= util
.detect_filetype(filename
)
698 if not player
.playlist
.save_to_new_playlist(filename
, ext
):
699 util
.notify(_('Error saving playlist...'))
704 def set_controls_sensitivity(self
, sensitive
):
705 self
.forward_button
.set_sensitive(sensitive
)
706 self
.rewind_button
.set_sensitive(sensitive
)
707 self
.fforward_button
.set_sensitive(sensitive
)
708 self
.rrewind_button
.set_sensitive(sensitive
)
710 def on_key_press(self
, widget
, event
):
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
.rewind_callback(self
.rewind_button
)
717 elif event
.keyval
== gtk
.keysyms
.Right
: # seek forward
718 self
.forward_callback(self
.forward_button
)
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
== util
.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
== util
.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
)
744 self
.volume_timer_id
= gobject
.timeout_add(
745 1000 * timeout
, self
.__volume
_hide
_callback
)
747 def __volume_hide_callback(self
):
748 self
.volume_timer_id
= None
752 def toggle_volumebar(self
, widget
=None):
753 if self
.volume_timer_id
is None:
754 self
.__set
_volume
_hide
_timer
(5)
756 self
.__volume
_hide
_callback
()
758 def volume_changed_gtk(self
, widget
, new_value
=0.5):
759 player
.volume_level
= new_value
761 def volume_changed_hildon(self
, widget
):
762 self
.__set
_volume
_hide
_timer
( 4, force_show
=True )
763 player
.volume_level
= widget
.get_level()/100.0
765 def mute_toggled(self
, widget
):
766 if widget
.get_mute():
767 player
.volume_level
= 0
769 player
.volume_level
= widget
.get_level()/100.0
771 def show_main_window(self
):
772 self
.main_window
.present()
774 def play_file(self
, filename
):
775 if self
.check_queue():
776 self
._play
_file
(filename
)
778 def _play_file(self
, filename
, pause_on_load
=False):
781 player
.playlist
.load( os
.path
.abspath(filename
) )
782 if player
.playlist
.is_empty
:
787 def on_player_stopped(self
):
788 self
.stop_progress_timer()
789 self
.title_label
.set_size_request(-1,-1)
790 self
.reset_progress()
791 self
.set_controls_sensitivity(False)
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)
798 def on_player_new_track(self
, metadata
):
799 image(self
.play_pause_button
, 'media-playback-start.png')
800 self
.play_pause_button
.disconnect(self
.button_handler_id
)
801 self
.button_handler_id
= self
.play_pause_button
.connect(
802 'clicked', self
.on_btn_play_pause_clicked
)
804 for widget
in [self
.title_label
,self
.artist_label
,self
.album_label
]:
808 self
.cover_art
.hide()
809 self
.has_coverart
= False
810 self
.set_metadata(metadata
)
812 text
, position
= player
.get_formatted_position()
813 estimated_length
= metadata
.get('length', 0)
814 self
.set_progress_callback( position
, estimated_length
)
816 def on_player_paused(self
):
817 self
.stop_progress_timer() # This should save some power
818 image(self
.play_pause_button
, 'media-playback-start.png')
820 def on_player_end_of_playlist(self
):
821 self
.play_pause_button
.disconnect(self
.button_handler_id
)
822 self
.button_handler_id
= self
.play_pause_button
.connect(
823 'clicked', self
.open_file_callback
)
824 image(self
.play_pause_button
, gtk
.STOCK_OPEN
, True)
826 def on_file_queued(self
, filepath
, success
, notify
):
828 filename
= os
.path
.basename(filepath
)
831 util
.notify( '%s added successfully.' % filename
))
834 util
.notify( 'Error adding %s to the queue.' % filename
))
836 def reset_progress(self
):
837 self
.progress
.set_fraction(0)
838 self
.set_progress_callback(0,0)
840 def set_progress_callback(self
, time_elapsed
, total_time
):
841 """ times must be in nanoseconds """
842 time_string
= "%s / %s" % ( util
.convert_ns(time_elapsed
),
843 util
.convert_ns(total_time
) )
844 self
.progress
.set_text( time_string
)
845 fraction
= float(time_elapsed
) / float(total_time
) if total_time
else 0
846 self
.progress
.set_fraction( fraction
)
848 def on_progressbar_changed(self
, widget
, event
):
849 if ( not self
.lock_progress
and
850 event
.type == gtk
.gdk
.BUTTON_PRESS
and event
.button
== 1 ):
851 new_fraction
= event
.x
/float(widget
.get_allocation().width
)
852 resp
= player
.do_seek(percent
=new_fraction
)
854 # Preemptively update the progressbar to make seeking smoother
855 self
.set_progress_callback( *resp
)
857 def on_btn_play_pause_clicked(self
, widget
=None):
858 player
.play_pause_toggle()
860 def progress_timer_callback( self
):
861 if player
.playing
and not player
.seeking
:
862 pos_int
, dur_int
= player
.get_position_duration()
863 # This prevents bogus values from being set while seeking
864 if ( pos_int
> 10**9 ) and ( dur_int
> 10**9 ):
865 self
.set_progress_callback( pos_int
, dur_int
)
868 def start_progress_timer( self
):
869 if self
.progress_timer_id
is not None:
870 self
.stop_progress_timer()
872 self
.progress_timer_id
= gobject
.timeout_add(
873 1000, self
.progress_timer_callback
)
875 def stop_progress_timer( self
):
876 if self
.progress_timer_id
is not None:
877 gobject
.source_remove( self
.progress_timer_id
)
878 self
.progress_timer_id
= None
880 def set_coverart( self
, pixbuf
):
881 self
.cover_art
.set_from_pixbuf(pixbuf
)
882 self
.cover_art
.show()
883 self
.has_coverart
= True
885 def set_metadata( self
, tag_message
):
886 tags
= { 'title': self
.title_label
, 'artist': self
.artist_label
,
887 'album': self
.album_label
}
889 if tag_message
.has_key('image') and tag_message
['image'] is not None:
890 value
= tag_message
['image']
892 pbl
= gtk
.gdk
.PixbufLoader()
896 pixbuf
= pbl
.get_pixbuf().scale_simple(
897 coverart_size
[0], coverart_size
[1], gtk
.gdk
.INTERP_BILINEAR
)
898 self
.set_coverart(pixbuf
)
900 self
.__log
.exception('Error setting coverart...')
902 tag_vals
= dict([ (i
,'') for i
in tags
.keys()])
903 for tag
,value
in tag_message
.iteritems():
904 if tags
.has_key(tag
) and value
is not None and value
.strip():
905 tags
[tag
].set_markup('<big>'+value
+'</big>')
906 tag_vals
[tag
] = value
907 tags
[tag
].set_alignment( 0.5*int(not self
.has_coverart
), 0.5)
910 if util
.platform
== util
.MAEMO
:
911 self
.main_window
.set_title(value
)
912 # oh man this is hacky :(
913 if self
.has_coverart
:
914 tags
[tag
].set_size_request(420,-1)
915 if len(value
) >= 80: value
= value
[:80] + '...'
917 self
.main_window
.set_title('Panucci - ' + value
)
919 tags
[tag
].set_markup('<b><big>'+value
+'</big></b>')
921 def seekbutton_callback( self
, widget
, seek_amount
):
922 resp
= player
.do_seek(from_current
=seek_amount
*10**9)
924 # Preemptively update the progressbar to make seeking smoother
925 self
.set_progress_callback( *resp
)
927 def pickle_file_conversion(self
):
928 pickle_file
= os
.path
.expanduser('~/.rmp-bookmarks')
929 if os
.path
.isfile(pickle_file
):
930 import pickle_converter
933 util
.notify( _('Converting old pickle format to SQLite.') ))
934 self
.__log
.info( util
.notify( _('This may take a while...') ))
936 if pickle_converter
.load_pickle_file(pickle_file
):
938 util
.notify( _('Pickle file converted successfully.') ))
940 self
.__log
.error( util
.notify(
941 _('Error converting pickle file, check your log...') ))
943 def run(filename
=None):
947 if __name__
== '__main__':
948 log
.error( 'WARNING: Use the "panucci" executable to run this program.' )
949 log
.error( 'Exiting...' )