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)
35 # At the moment, we don't have gettext support, so
36 # make a dummy "_" function to passthrough the string
39 log
= logging
.getLogger('panucci.panucci')
46 if util
.platform
== util
.MAEMO
:
47 log
.critical( 'Using GTK widgets, install "python2.5-hildon" '
48 'for this to work properly.' )
50 from simplegconf
import gconf
51 from settings
import settings
52 from player
import player
53 from dbusinterface
import interface
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/'
60 donate_wishlist_url
= 'http://www.amazon.de/gp/registry/2PD2MYGHE6857'
61 donate_device_url
= 'http://maemo.gpodder.org/donate.html'
63 coverart_names
= [ 'cover', 'cover.jpg', 'cover.png' ]
64 coverart_size
= [200, 200] if util
.platform
== util
.MAEMO
else [110, 110]
66 gtk
.about_dialog_set_url_hook(util
.open_link
, None)
67 gtk
.icon_size_register('panucci-button', 32, 32)
69 def image(widget
, filename
, is_stock
=False):
70 widget
.remove(widget
.get_child())
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
)
81 if util
.platform
== util
.MAEMO
:
82 image
.set_padding(20, 20)
84 image
.set_padding(5, 5)
88 def dialog( toplevel_window
, title
, question
, description
,
89 affirmative_button
=gtk
.STOCK_YES
, negative_button
=gtk
.STOCK_NO
,
90 abortion_button
=gtk
.STOCK_CANCEL
):
92 """Present the user with a yes/no/cancel dialog.
93 The return value is either True, False or None, depending on which
94 button has been pressed in the dialog:
96 affirmative button (default: Yes) => True
97 negative button (defaut: No) => False
98 abortion button (default: Cancel) => None
100 When the dialog is closed with the "X" button in the window manager
101 decoration, the return value is always None (same as abortion button).
103 You can set any of the affirmative_button, negative_button or
104 abortion_button values to "None" to hide the corresponding action.
106 dlg
= gtk
.MessageDialog( toplevel_window
, gtk
.DIALOG_MODAL
,
107 gtk
.MESSAGE_QUESTION
, message_format
=question
)
111 if abortion_button
is not None:
112 dlg
.add_button(abortion_button
, gtk
.RESPONSE_CANCEL
)
113 if negative_button
is not None:
114 dlg
.add_button(negative_button
, gtk
.RESPONSE_NO
)
115 if affirmative_button
is not None:
116 dlg
.add_button(affirmative_button
, gtk
.RESPONSE_YES
)
118 dlg
.format_secondary_text(description
)
123 if response
== gtk
.RESPONSE_YES
:
125 elif response
== gtk
.RESPONSE_NO
:
127 elif response
in [gtk
.RESPONSE_CANCEL
, gtk
.RESPONSE_DELETE_EVENT
]:
130 def get_file_from_filechooser(
131 toplevel_window
, folder
=False, save_file
=False, save_to
=None):
134 open_action
= gtk
.FILE_CHOOSER_ACTION_SELECT_FOLDER
136 open_action
= gtk
.FILE_CHOOSER_ACTION_OPEN
138 if util
.platform
== util
.MAEMO
:
140 args
= ( toplevel_window
, gtk
.FILE_CHOOSER_ACTION_SAVE
)
142 args
= ( toplevel_window
, open_action
)
144 dlg
= hildon
.FileChooserDialog( *args
)
147 args
= ( _('Select file to save playlist to'), None,
148 gtk
.FILE_CHOOSER_ACTION_SAVE
,
149 (( gtk
.STOCK_CANCEL
, gtk
.RESPONSE_REJECT
,
150 gtk
.STOCK_SAVE
, gtk
.RESPONSE_OK
)) )
152 args
= ( _('Select podcast or audiobook'), None, open_action
,
153 (( gtk
.STOCK_CANCEL
, gtk
.RESPONSE_REJECT
,
154 gtk
.STOCK_OPEN
, gtk
.RESPONSE_OK
)) )
156 dlg
= gtk
.FileChooserDialog(*args
)
158 current_folder
= os
.path
.expanduser(settings
.last_folder
)
160 if current_folder
is not None and os
.path
.isdir(current_folder
):
161 dlg
.set_current_folder(current_folder
)
163 if save_file
and save_to
is not None:
164 dlg
.set_current_name(save_to
)
166 if dlg
.run() == gtk
.RESPONSE_OK
:
167 filename
= dlg
.get_filename()
168 settings
.last_folder
= dlg
.get_current_folder()
175 def set_stock_button_text( button
, text
):
176 alignment
= button
.get_child()
177 hbox
= alignment
.get_child()
178 image
, label
= hbox
.get_children()
181 class PlaylistTab(gtk
.VBox
):
182 def __init__(self
, main_window
):
183 gtk
.VBox
.__init
__(self
)
184 self
.__log
= logging
.getLogger('panucci.panucci.BookmarksWindow')
185 self
.main
= main_window
188 self
.treeview
= gtk
.TreeView()
189 self
.treeview
.set_headers_visible(True)
190 tree_selection
= self
.treeview
.get_selection()
191 tree_selection
.set_mode(gtk
.SELECTION_MULTIPLE
)
192 tree_selection
.connect('changed', self
.tree_selection_changed
)
194 # The tree lines look nasty on maemo
195 if util
.platform
== util
.LINUX
:
196 self
.treeview
.set_enable_tree_lines(True)
199 ncol
= gtk
.TreeViewColumn(_('Name'))
200 ncell
= gtk
.CellRendererText()
201 ncell
.set_property('ellipsize', pango
.ELLIPSIZE_END
)
202 ncell
.set_property('editable', True)
203 ncell
.connect('edited', self
.label_edited
)
204 ncol
.set_expand(True)
205 ncol
.pack_start(ncell
)
206 ncol
.add_attribute(ncell
, 'text', 1)
208 tcol
= gtk
.TreeViewColumn(_('Position'))
209 tcell
= gtk
.CellRendererText()
210 tcol
.pack_start(tcell
)
211 tcol
.add_attribute(tcell
, 'text', 2)
213 self
.treeview
.append_column(ncol
)
214 self
.treeview
.append_column(tcol
)
215 self
.treeview
.connect('drag-data-received', self
.drag_data_recieved
)
216 self
.treeview
.connect('drag_data_get', self
.drag_data_get_data
)
219 ( 'playlist_row_data', gtk
.TARGET_SAME_WIDGET
, 0 ) ]
221 self
.treeview
.enable_model_drag_source(
222 gtk
.gdk
.BUTTON1_MASK
, treeview_targets
, gtk
.gdk
.ACTION_COPY
)
224 self
.treeview
.enable_model_drag_dest(
225 treeview_targets
, gtk
.gdk
.ACTION_COPY
)
227 sw
= gtk
.ScrolledWindow()
228 sw
.set_policy(gtk
.POLICY_AUTOMATIC
, gtk
.POLICY_AUTOMATIC
)
229 sw
.set_shadow_type(gtk
.SHADOW_IN
)
230 sw
.add(self
.treeview
)
233 self
.hbox
= gtk
.HBox()
235 self
.add_button
= gtk
.Button(gtk
.STOCK_NEW
)
236 self
.add_button
.set_use_stock(True)
237 set_stock_button_text( self
.add_button
, _('Add File') )
238 self
.add_button
.connect('clicked', self
.add_file
)
239 self
.hbox
.pack_start(self
.add_button
, True, True)
241 self
.dir_button
= gtk
.Button(gtk
.STOCK_OPEN
)
242 self
.dir_button
.set_use_stock(True)
243 set_stock_button_text( self
.dir_button
, _('Add Directory') )
244 self
.dir_button
.connect('clicked', self
.add_directory
)
245 self
.hbox
.pack_start(self
.dir_button
, True, True)
247 self
.remove_button
= gtk
.Button(gtk
.STOCK_REMOVE
)
248 self
.remove_button
.set_use_stock(True)
249 self
.remove_button
.connect('clicked', self
.remove_bookmark
)
250 self
.hbox
.pack_start(self
.remove_button
, True, True)
252 self
.jump_button
= gtk
.Button(gtk
.STOCK_JUMP_TO
)
253 self
.jump_button
.set_use_stock(True)
254 self
.jump_button
.connect('clicked', self
.jump_bookmark
)
255 self
.hbox
.pack_start(self
.jump_button
, True, True)
256 self
.pack_start(self
.hbox
, False, True)
258 player
.playlist
.register(
259 'file_queued', lambda x
,y
,z
: self
.update_model() )
263 def tree_selection_changed(self
, treeselection
):
264 count
= treeselection
.count_selected_rows()
265 self
.remove_button
.set_sensitive(count
> 0)
266 self
.jump_button
.set_sensitive(count
== 1)
268 def drag_data_get_data(
269 self
, treeview
, context
, selection
, target_id
, timestamp
):
271 treeselection
= treeview
.get_selection()
272 model
, iter = treeselection
.get_selected()
273 # only allow moving around top-level parents
274 if model
.iter_parent(iter) is None:
275 # send the path of the selected row
276 data
= model
.get_string_from_iter(iter)
277 selection
.set(selection
.target
, 8, data
)
279 self
.__log
.debug("Can't move children...")
281 def drag_data_recieved(
282 self
, treeview
, context
, x
, y
, selection
, info
, timestamp
):
284 drop_info
= treeview
.get_dest_row_at_pos(x
, y
)
286 # TODO: If user drags the row past the last row, drop_info is None
287 # I'm not sure if it's safe to simply assume that None is
288 # euqivalent to the last row...
289 if None not in [ drop_info
and selection
.data
]:
290 model
= treeview
.get_model()
291 path
, position
= drop_info
293 from_iter
= model
.get_iter_from_string(selection
.data
)
295 # make sure the to_iter doesn't have a parent
296 to_iter
= model
.get_iter(path
)
297 if model
.iter_parent(to_iter
) is not None:
298 to_iter
= model
.iter_parent(to_iter
)
300 from_row
= model
.get_path(from_iter
)[0]
303 if ( position
== gtk
.TREE_VIEW_DROP_BEFORE
or
304 position
== gtk
.TREE_VIEW_DROP_INTO_OR_BEFORE
):
305 model
.move_before( from_iter
, to_iter
)
306 to_row
= to_row
- 1 if from_row
< to_row
else to_row
307 elif ( position
== gtk
.TREE_VIEW_DROP_AFTER
or
308 position
== gtk
.TREE_VIEW_DROP_INTO_OR_AFTER
):
309 model
.move_after( from_iter
, to_iter
)
310 to_row
= to_row
+ 1 if from_row
> to_row
else to_row
312 self
.__log
.debug('Drop not supported: %s', position
)
314 # don't do anything if we're not actually moving rows around
315 if from_row
!= to_row
:
316 player
.playlist
.move_item( from_row
, to_row
)
319 self
.__log
.debug('No drop_data or selection.data available')
321 def update_model(self
):
322 path_info
= self
.treeview
.get_path_at_pos(0,0)
323 path
= path_info
[0] if path_info
is not None else None
325 self
.model
= player
.playlist
.get_bookmark_model()
326 self
.treeview
.set_model(self
.model
)
327 self
.treeview
.expand_all()
330 self
.treeview
.scroll_to_cell(path
)
332 def label_edited(self
, cellrenderer
, path
, new_text
):
333 iter = self
.model
.get_iter(path
)
334 old_text
= self
.model
.get_value(iter, 1)
337 if old_text
!= new_text
:
338 for m
, bkmk_id
, biter
, item_id
, iiter
in self
.__cur
_selection
():
339 if iiter
is not None:
340 self
.model
.set_value(iter, 1, new_text
)
342 self
.model
.set_value(iter, 1, new_text
)
343 player
.playlist
.update_bookmark(
344 item_id
, bkmk_id
, name
=new_text
)
346 self
.model
.set_value(iter, 1, old_text
)
348 def add_bookmark(self
, w
=None, lbl
=None, pos
=None):
349 (label
, position
) = player
.get_formatted_position(pos
)
350 label
= label
if lbl
is None else lbl
351 position
= position
if pos
is None else pos
352 player
.playlist
.save_bookmark( label
, position
)
353 util
.notify(_('Bookmark Added.'))
356 def add_file(self
, widget
):
357 filename
= get_file_from_filechooser(self
.main
.main_window
)
358 if filename
is not None:
359 player
.playlist
.append(filename
)
361 def add_directory(self
, widget
):
362 directory
= get_file_from_filechooser(
363 self
.main
.main_window
, folder
=True )
364 if directory
is not None:
365 player
.playlist
.load_directory(directory
, append
=True)
367 def __cur_selection(self
):
368 selection
= self
.treeview
.get_selection()
369 model
, bookmark_paths
= selection
.get_selected_rows()
371 # Convert the paths to gtk.TreeRowReference objects, because we
372 # might modify the model while this generator is running
373 bookmark_refs
= [gtk
.TreeRowReference(model
, p
) for p
in bookmark_paths
]
375 for reference
in bookmark_refs
:
376 bookmark_iter
= model
.get_iter(reference
.get_path())
377 item_iter
= model
.iter_parent(bookmark_iter
)
379 # bookmark_iter is actually an item_iter
380 if item_iter
is None:
381 item_iter
= bookmark_iter
382 item_id
= model
.get_value(item_iter
, 0)
383 bookmark_id
, bookmark_iter
= None, None
385 bookmark_id
= model
.get_value(bookmark_iter
, 0)
386 item_id
= model
.get_value(item_iter
, 0)
388 yield model
, bookmark_id
, bookmark_iter
, item_id
, item_iter
390 def remove_bookmark(self
, w
):
391 for model
, bkmk_id
, bkmk_iter
, item_id
, item_iter
in self
.__cur
_selection
():
392 player
.playlist
.remove_bookmark( item_id
, bkmk_id
)
393 if bkmk_iter
is not None:
394 model
.remove(bkmk_iter
)
395 elif item_iter
is not None:
396 model
.remove(item_iter
)
398 def jump_bookmark(self
, w
):
399 selected
= list(self
.__cur
_selection
())
400 if len(selected
) == 1:
401 # It should be guranteed by the fact that we only enable the
402 # "Jump to" button when the selection count equals 1.
403 model
, bkmk_id
, bkmk_iter
, item_id
, item_iter
= selected
.pop(0)
404 player
.playlist
.load_from_bookmark_id(item_id
, bkmk_id
)
406 # FIXME: The player/playlist should be able to take care of this
407 if not player
.playing
:
410 class GTK_Main(object):
412 def __init__(self
, filename
=None):
413 self
.__log
= logging
.getLogger('panucci.panucci.GTK_Main')
414 interface
.register_gui(self
)
415 self
.pickle_file_conversion()
418 self
.progress_timer_id
= None
419 self
.volume_timer_id
= None
420 self
.anti_blank_timer
= None
422 self
.recent_files
= []
423 self
.make_main_window()
424 self
.has_coverart
= False
425 self
.set_volume(settings
.volume
)
426 self
.last_seekbutton_pressed
= None
427 self
.last_seekbutton_pressed_time
= 0
428 self
.__window
_fullscreen
= False
430 if util
.platform
==util
.MAEMO
and interface
.headset_device
is not None:
431 # Enable play/pause with headset button
432 interface
.headset_device
.connect_to_signal(
433 'Condition', self
.handle_headset_button
)
435 settings
.register( 'volume_changed', self
.set_volume
)
436 settings
.register('allow_blanking_changed',self
.__set
_anti
_blank
_timer
)
437 self
.__set
_anti
_blank
_timer
( settings
.allow_blanking
)
439 player
.register( 'stopped', self
.on_player_stopped
)
440 player
.register( 'playing', self
.on_player_playing
)
441 player
.register( 'paused', self
.on_player_paused
)
442 player
.register( 'end_of_playlist', self
.on_player_end_of_playlist
)
443 player
.playlist
.register('new_track_metadata',self
.on_player_new_track
)
444 player
.playlist
.register( 'file_queued', self
.on_file_queued
)
445 player
.init(filepath
=filename
)
447 def make_main_window(self
):
448 if util
.platform
== util
.MAEMO
:
449 self
.app
= hildon
.Program()
450 window
= hildon
.Window()
451 self
.app
.add_window(window
)
453 window
= gtk
.Window(gtk
.WINDOW_TOPLEVEL
)
455 window
.set_title('Panucci')
456 self
.window_icon
= util
.find_image('panucci.png')
457 if self
.window_icon
is not None:
458 window
.set_icon_from_file( self
.window_icon
)
459 window
.set_default_size(400, -1)
460 window
.set_border_width(0)
461 window
.connect("destroy", self
.destroy
)
462 self
.main_window
= window
464 if util
.platform
== util
.MAEMO
:
465 window
.set_menu(self
.create_menu())
467 menu_vbox
= gtk
.VBox()
468 menu_vbox
.set_spacing(0)
469 window
.add(menu_vbox
)
470 menu_bar
= gtk
.MenuBar()
471 root_menu
= gtk
.MenuItem('Panucci')
472 root_menu
.set_submenu(self
.create_menu())
473 menu_bar
.append(root_menu
)
474 menu_vbox
.pack_start(menu_bar
, False, False, 0)
477 self
.notebook
= gtk
.Notebook()
479 if util
.platform
== util
.MAEMO
:
480 window
.add(self
.notebook
)
482 menu_vbox
.pack_end(self
.notebook
, True, True, 6)
484 main_hbox
= gtk
.HBox()
485 self
.notebook
.append_page(main_hbox
, gtk
.Label(_('Player')))
486 self
.notebook
.set_tab_label_packing(main_hbox
,True,True,gtk
.PACK_START
)
488 main_vbox
= gtk
.VBox()
489 main_vbox
.set_spacing(6)
490 # add a vbox to the main_hbox
491 main_hbox
.pack_start(main_vbox
, True, True)
493 # a hbox to hold the cover art and metadata vbox
494 metadata_hbox
= gtk
.HBox()
495 metadata_hbox
.set_spacing(6)
496 main_vbox
.pack_start(metadata_hbox
, True, False)
498 self
.cover_art
= gtk
.Image()
499 metadata_hbox
.pack_start( self
.cover_art
, False, False )
501 # vbox to hold metadata
502 metadata_vbox
= gtk
.VBox()
503 metadata_vbox
.set_spacing(8)
504 empty_label
= gtk
.Label()
505 metadata_vbox
.pack_start(empty_label
, True, True)
506 self
.artist_label
= gtk
.Label('')
507 self
.artist_label
.set_ellipsize(pango
.ELLIPSIZE_END
)
508 metadata_vbox
.pack_start(self
.artist_label
, False, False)
509 self
.album_label
= gtk
.Label('')
510 self
.album_label
.set_ellipsize(pango
.ELLIPSIZE_END
)
511 metadata_vbox
.pack_start(self
.album_label
, False, False)
512 self
.title_label
= gtk
.Label('')
513 self
.title_label
.set_line_wrap(True)
514 metadata_vbox
.pack_start(self
.title_label
, False, False)
515 empty_label
= gtk
.Label()
516 metadata_vbox
.pack_start(empty_label
, True, True)
517 metadata_hbox
.pack_start( metadata_vbox
, True, True )
519 progress_eventbox
= gtk
.EventBox()
520 progress_eventbox
.set_events(gtk
.gdk
.BUTTON_PRESS_MASK
)
521 progress_eventbox
.connect(
522 'button-press-event', self
.on_progressbar_changed
)
523 self
.progress
= gtk
.ProgressBar()
524 # make the progress bar more "finger-friendly"
525 if util
.platform
== util
.MAEMO
:
526 self
.progress
.set_size_request( -1, 50 )
527 progress_eventbox
.add(self
.progress
)
528 main_vbox
.pack_start( progress_eventbox
, False, False )
530 # make the button box
531 buttonbox
= gtk
.HBox()
533 self
.rrewind_button
= gtk
.Button('')
534 image(self
.rrewind_button
, 'media-skip-backward.png')
535 self
.rrewind_button
.connect( 'pressed', self
.on_seekbutton_pressed
)
536 self
.rrewind_button
.connect(
537 'clicked', self
.on_seekbutton_clicked
, -1*settings
.seek_long
, True )
538 buttonbox
.add(self
.rrewind_button
)
540 self
.rewind_button
= gtk
.Button('')
541 image(self
.rewind_button
, 'media-seek-backward.png')
542 self
.rewind_button
.connect( 'pressed', self
.on_seekbutton_pressed
)
543 self
.rewind_button
.connect(
544 'clicked', self
.on_seekbutton_clicked
, -1*settings
.seek_short
, False)
545 buttonbox
.add(self
.rewind_button
)
547 self
.play_pause_button
= gtk
.Button('')
548 image(self
.play_pause_button
, gtk
.STOCK_OPEN
, True)
549 self
.button_handler_id
= self
.play_pause_button
.connect(
550 'clicked', self
.open_file_callback
)
551 buttonbox
.add(self
.play_pause_button
)
553 self
.forward_button
= gtk
.Button('')
554 image(self
.forward_button
, 'media-seek-forward.png')
555 self
.forward_button
.connect( 'pressed', self
.on_seekbutton_pressed
)
556 self
.forward_button
.connect(
557 'clicked', self
.on_seekbutton_clicked
, settings
.seek_short
, False )
558 buttonbox
.add(self
.forward_button
)
560 self
.fforward_button
= gtk
.Button('')
561 image(self
.fforward_button
, 'media-skip-forward.png')
562 self
.fforward_button
.connect( 'pressed', self
.on_seekbutton_pressed
)
563 self
.fforward_button
.connect(
564 'clicked', self
.on_seekbutton_clicked
, settings
.seek_long
, True )
565 buttonbox
.add(self
.fforward_button
)
567 self
.bookmarks_button
= gtk
.Button('')
568 image(self
.bookmarks_button
, 'bookmark-new.png')
569 buttonbox
.add(self
.bookmarks_button
)
570 self
.set_controls_sensitivity(False)
571 main_vbox
.pack_start(buttonbox
, False, False)
573 self
.playlist_tab
= PlaylistTab(self
)
574 self
.bookmarks_button
.connect('clicked',self
.playlist_tab
.add_bookmark
)
575 self
.notebook
.append_page(self
.playlist_tab
, gtk
.Label(_('Playlist')))
576 self
.notebook
.set_tab_label_packing(
577 self
.playlist_tab
, True, True, gtk
.PACK_START
)
580 self
.notebook
.set_current_page(0)
582 if util
.platform
== util
.MAEMO
:
583 self
.volume
= hildon
.VVolumebar()
584 self
.volume
.set_property('can-focus', False)
585 self
.volume
.connect('level_changed', self
.volume_changed_hildon
)
586 self
.volume
.connect('mute_toggled', self
.mute_toggled
)
587 window
.connect('key-press-event', self
.on_key_press
)
588 main_hbox
.pack_start(self
.volume
, False, True)
590 # Add a button to pop out the volume bar
591 self
.volume_button
= gtk
.ToggleButton('')
592 image(self
.volume_button
, 'media-speaker.png')
593 self
.volume_button
.connect('clicked', self
.toggle_volumebar
)
595 'show', lambda x
: self
.volume_button
.set_active(True))
597 'hide', lambda x
: self
.volume_button
.set_active(False))
598 buttonbox
.add(self
.volume_button
)
599 self
.volume_button
.show()
601 # Disable focus for all widgets, so we can use the cursor
602 # keys + enter to directly control our media player, which
603 # is handled by "key-press-event"
605 self
.rrewind_button
, self
.rewind_button
,
606 self
.play_pause_button
, self
.forward_button
,
607 self
.fforward_button
, self
.progress
,
608 self
.bookmarks_button
, self
.volume_button
, ):
609 w
.unset_flags(gtk
.CAN_FOCUS
)
611 self
.volume
= gtk
.VolumeButton()
612 self
.volume
.connect('value-changed', self
.volume_changed_gtk
)
613 buttonbox
.add(self
.volume
)
616 self
.set_volume(settings
.volume
)
618 def create_menu(self
):
622 menu_open
= gtk
.ImageMenuItem(_('Open playlist'))
624 gtk
.image_new_from_stock(gtk
.STOCK_OPEN
, gtk
.ICON_SIZE_MENU
))
625 menu_open
.connect("activate", self
.open_file_callback
)
626 menu
.append(menu_open
)
628 # the recent files menu
629 self
.menu_recent
= gtk
.MenuItem(_('Open recent playlist'))
630 menu
.append(self
.menu_recent
)
631 self
.create_recent_files_menu()
633 menu
.append(gtk
.SeparatorMenuItem())
635 menu_save
= gtk
.ImageMenuItem(_('Save current playlist'))
637 gtk
.image_new_from_stock(gtk
.STOCK_SAVE_AS
, gtk
.ICON_SIZE_MENU
))
638 menu_save
.connect("activate", self
.save_to_playlist_callback
)
639 menu
.append(menu_save
)
641 menu
.append(gtk
.SeparatorMenuItem())
643 # the settings sub-menu
644 menu_settings
= gtk
.MenuItem(_('Settings'))
645 menu
.append(menu_settings
)
647 menu_settings_sub
= gtk
.Menu()
648 menu_settings
.set_submenu(menu_settings_sub
)
650 menu_settings_disable_skip
= gtk
.CheckMenuItem(
651 _('Disable Delayed Track Skipping') )
652 settings
.attach_checkbutton(
653 menu_settings_disable_skip
, 'disable_delayed_skip' )
654 menu_settings_sub
.append(menu_settings_disable_skip
)
656 menu_settings_lock_progress
= gtk
.CheckMenuItem(_('Lock Progress Bar'))
657 settings
.attach_checkbutton(
658 menu_settings_lock_progress
, 'progress_locked' )
659 menu_settings_sub
.append(menu_settings_lock_progress
)
661 menu_about
= gtk
.ImageMenuItem(gtk
.STOCK_ABOUT
)
662 menu_about
.connect("activate", self
.show_about
, self
.main_window
)
663 menu
.append(menu_about
)
665 menu
.append(gtk
.SeparatorMenuItem())
667 menu_quit
= gtk
.ImageMenuItem(gtk
.STOCK_QUIT
)
668 menu_quit
.connect("activate", self
.destroy
)
669 menu
.append(menu_quit
)
673 def create_recent_files_menu( self
):
674 max_files
= settings
.max_recent_files
675 self
.recent_files
= player
.playlist
.get_recent_files(max_files
)
676 menu_recent_sub
= gtk
.Menu()
678 temp_playlist
= os
.path
.expanduser(settings
.temp_playlist
)
680 if len(self
.recent_files
) > 0:
681 for f
in self
.recent_files
:
682 # don't include the temporary playlist in the file list
683 if f
== temp_playlist
: continue
684 filename
, extension
= os
.path
.splitext(os
.path
.basename(f
))
685 menu_item
= gtk
.MenuItem( filename
.replace('_', ' '))
686 menu_item
.connect('activate', self
.on_recent_file_activate
, f
)
687 menu_recent_sub
.append(menu_item
)
689 menu_item
= gtk
.MenuItem(_('No recent files available.'))
690 menu_item
.set_sensitive(False)
691 menu_recent_sub
.append(menu_item
)
693 self
.menu_recent
.set_submenu(menu_recent_sub
)
695 def __get_fullscreen(self
):
696 return self
.__window
_fullscreen
698 def __set_fullscreen(self
, value
):
699 if value
and not self
.__window
_fullscreen
:
700 self
.main_window
.fullscreen()
701 elif not value
and self
.__window
_fullscreen
:
702 self
.main_window
.unfullscreen()
704 self
.__window
_fullscreen
= value
706 fullscreen
= property( __get_fullscreen
, __set_fullscreen
)
708 def on_recent_file_activate(self
, widget
, filepath
):
709 self
.play_file(filepath
)
711 def show_about(self
, w
, win
):
712 dialog
= gtk
.AboutDialog()
713 dialog
.set_website(about_website
)
714 dialog
.set_website_label(about_website
)
715 dialog
.set_name(about_name
)
716 dialog
.set_authors(about_authors
)
717 dialog
.set_comments(about_text
)
718 dialog
.set_version(app_version
)
722 def destroy(self
, widget
):
726 def handle_headset_button(self
, event
, button
):
727 if event
== 'ButtonPressed' and button
== 'phone':
728 self
.on_btn_play_pause_clicked()
730 def check_queue(self
):
731 """ Makes sure the queue is saved if it has been modified
732 True means a new file can be opened
733 False means the user does not want to continue """
735 if player
.playlist
.queue_modified
:
737 self
.main_window
, _('Save current playlist'),
738 _('Current playlist has been modified'),
739 _('Opening a new file will replace the current playlist. ') +
740 _('Do you want to save it before creating a new one?'),
741 affirmative_button
=gtk
.STOCK_SAVE
,
742 negative_button
=_('Discard changes'))
744 self
.__log
.debug('Response to "Save Queue?": %s', response
)
749 return self
.save_to_playlist_callback()
757 def open_file_callback(self
, widget
=None):
758 if self
.check_queue():
759 filename
= get_file_from_filechooser(self
.main_window
)
760 if filename
is not None:
761 self
._play
_file
(filename
)
763 def save_to_playlist_callback(self
, widget
=None):
764 filename
= get_file_from_filechooser(
765 self
.main_window
, save_file
=True, save_to
='playlist.m3u' )
770 if os
.path
.isfile(filename
):
771 response
= dialog( self
.main_window
, _('File already exists'),
772 _('File already exists'),
773 _('The file %s already exists. You can choose another name or '
774 'overwrite the existing file.') % os
.path
.basename(filename
),
775 affirmative_button
=gtk
.STOCK_SAVE
,
776 negative_button
=_('Rename file'))
784 return self
.save_to_playlist_callback()
786 ext
= util
.detect_filetype(filename
)
787 if not player
.playlist
.save_to_new_playlist(filename
, ext
):
788 util
.notify(_('Error saving playlist...'))
793 def set_controls_sensitivity(self
, sensitive
):
794 self
.forward_button
.set_sensitive(sensitive
)
795 self
.rewind_button
.set_sensitive(sensitive
)
796 self
.fforward_button
.set_sensitive(sensitive
)
797 self
.rrewind_button
.set_sensitive(sensitive
)
799 def on_key_press(self
, widget
, event
):
800 # this stuff is only maemo-related
801 if util
.platform
== util
.MAEMO
:
802 if event
.keyval
== gtk
.keysyms
.F7
: #plus
803 self
.set_volume( min( 1, self
.get_volume() + 0.10 ))
804 elif event
.keyval
== gtk
.keysyms
.F8
: #minus
805 self
.set_volume( max( 0, self
.get_volume() - 0.10 ))
806 elif event
.keyval
== gtk
.keysyms
.Left
: # seek back
807 self
.rewind_callback(self
.rewind_button
)
808 elif event
.keyval
== gtk
.keysyms
.Right
: # seek forward
809 self
.forward_callback(self
.forward_button
)
810 elif event
.keyval
== gtk
.keysyms
.Return
: # play/pause
811 self
.on_btn_play_pause_clicked()
812 elif event
.keyval
== gtk
.keysyms
.F6
:
813 self
.fullscreen
= not self
.fullscreen
815 # The following two functions get and set the
816 # volume from the volume control widgets.
817 def get_volume(self
):
818 if util
.platform
== util
.MAEMO
:
819 return self
.volume
.get_level()/100.0
821 return self
.volume
.get_value()
823 def set_volume(self
, vol
):
824 """ vol is a float from 0 to 1 """
826 if util
.platform
== util
.MAEMO
:
827 self
.volume
.set_level(vol
*100.0)
829 self
.volume
.set_value(vol
)
831 def __set_volume_hide_timer(self
, timeout
, force_show
=False):
832 if force_show
or self
.volume_button
.get_active():
834 if self
.volume_timer_id
is not None:
835 gobject
.source_remove(self
.volume_timer_id
)
836 self
.volume_timer_id
= None
838 self
.volume_timer_id
= gobject
.timeout_add(
839 1000 * timeout
, self
.__volume
_hide
_callback
)
841 def __volume_hide_callback(self
):
842 self
.volume_timer_id
= None
846 def __set_anti_blank_timer(self
, allow_blanking
):
847 if util
.platform
== util
.MAEMO
:
848 if allow_blanking
and self
.anti_blank_timer
is not None:
849 self
.__log
.info('Screen blanking enabled.')
850 gobject
.source_remove(self
.anti_blank_timer
)
851 self
.anti_blank_timer
= None
852 elif not allow_blanking
and self
.anti_blank_timer
is None:
853 self
.__log
.info('Attempting to disable screen blanking.')
854 self
.anti_blank_timer
= gobject
.timeout_add(
855 1000 * 59, util
.poke_backlight
)
857 self
.__log
.info('Blanking controls are for Maemo only.')
859 def toggle_volumebar(self
, widget
=None):
860 if self
.volume_timer_id
is None:
861 self
.__set
_volume
_hide
_timer
(5)
863 self
.__volume
_hide
_callback
()
865 def volume_changed_gtk(self
, widget
, new_value
=0.5):
866 settings
.volume
= new_value
868 def volume_changed_hildon(self
, widget
):
869 self
.__set
_volume
_hide
_timer
( 4, force_show
=True )
870 settings
.volume
= widget
.get_level()/100.0
872 def mute_toggled(self
, widget
):
873 if widget
.get_mute():
876 settings
.volume
= widget
.get_level()/100.0
878 def show_main_window(self
):
879 self
.main_window
.present()
881 def play_file(self
, filename
):
882 if self
.check_queue():
883 self
._play
_file
(filename
)
885 def _play_file(self
, filename
, pause_on_load
=False):
888 player
.playlist
.load( os
.path
.abspath(filename
) )
889 if player
.playlist
.is_empty
:
894 def on_player_stopped(self
):
895 self
.stop_progress_timer()
896 self
.title_label
.set_size_request(-1,-1)
897 self
.reset_progress()
898 self
.set_controls_sensitivity(False)
900 def on_player_playing(self
):
901 self
.start_progress_timer()
902 image(self
.play_pause_button
, 'media-playback-pause.png')
903 self
.set_controls_sensitivity(True)
905 def on_player_new_track(self
, metadata
):
906 image(self
.play_pause_button
, 'media-playback-start.png')
907 self
.play_pause_button
.disconnect(self
.button_handler_id
)
908 self
.button_handler_id
= self
.play_pause_button
.connect(
909 'clicked', self
.on_btn_play_pause_clicked
)
911 for widget
in [self
.title_label
,self
.artist_label
,self
.album_label
]:
915 self
.cover_art
.hide()
916 self
.has_coverart
= False
917 self
.set_metadata(metadata
)
919 text
, position
= player
.get_formatted_position()
920 estimated_length
= metadata
.get('length', 0)
921 self
.set_progress_callback( position
, estimated_length
)
923 def on_player_paused(self
):
924 self
.stop_progress_timer() # This should save some power
925 image(self
.play_pause_button
, 'media-playback-start.png')
927 def on_player_end_of_playlist(self
):
928 self
.play_pause_button
.disconnect(self
.button_handler_id
)
929 self
.button_handler_id
= self
.play_pause_button
.connect(
930 'clicked', self
.open_file_callback
)
931 image(self
.play_pause_button
, gtk
.STOCK_OPEN
, True)
933 def on_file_queued(self
, filepath
, success
, notify
):
935 filename
= os
.path
.basename(filepath
)
938 util
.notify( '%s added successfully.' % filename
))
941 util
.notify( 'Error adding %s to the queue.' % filename
))
943 def reset_progress(self
):
944 self
.progress
.set_fraction(0)
945 self
.set_progress_callback(0,0)
947 def set_progress_callback(self
, time_elapsed
, total_time
):
948 """ times must be in nanoseconds """
949 time_string
= "%s / %s" % ( util
.convert_ns(time_elapsed
),
950 util
.convert_ns(total_time
) )
951 self
.progress
.set_text( time_string
)
952 fraction
= float(time_elapsed
) / float(total_time
) if total_time
else 0
953 self
.progress
.set_fraction( fraction
)
955 def on_progressbar_changed(self
, widget
, event
):
956 if ( not settings
.progress_locked
and
957 event
.type == gtk
.gdk
.BUTTON_PRESS
and event
.button
== 1 ):
958 new_fraction
= event
.x
/float(widget
.get_allocation().width
)
959 resp
= player
.do_seek(percent
=new_fraction
)
961 # Preemptively update the progressbar to make seeking smoother
962 self
.set_progress_callback( *resp
)
964 def on_btn_play_pause_clicked(self
, widget
=None):
965 player
.play_pause_toggle()
967 def progress_timer_callback( self
):
968 if player
.playing
and not player
.seeking
:
969 pos_int
, dur_int
= player
.get_position_duration()
970 # This prevents bogus values from being set while seeking
971 if ( pos_int
> 10**9 ) and ( dur_int
> 10**9 ):
972 self
.set_progress_callback( pos_int
, dur_int
)
975 def start_progress_timer( self
):
976 if self
.progress_timer_id
is not None:
977 self
.stop_progress_timer()
979 self
.progress_timer_id
= gobject
.timeout_add(
980 1000, self
.progress_timer_callback
)
982 def stop_progress_timer( self
):
983 if self
.progress_timer_id
is not None:
984 gobject
.source_remove( self
.progress_timer_id
)
985 self
.progress_timer_id
= None
987 def set_coverart( self
, pixbuf
):
988 self
.cover_art
.set_from_pixbuf(pixbuf
)
989 self
.cover_art
.show()
990 self
.has_coverart
= True
992 def set_metadata( self
, tag_message
):
993 tags
= { 'title': self
.title_label
, 'artist': self
.artist_label
,
994 'album': self
.album_label
}
996 if tag_message
.has_key('image') and tag_message
['image'] is not None:
997 value
= tag_message
['image']
999 pbl
= gtk
.gdk
.PixbufLoader()
1003 pixbuf
= pbl
.get_pixbuf().scale_simple(
1004 coverart_size
[0], coverart_size
[1], gtk
.gdk
.INTERP_BILINEAR
)
1005 self
.set_coverart(pixbuf
)
1006 except Exception, e
:
1007 self
.__log
.exception('Error setting coverart...')
1009 tag_vals
= dict([ (i
,'') for i
in tags
.keys()])
1010 for tag
,value
in tag_message
.iteritems():
1011 if tags
.has_key(tag
) and value
is not None and value
.strip():
1012 tags
[tag
].set_markup('<big>'+value
+'</big>')
1013 tag_vals
[tag
] = value
1014 tags
[tag
].set_alignment( 0.5*int(not self
.has_coverart
), 0.5)
1017 if util
.platform
== util
.MAEMO
:
1018 self
.main_window
.set_title(value
)
1019 # oh man this is hacky :(
1020 if self
.has_coverart
:
1021 tags
[tag
].set_size_request(420,-1)
1022 if len(value
) >= 80: value
= value
[:80] + '...'
1024 self
.main_window
.set_title(value
+ ' - Panucci')
1026 tags
[tag
].set_markup('<b><big>'+value
+'</big></b>')
1028 def on_seekbutton_pressed(self
, widget
):
1029 self
.last_seekbutton_pressed
= widget
1030 self
.last_seekbutton_pressed_time
= time
.time()
1031 self
.__log
.debug( 'Seekbutton %s pressed at %f',
1032 hash(widget
), self
.last_seekbutton_pressed_time
)
1034 def on_seekbutton_clicked(self
, widget
, seek_amount
, long_seek
):
1035 time_delta
= time
.time() - self
.last_seekbutton_pressed_time
1036 self
.__log
.debug('Seekbutton %s released, delta t = %f',
1037 hash(widget
), time_delta
)
1039 if ( long_seek
and not settings
.disable_delayed_skip
and
1040 widget
== self
.last_seekbutton_pressed
and
1041 time_delta
> settings
.skip_delay
):
1044 player
.playlist
.next()
1046 player
.playlist
.prev()
1048 resp
= player
.do_seek(from_current
=seek_amount
*10**9)
1050 # Preemptively update the progressbar to make seeking smoother
1051 self
.set_progress_callback( *resp
)
1053 def pickle_file_conversion(self
):
1054 pickle_file
= os
.path
.expanduser('~/.rmp-bookmarks')
1055 if os
.path
.isfile(pickle_file
):
1056 import pickle_converter
1059 util
.notify( _('Converting old pickle format to SQLite.') ))
1060 self
.__log
.info( util
.notify( _('This may take a while...') ))
1062 if pickle_converter
.load_pickle_file(pickle_file
):
1064 util
.notify( _('Pickle file converted successfully.') ))
1066 self
.__log
.error( util
.notify(
1067 _('Error converting pickle file, check your log...') ))
1069 def run(filename
=None):
1070 GTK_Main( filename
)
1073 if __name__
== '__main__':
1074 log
.error( 'WARNING: Use the "panucci" executable to run this program.' )
1075 log
.error( 'Exiting...' )