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
= [240, 240] if util
.platform
== util
.MAEMO
else [130, 130]
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( toplevel_window
, save_file
=False, save_to
=None):
114 if util
.platform
== util
.MAEMO
:
116 args
= ( toplevel_window
, gtk
.FILE_CHOOSER_ACTION_SAVE
)
118 args
= ( toplevel_window
, gtk
.FILE_CHOOSER_ACTION_OPEN
)
120 dlg
= hildon
.FileChooserDialog( *args
)
123 args
= ( _('Select file to save playlist to'), None,
124 gtk
.FILE_CHOOSER_ACTION_SAVE
,
125 (( gtk
.STOCK_CANCEL
, gtk
.RESPONSE_REJECT
,
126 gtk
.STOCK_SAVE
, gtk
.RESPONSE_OK
)) )
128 args
= ( _('Select podcast or audiobook'), None,
129 gtk
.FILE_CHOOSER_ACTION_OPEN
,
130 (( gtk
.STOCK_CANCEL
, gtk
.RESPONSE_REJECT
,
131 gtk
.STOCK_MEDIA_PLAY
, gtk
.RESPONSE_OK
)) )
133 dlg
= gtk
.FileChooserDialog(*args
)
135 current_folder
= os
.path
.expanduser(settings
.last_folder
)
137 if current_folder
is not None and os
.path
.isdir(current_folder
):
138 dlg
.set_current_folder(current_folder
)
140 if save_file
and save_to
is not None:
141 dlg
.set_current_name(save_to
)
143 if dlg
.run() == gtk
.RESPONSE_OK
:
144 filename
= dlg
.get_filename()
145 settings
.last_folder
= dlg
.get_current_folder()
152 class BookmarksWindow(gtk
.Window
):
154 gtk
.Window
.__init
__(self
, gtk
.WINDOW_TOPLEVEL
)
155 self
.__log
= logging
.getLogger('panucci.panucci.BookmarksWindow')
157 self
.set_title('Bookmarks')
158 window_icon
= util
.find_image('panucci.png')
159 if window_icon
is not None:
160 self
.set_icon_from_file( window_icon
)
162 self
.set_default_size(400, 300)
163 self
.set_border_width(10)
164 self
.vbox
= gtk
.VBox()
165 self
.vbox
.set_spacing(5)
166 self
.treeview
= gtk
.TreeView()
167 self
.treeview
.set_enable_tree_lines(True)
168 self
.treeview
.set_headers_visible(True)
171 ncol
= gtk
.TreeViewColumn(_('Name'))
172 ncell
= gtk
.CellRendererText()
173 ncell
.set_property('editable', True)
174 ncell
.connect('edited', self
.label_edited
)
175 ncol
.pack_start(ncell
)
176 ncol
.add_attribute(ncell
, 'text', 1)
178 tcol
= gtk
.TreeViewColumn(_('Position'))
179 tcell
= gtk
.CellRendererText()
180 tcol
.pack_start(tcell
)
181 tcol
.add_attribute(tcell
, 'text', 2)
183 self
.treeview
.append_column(ncol
)
184 self
.treeview
.append_column(tcol
)
185 self
.treeview
.connect('drag-data-received', self
.drag_data_recieved
)
186 self
.treeview
.connect('drag_data_get', self
.drag_data_get_data
)
189 ( 'playlist_row_data', gtk
.TARGET_SAME_WIDGET
, 0 ) ]
191 self
.treeview
.enable_model_drag_source(
192 gtk
.gdk
.BUTTON1_MASK
, treeview_targets
, gtk
.gdk
.ACTION_COPY
)
194 self
.treeview
.enable_model_drag_dest(
195 treeview_targets
, gtk
.gdk
.ACTION_COPY
)
197 sw
= gtk
.ScrolledWindow()
198 sw
.set_policy(gtk
.POLICY_AUTOMATIC
, gtk
.POLICY_AUTOMATIC
)
199 sw
.set_shadow_type(gtk
.SHADOW_IN
)
200 sw
.add(self
.treeview
)
202 self
.hbox
= gtk
.HButtonBox()
203 self
.add_button
= gtk
.Button(gtk
.STOCK_ADD
)
204 self
.add_button
.set_use_stock(True)
205 self
.add_button
.connect('clicked', self
.add_bookmark
)
206 self
.hbox
.pack_start(self
.add_button
)
207 self
.remove_button
= gtk
.Button(gtk
.STOCK_REMOVE
)
208 self
.remove_button
.set_use_stock(True)
209 self
.remove_button
.connect('clicked', self
.remove_bookmark
)
210 self
.hbox
.pack_start(self
.remove_button
)
211 self
.jump_button
= gtk
.Button(gtk
.STOCK_JUMP_TO
)
212 self
.jump_button
.set_use_stock(True)
213 self
.jump_button
.connect('clicked', self
.jump_bookmark
)
214 self
.hbox
.pack_start(self
.jump_button
)
215 self
.close_button
= gtk
.Button(gtk
.STOCK_CLOSE
)
216 self
.close_button
.set_use_stock(True)
217 self
.close_button
.connect('clicked', self
.close
)
218 self
.hbox
.pack_start(self
.close_button
)
219 self
.vbox
.pack_start(self
.hbox
, False, True)
223 def drag_data_get_data(
224 self
, treeview
, context
, selection
, target_id
, timestamp
):
226 treeselection
= treeview
.get_selection()
227 model
, iter = treeselection
.get_selected()
228 # only allow moving around top-level parents
229 if model
.iter_parent(iter) is None:
230 # send the path of the selected row
231 data
= model
.get_string_from_iter(iter)
232 selection
.set(selection
.target
, 8, data
)
234 self
.__log
.debug("Can't move children...")
236 def drag_data_recieved(
237 self
, treeview
, context
, x
, y
, selection
, info
, timestamp
):
239 drop_info
= treeview
.get_dest_row_at_pos(x
, y
)
241 # TODO: If user drags the row past the last row, drop_info is None
242 # I'm not sure if it's safe to simply assume that None is
243 # euqivalent to the last row...
244 if None not in [ drop_info
and selection
.data
]:
245 model
= treeview
.get_model()
246 path
, position
= drop_info
248 from_iter
= model
.get_iter_from_string(selection
.data
)
250 # make sure the to_iter doesn't have a parent
251 to_iter
= model
.get_iter(path
)
252 if model
.iter_parent(to_iter
) is not None:
253 to_iter
= model
.iter_parent(to_iter
)
255 from_row
= model
.get_path(from_iter
)[0]
258 if ( position
== gtk
.TREE_VIEW_DROP_BEFORE
or
259 position
== gtk
.TREE_VIEW_DROP_INTO_OR_BEFORE
):
260 model
.move_before( from_iter
, to_iter
)
261 to_row
= to_row
- 1 if from_row
< to_row
else to_row
262 elif ( position
== gtk
.TREE_VIEW_DROP_AFTER
or
263 position
== gtk
.TREE_VIEW_DROP_INTO_OR_AFTER
):
264 model
.move_after( from_iter
, to_iter
)
265 to_row
= to_row
+ 1 if from_row
> to_row
else to_row
267 self
.__log
.debug('Drop not supported: %s', position
)
269 # don't do anything if we're not actually moving rows around
270 if from_row
!= to_row
:
271 player
.playlist
.move_item( from_row
, to_row
)
274 self
.__log
.debug('No drop_data or selection.data available')
276 def update_model(self
):
277 self
.model
= player
.playlist
.get_bookmark_model()
278 self
.treeview
.set_model(self
.model
)
281 player
.playlist
.update_bookmarks()
284 def label_edited(self
, cellrenderer
, path
, new_text
):
285 iter = self
.model
.get_iter(path
)
286 old_text
= self
.model
.get_value(iter, 1)
289 if old_text
!= new_text
:
290 self
.model
.set_value(iter, 1, new_text
)
291 m
, bkmk_id
, biter
, item_id
, iiter
= self
.__cur
_selection
()
293 player
.playlist
.update_bookmark(
294 item_id
, bkmk_id
, name
=new_text
)
296 self
.model
.set_value(iter, 1, old_text
)
298 def add_bookmark(self
, w
=None, lbl
=None, pos
=None):
299 (label
, position
) = player
.get_formatted_position(pos
)
300 label
= label
if lbl
is None else lbl
301 position
= position
if pos
is None else pos
302 player
.playlist
.save_bookmark( label
, position
)
305 def __cur_selection(self
):
306 bookmark_id
, bookmark_iter
, item_id
, item_iter
= (None,)*4
308 selection
= self
.treeview
.get_selection()
309 # Assume the user selects a bookmark.
310 # bookmark_iter will get set to None if that is not the case...
311 model
, bookmark_iter
= selection
.get_selected()
313 if bookmark_iter
is not None:
314 item_iter
= model
.iter_parent(bookmark_iter
)
316 # bookmark_iter is actually an item_iter
317 if item_iter
is None:
318 item_iter
= bookmark_iter
319 item_id
= model
.get_value(item_iter
, 0)
320 bookmark_id
, bookmark_iter
= None, None
322 bookmark_id
= model
.get_value(bookmark_iter
, 0)
323 item_id
= model
.get_value(item_iter
, 0)
325 return model
, bookmark_id
, bookmark_iter
, item_id
, item_iter
327 def remove_bookmark(self
, w
):
328 model
, bkmk_id
, bkmk_iter
, item_id
, item_iter
= self
.__cur
_selection
()
329 player
.playlist
.remove_bookmark( item_id
, bkmk_id
)
330 if bkmk_iter
is not None:
331 model
.remove(bkmk_iter
)
332 elif item_iter
is not None:
333 model
.remove(item_iter
)
335 def jump_bookmark(self
, w
):
336 model
, bkmk_id
, bkmk_iter
, item_id
, item_iter
= self
.__cur
_selection
()
337 if item_iter
is not None:
339 player
.playlist
.load_from_bookmark_id( item_id
, bkmk_id
)
342 class GTK_Main(object):
344 def __init__(self
, filename
=None):
345 self
.__log
= logging
.getLogger('panucci.panucci.GTK_Main')
346 interface
.register_gui(self
)
347 self
.pickle_file_conversion()
349 self
.recent_files
= []
350 self
.progress_timer_id
= None
351 self
.volume_timer_id
= None
352 self
.make_main_window()
353 self
.has_coverart
= False
354 self
.set_volume(settings
.volume
)
356 if util
.platform
==util
.MAEMO
and interface
.headset_device
is not None:
357 # Enable play/pause with headset button
358 interface
.headset_device
.connect_to_signal(
359 'Condition', self
.handle_headset_button
)
361 player
.register( 'stopped', self
.on_player_stopped
)
362 player
.register( 'playing', self
.on_player_playing
)
363 player
.register( 'paused', self
.on_player_paused
)
364 player
.register( 'end_of_playlist', self
.on_player_end_of_playlist
)
365 player
.playlist
.register( 'new_track', self
.on_player_new_track
)
366 player
.playlist
.register( 'file_queued', self
.on_file_queued
)
369 def make_main_window(self
):
372 if util
.platform
== util
.MAEMO
:
373 self
.app
= hildon
.Program()
374 window
= hildon
.Window()
375 self
.app
.add_window(window
)
377 window
= gtk
.Window(gtk
.WINDOW_TOPLEVEL
)
379 window
.set_title('Panucci')
380 self
.window_icon
= util
.find_image('panucci.png')
381 if self
.window_icon
is not None:
382 window
.set_icon_from_file( self
.window_icon
)
383 window
.set_default_size(400, -1)
384 window
.set_border_width(0)
385 window
.connect("destroy", self
.destroy
)
386 self
.main_window
= window
388 if util
.platform
== util
.MAEMO
:
389 window
.set_menu(self
.create_menu())
391 menu_vbox
= gtk
.VBox()
392 menu_vbox
.set_spacing(0)
393 window
.add(menu_vbox
)
394 menu_bar
= gtk
.MenuBar()
395 root_menu
= gtk
.MenuItem('Panucci')
396 root_menu
.set_submenu(self
.create_menu())
397 menu_bar
.append(root_menu
)
398 menu_vbox
.pack_start(menu_bar
, False, False, 0)
401 main_hbox
= gtk
.HBox()
402 main_hbox
.set_spacing(6)
403 if util
.platform
== util
.MAEMO
:
404 window
.add(main_hbox
)
406 menu_vbox
.pack_end(main_hbox
, True, True, 6)
408 main_vbox
= gtk
.VBox()
409 main_vbox
.set_spacing(6)
410 # add a vbox to the main_hbox
411 main_hbox
.pack_start(main_vbox
, True, True)
413 # a hbox to hold the cover art and metadata vbox
414 metadata_hbox
= gtk
.HBox()
415 metadata_hbox
.set_spacing(6)
416 main_vbox
.pack_start(metadata_hbox
, True, False)
418 self
.cover_art
= gtk
.Image()
419 metadata_hbox
.pack_start( self
.cover_art
, False, False )
421 # vbox to hold metadata
422 metadata_vbox
= gtk
.VBox()
423 metadata_vbox
.set_spacing(8)
424 empty_label
= gtk
.Label()
425 metadata_vbox
.pack_start(empty_label
, True, True)
426 self
.artist_label
= gtk
.Label('')
427 self
.artist_label
.set_ellipsize(pango
.ELLIPSIZE_END
)
428 metadata_vbox
.pack_start(self
.artist_label
, False, False)
429 self
.album_label
= gtk
.Label('')
430 self
.album_label
.set_ellipsize(pango
.ELLIPSIZE_END
)
431 metadata_vbox
.pack_start(self
.album_label
, False, False)
432 self
.title_label
= gtk
.Label('')
433 self
.title_label
.set_line_wrap(True)
434 metadata_vbox
.pack_start(self
.title_label
, False, False)
435 empty_label
= gtk
.Label()
436 metadata_vbox
.pack_start(empty_label
, True, True)
437 metadata_hbox
.pack_start( metadata_vbox
, True, True )
439 progress_eventbox
= gtk
.EventBox()
440 progress_eventbox
.set_events(gtk
.gdk
.BUTTON_PRESS_MASK
)
441 progress_eventbox
.connect('button-press-event', self
.on_progressbar_changed
)
442 self
.progress
= gtk
.ProgressBar()
443 # make the progress bar more "finger-friendly"
444 if util
.platform
== util
.MAEMO
:
445 self
.progress
.set_size_request( -1, 50 )
446 progress_eventbox
.add(self
.progress
)
447 main_vbox
.pack_start( progress_eventbox
, False, False )
449 # make the button box
450 buttonbox
= gtk
.HBox()
451 self
.rrewind_button
= gtk
.Button('')
452 image(self
.rrewind_button
, 'media-skip-backward.png')
453 self
.rrewind_button
.connect('clicked', self
.seekbutton_callback
, -1*long_seek
)
454 buttonbox
.add(self
.rrewind_button
)
455 self
.rewind_button
= gtk
.Button('')
456 image(self
.rewind_button
, 'media-seek-backward.png')
457 self
.rewind_button
.connect('clicked', self
.seekbutton_callback
, -1*short_seek
)
458 buttonbox
.add(self
.rewind_button
)
459 self
.play_pause_button
= gtk
.Button('')
460 image(self
.play_pause_button
, gtk
.STOCK_OPEN
, True)
461 self
.button_handler_id
= self
.play_pause_button
.connect(
462 'clicked', self
.open_file_callback
)
463 buttonbox
.add(self
.play_pause_button
)
464 self
.forward_button
= gtk
.Button('')
465 image(self
.forward_button
, 'media-seek-forward.png')
466 self
.forward_button
.connect('clicked', self
.seekbutton_callback
, short_seek
)
467 buttonbox
.add(self
.forward_button
)
468 self
.fforward_button
= gtk
.Button('')
469 image(self
.fforward_button
, 'media-skip-forward.png')
470 self
.fforward_button
.connect('clicked', self
.seekbutton_callback
, long_seek
)
471 buttonbox
.add(self
.fforward_button
)
472 self
.bookmarks_button
= gtk
.Button('')
473 image(self
.bookmarks_button
, 'bookmark-new.png')
474 self
.bookmarks_button
.connect('clicked', self
.bookmarks_callback
)
475 buttonbox
.add(self
.bookmarks_button
)
476 self
.set_controls_sensitivity(False)
477 main_vbox
.pack_start(buttonbox
, False, False)
481 if util
.platform
== util
.MAEMO
:
482 self
.volume
= hildon
.VVolumebar()
483 self
.volume
.set_property('can-focus', False)
484 self
.volume
.connect('level_changed', self
.volume_changed_hildon
)
485 self
.volume
.connect('mute_toggled', self
.mute_toggled
)
486 window
.connect('key-press-event', self
.on_key_press
)
487 main_hbox
.pack_start(self
.volume
, False, True)
489 # Add a button to pop out the volume bar
490 self
.volume_button
= gtk
.ToggleButton('')
491 image(self
.volume_button
, 'media-speaker.png')
492 self
.volume_button
.connect('clicked', self
.toggle_volumebar
)
494 'show', lambda x
: self
.volume_button
.set_active(True))
496 'hide', lambda x
: self
.volume_button
.set_active(False))
497 buttonbox
.add(self
.volume_button
)
498 self
.volume_button
.show()
500 # Disable focus for all widgets, so we can use the cursor
501 # keys + enter to directly control our media player, which
502 # is handled by "key-press-event"
504 self
.rrewind_button
, self
.rewind_button
,
505 self
.play_pause_button
, self
.forward_button
,
506 self
.fforward_button
, self
.progress
,
507 self
.bookmarks_button
, self
.volume_button
, ):
508 w
.unset_flags(gtk
.CAN_FOCUS
)
510 self
.volume
= gtk
.VolumeButton()
511 self
.volume
.connect('value-changed', self
.volume_changed_gtk
)
512 buttonbox
.add(self
.volume
)
515 def create_menu(self
):
519 menu_open
= gtk
.ImageMenuItem(gtk
.STOCK_OPEN
)
520 menu_open
.connect("activate", self
.open_file_callback
)
521 menu
.append(menu_open
)
523 menu_queue
= gtk
.MenuItem(_('Add file to the queue'))
524 menu_queue
.connect('activate', self
.queue_file_callback
)
525 menu
.append(menu_queue
)
527 # the recent files menu
528 self
.menu_recent
= gtk
.MenuItem(_('Recent Files'))
529 menu
.append(self
.menu_recent
)
530 self
.create_recent_files_menu()
532 menu
.append(gtk
.SeparatorMenuItem())
534 menu_bookmarks
= gtk
.MenuItem(_('Bookmarks'))
535 menu_bookmarks
.connect('activate', self
.bookmarks_callback
)
536 menu
.append(menu_bookmarks
)
539 # the settings sub-menu
540 menu_settings
= gtk
.MenuItem(_('Settings'))
541 menu
.append(menu_settings
)
543 menu_settings_sub
= gtk
.Menu()
544 menu_settings
.set_submenu(menu_settings_sub
)
546 menu_settings_lock_progress
= gtk
.CheckMenuItem(_('Lock Progress Bar'))
547 menu_settings_lock_progress
.connect('toggled', lambda w
:
548 setattr( settings
, 'progress_locked', w
.get_active()))
549 menu_settings_lock_progress
.set_active(self
.lock_progress
)
550 menu_settings_sub
.append(menu_settings_lock_progress
)
552 menu
.append(gtk
.SeparatorMenuItem())
554 # the donate sub-menu
555 menu_donate
= gtk
.MenuItem(_('Donate'))
556 menu
.append(menu_donate
)
558 menu_donate_sub
= gtk
.Menu()
559 menu_donate
.set_submenu(menu_donate_sub
)
561 menu_donate_device
= gtk
.MenuItem(_('Developer device'))
562 menu_donate_device
.connect("activate", lambda w
: webbrowser
.open_new(donate_device_url
))
563 menu_donate_sub
.append(menu_donate_device
)
565 menu_donate_wishlist
= gtk
.MenuItem(_('Amazon Wishlist'))
566 menu_donate_wishlist
.connect("activate", lambda w
: webbrowser
.open_new(donate_wishlist_url
))
567 menu_donate_sub
.append(menu_donate_wishlist
)
569 menu_about
= gtk
.ImageMenuItem(gtk
.STOCK_ABOUT
)
570 menu_about
.connect("activate", self
.show_about
, self
.main_window
)
571 menu
.append(menu_about
)
573 menu
.append(gtk
.SeparatorMenuItem())
575 menu_quit
= gtk
.ImageMenuItem(gtk
.STOCK_QUIT
)
576 menu_quit
.connect("activate", self
.destroy
)
577 menu
.append(menu_quit
)
581 def create_recent_files_menu( self
):
582 max_files
= settings
.max_recent_files
583 self
.recent_files
= player
.playlist
.get_recent_files(max_files
)
584 menu_recent_sub
= gtk
.Menu()
586 temp_playlist
= os
.path
.expanduser(settings
.temp_playlist
)
588 if len(self
.recent_files
) > 0:
589 for f
in self
.recent_files
:
590 # don't include the temporary playlist in the file list
591 if f
== temp_playlist
: continue
592 filename
, extension
= os
.path
.splitext(os
.path
.basename(f
))
593 menu_item
= gtk
.MenuItem( filename
.replace('_', ' '))
594 menu_item
.connect('activate', self
.on_recent_file_activate
, f
)
595 menu_recent_sub
.append(menu_item
)
597 menu_item
= gtk
.MenuItem(_('No recent files available.'))
598 menu_item
.set_sensitive(False)
599 menu_recent_sub
.append(menu_item
)
601 self
.menu_recent
.set_submenu(menu_recent_sub
)
603 def on_recent_file_activate(self
, widget
, filepath
):
604 self
.play_file(filepath
)
607 def lock_progress(self
):
608 return settings
.progress_locked
610 def show_about(self
, w
, win
):
611 dialog
= gtk
.AboutDialog()
612 dialog
.set_website(about_website
)
613 dialog
.set_website_label(about_website
)
614 dialog
.set_name(about_name
)
615 dialog
.set_authors(about_authors
)
616 dialog
.set_comments(about_text
)
617 dialog
.set_version(app_version
)
621 def destroy(self
, widget
):
625 def handle_headset_button(self
, event
, button
):
626 if event
== 'ButtonPressed' and button
== 'phone':
627 self
.on_btn_play_pause_clicked()
629 def queue_file_callback(self
, widget
=None):
630 filename
= get_file_from_filechooser(self
.main_window
)
631 if filename
is not None:
632 player
.playlist
.append(filename
)
634 def check_queue(self
):
635 """ Makes sure the queue is saved if it has been modified
636 True means a new file can be opened
637 False means the user does not want to continue """
639 if player
.playlist
.queue_modified
:
641 self
.main_window
, _('Save queue to playlist file'),
642 _('Save Queue?'), _("The queue has been modified, "
643 "you will lose all additions if you don't save.") )
645 self
.__log
.debug('Response to "Save Queue?": %s', response
)
650 return self
.save_to_playlist_callback()
658 def open_file_callback(self
, widget
=None):
659 if self
.check_queue():
660 filename
= get_file_from_filechooser(self
.main_window
)
661 if filename
is not None:
662 self
._play
_file
(filename
)
664 def save_to_playlist_callback(self
, widget
=None):
665 filename
= get_file_from_filechooser(
666 self
.main_window
, save_file
=True, save_to
='playlist.m3u' )
671 if os
.path
.isfile(filename
):
673 self
.main_window
, _('Overwrite File Warning'),
674 _('Overwrite ') + '%s?' % os
.path
.basename(filename
),
675 _('All data in the file will be erased.') )
682 return self
.save_to_playlist_callback()
684 ext
= util
.detect_filetype(filename
)
685 if not player
.playlist
.save_to_new_playlist(filename
, ext
):
686 util
.notify(_('Error saving playlist...'))
691 def set_controls_sensitivity(self
, sensitive
):
692 self
.forward_button
.set_sensitive(sensitive
)
693 self
.rewind_button
.set_sensitive(sensitive
)
694 self
.fforward_button
.set_sensitive(sensitive
)
695 self
.rrewind_button
.set_sensitive(sensitive
)
697 def on_key_press(self
, widget
, event
):
698 if event
.keyval
== gtk
.keysyms
.F7
: #plus
699 self
.set_volume( min( 1, self
.get_volume() + 0.10 ))
700 elif event
.keyval
== gtk
.keysyms
.F8
: #minus
701 self
.set_volume( max( 0, self
.get_volume() - 0.10 ))
702 elif event
.keyval
== gtk
.keysyms
.Left
: # seek back
703 self
.rewind_callback(self
.rewind_button
)
704 elif event
.keyval
== gtk
.keysyms
.Right
: # seek forward
705 self
.forward_callback(self
.forward_button
)
706 elif event
.keyval
== gtk
.keysyms
.Return
: # play/pause
707 self
.on_btn_play_pause_clicked()
709 # The following two functions get and set the
710 # volume from the volume control widgets.
711 def get_volume(self
):
712 if util
.platform
== util
.MAEMO
:
713 return self
.volume
.get_level()/100.0
715 return self
.volume
.get_value()
717 def set_volume(self
, vol
):
718 """ vol is a float from 0 to 1 """
720 if util
.platform
== util
.MAEMO
:
721 self
.volume
.set_level(vol
*100.0)
723 self
.volume
.set_value(vol
)
725 def __set_volume_hide_timer(self
, timeout
, force_show
=False):
726 if force_show
or self
.volume_button
.get_active():
728 if self
.volume_timer_id
is not None:
729 gobject
.source_remove(self
.volume_timer_id
)
731 self
.volume_timer_id
= gobject
.timeout_add(
732 1000 * timeout
, self
.__volume
_hide
_callback
)
734 def __volume_hide_callback(self
):
735 self
.volume_timer_id
= None
739 def toggle_volumebar(self
, widget
=None):
740 if self
.volume_timer_id
is None:
741 self
.__set
_volume
_hide
_timer
(5)
743 self
.__volume
_hide
_callback
()
745 def volume_changed_gtk(self
, widget
, new_value
=0.5):
746 player
.volume_level
= new_value
748 def volume_changed_hildon(self
, widget
):
749 self
.__set
_volume
_hide
_timer
( 4, force_show
=True )
750 player
.volume_level
= widget
.get_level()/100.0
752 def mute_toggled(self
, widget
):
753 if widget
.get_mute():
754 player
.volume_level
= 0
756 player
.volume_level
= widget
.get_level()/100.0
758 def show_main_window(self
):
759 self
.main_window
.present()
761 def play_file(self
, filename
):
762 if self
.check_queue():
763 self
._play
_file
(filename
)
765 def _play_file(self
, filename
, pause_on_load
=False):
768 player
.playlist
.load( os
.path
.abspath(filename
) )
769 if player
.playlist
.is_empty
:
774 def on_player_stopped(self
):
775 self
.stop_progress_timer()
776 self
.title_label
.set_size_request(-1,-1)
777 self
.reset_progress()
778 self
.set_controls_sensitivity(False)
780 def on_player_playing(self
):
781 self
.start_progress_timer()
782 image(self
.play_pause_button
, 'media-playback-pause.png')
784 def on_player_new_track(self
, metadata
):
785 image(self
.play_pause_button
, 'media-playback-start.png')
786 self
.play_pause_button
.disconnect(self
.button_handler_id
)
787 self
.button_handler_id
= self
.play_pause_button
.connect(
788 'clicked', self
.on_btn_play_pause_clicked
)
790 self
.set_controls_sensitivity(True)
791 for widget
in [self
.title_label
,self
.artist_label
,self
.album_label
]:
795 self
.cover_art
.hide()
796 self
.has_coverart
= False
797 self
.set_metadata(metadata
)
799 text
, position
= player
.get_formatted_position()
800 estimated_length
= metadata
['length']
801 self
.set_progress_callback( position
, estimated_length
)
803 def on_player_paused(self
):
804 self
.stop_progress_timer() # This should save some power
805 image(self
.play_pause_button
, 'media-playback-start.png')
807 def on_player_end_of_playlist(self
):
808 self
.play_pause_button
.disconnect(self
.button_handler_id
)
809 self
.button_handler_id
= self
.play_pause_button
.connect(
810 'clicked', self
.open_file_callback
)
811 image(self
.play_pause_button
, gtk
.STOCK_OPEN
, True)
813 def on_file_queued(self
, filepath
, success
):
814 filename
= os
.path
.basename(filepath
)
816 self
.__log
.info(util
.notify('%s added successfully.' % filename
))
819 util
.notify('Error adding %s to the queue.' % filename
) )
821 def reset_progress(self
):
822 self
.progress
.set_fraction(0)
823 self
.set_progress_callback(0,0)
825 def set_progress_callback(self
, time_elapsed
, total_time
):
826 """ times must be in nanoseconds """
827 time_string
= "%s / %s" % ( util
.convert_ns(time_elapsed
),
828 util
.convert_ns(total_time
) )
829 self
.progress
.set_text( time_string
)
830 fraction
= float(time_elapsed
) / float(total_time
) if total_time
else 0
831 self
.progress
.set_fraction( fraction
)
833 def on_progressbar_changed(self
, widget
, event
):
834 if ( not self
.lock_progress
and
835 event
.type == gtk
.gdk
.BUTTON_PRESS
and event
.button
== 1 ):
836 new_fraction
= event
.x
/float(widget
.get_allocation().width
)
837 resp
= player
.do_seek(percent
=new_fraction
)
839 # Preemptively update the progressbar to make seeking smoother
840 self
.set_progress_callback( *resp
)
842 def on_btn_play_pause_clicked(self
, widget
=None):
843 player
.play_pause_toggle()
845 def progress_timer_callback( self
):
846 if player
.playing
and not player
.seeking
:
847 pos_int
, dur_int
= player
.get_position_duration()
848 # This prevents bogus values from being set while seeking
849 if ( pos_int
> 10**9 ) and ( dur_int
> 10**9 ):
850 self
.set_progress_callback( pos_int
, dur_int
)
853 def start_progress_timer( self
):
854 if self
.progress_timer_id
is not None:
855 self
.stop_progress_timer()
857 self
.progress_timer_id
= gobject
.timeout_add(
858 1000, self
.progress_timer_callback
)
860 def stop_progress_timer( self
):
861 if self
.progress_timer_id
is not None:
862 gobject
.source_remove( self
.progress_timer_id
)
863 self
.progress_timer_id
= None
865 def set_coverart( self
, pixbuf
):
866 self
.cover_art
.set_from_pixbuf(pixbuf
)
867 self
.cover_art
.show()
868 self
.has_coverart
= True
870 def set_metadata( self
, tag_message
):
871 tags
= { 'title': self
.title_label
, 'artist': self
.artist_label
,
872 'album': self
.album_label
}
874 if tag_message
.has_key('image') and tag_message
['image'] is not None:
875 value
= tag_message
['image']
877 pbl
= gtk
.gdk
.PixbufLoader()
881 pixbuf
= pbl
.get_pixbuf().scale_simple(
882 coverart_size
[0], coverart_size
[1], gtk
.gdk
.INTERP_BILINEAR
)
883 self
.set_coverart(pixbuf
)
885 self
.__log
.exception('Error setting coverart...')
887 tag_vals
= dict([ (i
,'') for i
in tags
.keys()])
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 tag_vals
[tag
] = value
892 tags
[tag
].set_alignment( 0.5*int(not self
.has_coverart
), 0.5)
895 if util
.platform
== util
.MAEMO
:
896 self
.main_window
.set_title(value
)
897 # oh man this is hacky :(
898 if self
.has_coverart
:
899 tags
[tag
].set_size_request(420,-1)
900 if len(value
) >= 80: value
= value
[:80] + '...'
902 self
.main_window
.set_title('Panucci - ' + value
)
904 tags
[tag
].set_markup('<b><big>'+value
+'</big></b>')
906 def seekbutton_callback( self
, widget
, seek_amount
):
907 resp
= player
.do_seek(from_current
=seek_amount
*10**9)
909 # Preemptively update the progressbar to make seeking smoother
910 self
.set_progress_callback( *resp
)
912 def bookmarks_callback(self
, w
):
915 def pickle_file_conversion(self
):
916 pickle_file
= os
.path
.expanduser('~/.rmp-bookmarks')
917 if os
.path
.isfile(pickle_file
):
918 import pickle_converter
921 util
.notify( _('Converting old pickle format to SQLite.') ))
922 self
.__log
.info( util
.notify( _('This may take a while...') ))
924 if pickle_converter
.load_pickle_file(pickle_file
):
926 util
.notify( _('Pickle file converted successfully.') ))
928 self
.__log
.error( util
.notify(
929 _('Error converting pickle file, check your log...') ))
931 def run(filename
=None):
935 if __name__
== '__main__':
936 log
.error( 'WARNING: Use the "panucci" executable to run this program.' )
937 log
.error( 'Exiting...' )