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 PlaylistItemDetails(gtk
.Dialog
):
182 def __init__(self
, main
, playlist_item
):
183 gtk
.Dialog
.__init
__(self
, _('Playlist item details'), main
.main_window
, gtk
.DIALOG_MODAL
, (gtk
.STOCK_CLOSE
, gtk
.RESPONSE_OK
))
185 self
.fill(playlist_item
)
186 self
.set_has_separator(False)
187 self
.set_resizable(False)
192 def fill(self
, playlist_item
):
194 self
.vbox
.pack_start(t
, expand
=False)
196 metadata
= playlist_item
.metadata
198 t
.attach(gtk
.Label(_('Custom title:')), 0, 1, 0, 1)
199 t
.attach(gtk
.Label(_('ID:')), 0, 1, 1, 2)
200 t
.attach(gtk
.Label(_('Playlist ID:')), 0, 1, 2, 3)
201 t
.attach(gtk
.Label(_('Filepath:')), 0, 1, 3, 4)
205 if metadata
[key
] is not None:
206 t
.attach(gtk
.Label(key
.capitalize()+':'), 0, 1, row_num
, row_num
+1)
209 t
.foreach(lambda x
, y
: x
.set_alignment(1, 0.5), None)
210 t
.foreach(lambda x
, y
: x
.set_markup('<b>%s</b>' % x
.get_label()), None)
212 t
.attach(gtk
.Label(playlist_item
.title
or _('<not modified>')), 1, 2, 0, 1)
213 t
.attach(gtk
.Label(str(playlist_item
)), 1, 2, 1, 2)
214 t
.attach(gtk
.Label(playlist_item
.playlist_id
), 1, 2, 2, 3)
215 t
.attach(gtk
.Label(playlist_item
.filepath
), 1, 2, 3, 4)
219 value
= metadata
[key
]
221 value
= util
.convert_ns(value
)
222 if metadata
[key
] is not None:
223 t
.attach(gtk
.Label(str(value
) or _('<not set>')), 1, 2, row_num
, row_num
+1)
226 t
.foreach(lambda x
, y
: x
.get_alignment() == (0.5, 0.5) and \
227 x
.set_alignment(0, 0.5), None)
229 t
.set_border_width(8)
230 t
.set_row_spacings(4)
231 t
.set_col_spacings(8)
233 l
= gtk
.ListStore(str, str)
235 cr
= gtk
.CellRendererText()
236 cr
.set_property('ellipsize', pango
.ELLIPSIZE_END
)
237 c
= gtk
.TreeViewColumn(_('Title'), cr
, text
=0)
240 c
= gtk
.TreeViewColumn(_('Time'), gtk
.CellRendererText(), text
=1)
242 playlist_item
.load_bookmarks()
243 for bookmark
in playlist_item
.bookmarks
:
244 l
.append([bookmark
.bookmark_name
, \
245 util
.convert_ns(bookmark
.seek_position
)])
247 sw
= gtk
.ScrolledWindow()
248 sw
.set_shadow_type(gtk
.SHADOW_IN
)
250 sw
.set_policy(gtk
.POLICY_AUTOMATIC
, gtk
.POLICY_AUTOMATIC
)
251 e
= gtk
.Expander(_('Bookmarks'))
253 self
.vbox
.pack_start(e
)
255 class PlaylistTab(gtk
.VBox
):
256 def __init__(self
, main_window
):
257 gtk
.VBox
.__init
__(self
)
258 self
.__log
= logging
.getLogger('panucci.panucci.BookmarksWindow')
259 self
.main
= main_window
262 self
.treeview
= gtk
.TreeView()
263 self
.treeview
.set_headers_visible(True)
264 tree_selection
= self
.treeview
.get_selection()
265 tree_selection
.set_mode(gtk
.SELECTION_MULTIPLE
)
266 tree_selection
.connect('changed', self
.tree_selection_changed
)
268 # The tree lines look nasty on maemo
269 if util
.platform
== util
.LINUX
:
270 self
.treeview
.set_enable_tree_lines(True)
273 ncol
= gtk
.TreeViewColumn(_('Name'))
274 ncell
= gtk
.CellRendererText()
275 ncell
.set_property('ellipsize', pango
.ELLIPSIZE_END
)
276 ncell
.set_property('editable', True)
277 ncell
.connect('edited', self
.label_edited
)
278 ncol
.set_expand(True)
279 ncol
.pack_start(ncell
)
280 ncol
.add_attribute(ncell
, 'text', 1)
282 tcol
= gtk
.TreeViewColumn(_('Position'))
283 tcell
= gtk
.CellRendererText()
284 tcol
.pack_start(tcell
)
285 tcol
.add_attribute(tcell
, 'text', 2)
287 self
.treeview
.append_column(ncol
)
288 self
.treeview
.append_column(tcol
)
289 self
.treeview
.connect('drag-data-received', self
.drag_data_recieved
)
290 self
.treeview
.connect('drag_data_get', self
.drag_data_get_data
)
293 ( 'playlist_row_data', gtk
.TARGET_SAME_WIDGET
, 0 ) ]
295 self
.treeview
.enable_model_drag_source(
296 gtk
.gdk
.BUTTON1_MASK
, treeview_targets
, gtk
.gdk
.ACTION_COPY
)
298 self
.treeview
.enable_model_drag_dest(
299 treeview_targets
, gtk
.gdk
.ACTION_COPY
)
301 sw
= gtk
.ScrolledWindow()
302 sw
.set_policy(gtk
.POLICY_AUTOMATIC
, gtk
.POLICY_AUTOMATIC
)
303 sw
.set_shadow_type(gtk
.SHADOW_IN
)
304 sw
.add(self
.treeview
)
307 self
.hbox
= gtk
.HBox()
309 self
.add_button
= gtk
.Button(gtk
.STOCK_NEW
)
310 self
.add_button
.set_use_stock(True)
311 set_stock_button_text( self
.add_button
, _('Add File') )
312 self
.add_button
.connect('clicked', self
.add_file
)
313 self
.hbox
.pack_start(self
.add_button
, True, True)
315 self
.dir_button
= gtk
.Button(gtk
.STOCK_OPEN
)
316 self
.dir_button
.set_use_stock(True)
317 set_stock_button_text( self
.dir_button
, _('Add Directory') )
318 self
.dir_button
.connect('clicked', self
.add_directory
)
319 self
.hbox
.pack_start(self
.dir_button
, True, True)
321 self
.remove_button
= gtk
.Button(gtk
.STOCK_REMOVE
)
322 self
.remove_button
.set_use_stock(True)
323 self
.remove_button
.connect('clicked', self
.remove_bookmark
)
324 self
.hbox
.pack_start(self
.remove_button
, True, True)
326 self
.jump_button
= gtk
.Button(gtk
.STOCK_JUMP_TO
)
327 self
.jump_button
.set_use_stock(True)
328 self
.jump_button
.connect('clicked', self
.jump_bookmark
)
329 self
.hbox
.pack_start(self
.jump_button
, True, True)
331 self
.info_button
= gtk
.Button()
332 self
.info_button
.add(gtk
.image_new_from_stock(gtk
.STOCK_INFO
, gtk
.ICON_SIZE_BUTTON
))
333 self
.info_button
.connect('clicked', self
.show_playlist_item_details
)
334 self
.hbox
.pack_start(self
.info_button
, True, True)
336 self
.pack_start(self
.hbox
, False, True)
338 player
.playlist
.register(
339 'file_queued', lambda x
,y
,z
: self
.update_model() )
343 def tree_selection_changed(self
, treeselection
):
344 count
= treeselection
.count_selected_rows()
345 self
.remove_button
.set_sensitive(count
> 0)
346 self
.jump_button
.set_sensitive(count
== 1)
347 self
.info_button
.set_sensitive(count
== 1)
349 def drag_data_get_data(
350 self
, treeview
, context
, selection
, target_id
, timestamp
):
352 treeselection
= treeview
.get_selection()
353 model
, iter = treeselection
.get_selected()
354 # only allow moving around top-level parents
355 if model
.iter_parent(iter) is None:
356 # send the path of the selected row
357 data
= model
.get_string_from_iter(iter)
358 selection
.set(selection
.target
, 8, data
)
360 self
.__log
.debug("Can't move children...")
362 def drag_data_recieved(
363 self
, treeview
, context
, x
, y
, selection
, info
, timestamp
):
365 drop_info
= treeview
.get_dest_row_at_pos(x
, y
)
367 # TODO: If user drags the row past the last row, drop_info is None
368 # I'm not sure if it's safe to simply assume that None is
369 # euqivalent to the last row...
370 if None not in [ drop_info
and selection
.data
]:
371 model
= treeview
.get_model()
372 path
, position
= drop_info
374 from_iter
= model
.get_iter_from_string(selection
.data
)
376 # make sure the to_iter doesn't have a parent
377 to_iter
= model
.get_iter(path
)
378 if model
.iter_parent(to_iter
) is not None:
379 to_iter
= model
.iter_parent(to_iter
)
381 from_row
= model
.get_path(from_iter
)[0]
384 if ( position
== gtk
.TREE_VIEW_DROP_BEFORE
or
385 position
== gtk
.TREE_VIEW_DROP_INTO_OR_BEFORE
):
386 model
.move_before( from_iter
, to_iter
)
387 to_row
= to_row
- 1 if from_row
< to_row
else to_row
388 elif ( position
== gtk
.TREE_VIEW_DROP_AFTER
or
389 position
== gtk
.TREE_VIEW_DROP_INTO_OR_AFTER
):
390 model
.move_after( from_iter
, to_iter
)
391 to_row
= to_row
+ 1 if from_row
> to_row
else to_row
393 self
.__log
.debug('Drop not supported: %s', position
)
395 # don't do anything if we're not actually moving rows around
396 if from_row
!= to_row
:
397 player
.playlist
.move_item( from_row
, to_row
)
400 self
.__log
.debug('No drop_data or selection.data available')
402 def update_model(self
):
403 path_info
= self
.treeview
.get_path_at_pos(0,0)
404 path
= path_info
[0] if path_info
is not None else None
406 self
.model
= player
.playlist
.get_bookmark_model()
407 self
.treeview
.set_model(self
.model
)
408 self
.treeview
.expand_all()
411 self
.treeview
.scroll_to_cell(path
)
413 def label_edited(self
, cellrenderer
, path
, new_text
):
414 iter = self
.model
.get_iter(path
)
415 old_text
= self
.model
.get_value(iter, 1)
418 if old_text
!= new_text
:
419 for m
, bkmk_id
, biter
, item_id
, iiter
in self
.__cur
_selection
():
420 if iiter
is not None:
421 self
.model
.set_value(iter, 1, new_text
)
423 self
.model
.set_value(iter, 1, new_text
)
424 player
.playlist
.update_bookmark(
425 item_id
, bkmk_id
, name
=new_text
)
427 self
.model
.set_value(iter, 1, old_text
)
429 def add_bookmark(self
, w
=None, lbl
=None, pos
=None):
430 (label
, position
) = player
.get_formatted_position(pos
)
431 label
= label
if lbl
is None else lbl
432 position
= position
if pos
is None else pos
433 player
.playlist
.save_bookmark( label
, position
)
434 util
.notify(_('Bookmark added: %s') % label
)
437 def add_file(self
, widget
):
438 filename
= get_file_from_filechooser(self
.main
.main_window
)
439 if filename
is not None:
440 player
.playlist
.append(filename
)
442 def add_directory(self
, widget
):
443 directory
= get_file_from_filechooser(
444 self
.main
.main_window
, folder
=True )
445 if directory
is not None:
446 player
.playlist
.load_directory(directory
, append
=True)
448 def __cur_selection(self
):
449 selection
= self
.treeview
.get_selection()
450 model
, bookmark_paths
= selection
.get_selected_rows()
452 # Convert the paths to gtk.TreeRowReference objects, because we
453 # might modify the model while this generator is running
454 bookmark_refs
= [gtk
.TreeRowReference(model
, p
) for p
in bookmark_paths
]
456 for reference
in bookmark_refs
:
457 bookmark_iter
= model
.get_iter(reference
.get_path())
458 item_iter
= model
.iter_parent(bookmark_iter
)
460 # bookmark_iter is actually an item_iter
461 if item_iter
is None:
462 item_iter
= bookmark_iter
463 item_id
= model
.get_value(item_iter
, 0)
464 bookmark_id
, bookmark_iter
= None, None
466 bookmark_id
= model
.get_value(bookmark_iter
, 0)
467 item_id
= model
.get_value(item_iter
, 0)
469 yield model
, bookmark_id
, bookmark_iter
, item_id
, item_iter
471 def remove_bookmark(self
, w
):
472 for model
, bkmk_id
, bkmk_iter
, item_id
, item_iter
in self
.__cur
_selection
():
473 player
.playlist
.remove_bookmark( item_id
, bkmk_id
)
474 if bkmk_iter
is not None:
475 model
.remove(bkmk_iter
)
476 elif item_iter
is not None:
477 model
.remove(item_iter
)
479 def show_playlist_item_details(self
, w
):
480 selection
= self
.treeview
.get_selection()
481 if selection
.count_selected_rows() == 1:
482 selected
= self
.__cur
_selection
().next()
483 model
, bkmk_id
, bkmk_iter
, item_id
, item_iter
= selected
484 playlist_item
= player
.playlist
.get_item_by_id(item_id
)
485 PlaylistItemDetails(self
.main
, playlist_item
)
487 def jump_bookmark(self
, w
):
488 selected
= list(self
.__cur
_selection
())
489 if len(selected
) == 1:
490 # It should be guranteed by the fact that we only enable the
491 # "Jump to" button when the selection count equals 1.
492 model
, bkmk_id
, bkmk_iter
, item_id
, item_iter
= selected
.pop(0)
493 player
.playlist
.load_from_bookmark_id(item_id
, bkmk_id
)
495 # FIXME: The player/playlist should be able to take care of this
496 if not player
.playing
:
499 class GTK_Main(object):
501 def __init__(self
, filename
=None):
502 self
.__log
= logging
.getLogger('panucci.panucci.GTK_Main')
503 interface
.register_gui(self
)
504 self
.pickle_file_conversion()
507 self
.progress_timer_id
= None
508 self
.volume_timer_id
= None
509 self
.anti_blank_timer
= None
511 self
.recent_files
= []
512 self
.make_main_window()
513 self
.has_coverart
= False
514 self
.set_volume(settings
.volume
)
515 self
.last_seekbutton_pressed
= None
516 self
.last_seekbutton_pressed_time
= 0
517 self
.__window
_fullscreen
= False
519 if util
.platform
==util
.MAEMO
and interface
.headset_device
is not None:
520 # Enable play/pause with headset button
521 interface
.headset_device
.connect_to_signal(
522 'Condition', self
.handle_headset_button
)
524 settings
.register( 'volume_changed', self
.set_volume
)
525 settings
.register('allow_blanking_changed',self
.__set
_anti
_blank
_timer
)
526 self
.__set
_anti
_blank
_timer
( settings
.allow_blanking
)
528 player
.register( 'stopped', self
.on_player_stopped
)
529 player
.register( 'playing', self
.on_player_playing
)
530 player
.register( 'paused', self
.on_player_paused
)
531 player
.register( 'end_of_playlist', self
.on_player_end_of_playlist
)
532 player
.playlist
.register('new_track_metadata',self
.on_player_new_track
)
533 player
.playlist
.register( 'file_queued', self
.on_file_queued
)
534 player
.init(filepath
=filename
)
536 def make_main_window(self
):
537 if util
.platform
== util
.MAEMO
:
538 self
.app
= hildon
.Program()
539 window
= hildon
.Window()
540 self
.app
.add_window(window
)
542 window
= gtk
.Window(gtk
.WINDOW_TOPLEVEL
)
544 window
.set_title('Panucci')
545 self
.window_icon
= util
.find_image('panucci.png')
546 if self
.window_icon
is not None:
547 window
.set_icon_from_file( self
.window_icon
)
548 window
.set_default_size(400, -1)
549 window
.set_border_width(0)
550 window
.connect("destroy", self
.destroy
)
551 self
.main_window
= window
553 if util
.platform
== util
.MAEMO
:
554 window
.set_menu(self
.create_menu())
556 menu_vbox
= gtk
.VBox()
557 menu_vbox
.set_spacing(0)
558 window
.add(menu_vbox
)
559 menu_bar
= gtk
.MenuBar()
560 root_menu
= gtk
.MenuItem('Panucci')
561 root_menu
.set_submenu(self
.create_menu())
562 menu_bar
.append(root_menu
)
563 menu_vbox
.pack_start(menu_bar
, False, False, 0)
566 self
.notebook
= gtk
.Notebook()
568 if util
.platform
== util
.MAEMO
:
569 window
.add(self
.notebook
)
571 menu_vbox
.pack_end(self
.notebook
, True, True, 6)
573 main_hbox
= gtk
.HBox()
574 self
.notebook
.append_page(main_hbox
, gtk
.Label(_('Player')))
575 self
.notebook
.set_tab_label_packing(main_hbox
,True,True,gtk
.PACK_START
)
577 main_vbox
= gtk
.VBox()
578 main_vbox
.set_spacing(6)
579 # add a vbox to the main_hbox
580 main_hbox
.pack_start(main_vbox
, True, True)
582 # a hbox to hold the cover art and metadata vbox
583 metadata_hbox
= gtk
.HBox()
584 metadata_hbox
.set_spacing(6)
585 main_vbox
.pack_start(metadata_hbox
, True, False)
587 self
.cover_art
= gtk
.Image()
588 metadata_hbox
.pack_start( self
.cover_art
, False, False )
590 # vbox to hold metadata
591 metadata_vbox
= gtk
.VBox()
592 metadata_vbox
.set_spacing(8)
593 empty_label
= gtk
.Label()
594 metadata_vbox
.pack_start(empty_label
, True, True)
595 self
.artist_label
= gtk
.Label('')
596 self
.artist_label
.set_ellipsize(pango
.ELLIPSIZE_END
)
597 metadata_vbox
.pack_start(self
.artist_label
, False, False)
598 self
.album_label
= gtk
.Label('')
599 self
.album_label
.set_ellipsize(pango
.ELLIPSIZE_END
)
600 metadata_vbox
.pack_start(self
.album_label
, False, False)
601 self
.title_label
= gtk
.Label('')
602 self
.title_label
.set_line_wrap(True)
603 metadata_vbox
.pack_start(self
.title_label
, False, False)
604 empty_label
= gtk
.Label()
605 metadata_vbox
.pack_start(empty_label
, True, True)
606 metadata_hbox
.pack_start( metadata_vbox
, True, True )
608 progress_eventbox
= gtk
.EventBox()
609 progress_eventbox
.set_events(gtk
.gdk
.BUTTON_PRESS_MASK
)
610 progress_eventbox
.connect(
611 'button-press-event', self
.on_progressbar_changed
)
612 self
.progress
= gtk
.ProgressBar()
613 # make the progress bar more "finger-friendly"
614 if util
.platform
== util
.MAEMO
:
615 self
.progress
.set_size_request( -1, 50 )
616 progress_eventbox
.add(self
.progress
)
617 main_vbox
.pack_start( progress_eventbox
, False, False )
619 # make the button box
620 buttonbox
= gtk
.HBox()
622 self
.rrewind_button
= gtk
.Button('')
623 image(self
.rrewind_button
, 'media-skip-backward.png')
624 self
.rrewind_button
.connect( 'pressed', self
.on_seekbutton_pressed
)
625 self
.rrewind_button
.connect(
626 'clicked', self
.on_seekbutton_clicked
, -1*settings
.seek_long
, True )
627 buttonbox
.add(self
.rrewind_button
)
629 self
.rewind_button
= gtk
.Button('')
630 image(self
.rewind_button
, 'media-seek-backward.png')
631 self
.rewind_button
.connect( 'pressed', self
.on_seekbutton_pressed
)
632 self
.rewind_button
.connect(
633 'clicked', self
.on_seekbutton_clicked
, -1*settings
.seek_short
, False)
634 buttonbox
.add(self
.rewind_button
)
636 self
.play_pause_button
= gtk
.Button('')
637 image(self
.play_pause_button
, gtk
.STOCK_OPEN
, True)
638 self
.button_handler_id
= self
.play_pause_button
.connect(
639 'clicked', self
.open_file_callback
)
640 buttonbox
.add(self
.play_pause_button
)
642 self
.forward_button
= gtk
.Button('')
643 image(self
.forward_button
, 'media-seek-forward.png')
644 self
.forward_button
.connect( 'pressed', self
.on_seekbutton_pressed
)
645 self
.forward_button
.connect(
646 'clicked', self
.on_seekbutton_clicked
, settings
.seek_short
, False )
647 buttonbox
.add(self
.forward_button
)
649 self
.fforward_button
= gtk
.Button('')
650 image(self
.fforward_button
, 'media-skip-forward.png')
651 self
.fforward_button
.connect( 'pressed', self
.on_seekbutton_pressed
)
652 self
.fforward_button
.connect(
653 'clicked', self
.on_seekbutton_clicked
, settings
.seek_long
, True )
654 buttonbox
.add(self
.fforward_button
)
656 self
.bookmarks_button
= gtk
.Button('')
657 image(self
.bookmarks_button
, 'bookmark-new.png')
658 buttonbox
.add(self
.bookmarks_button
)
659 self
.set_controls_sensitivity(False)
660 main_vbox
.pack_start(buttonbox
, False, False)
662 self
.playlist_tab
= PlaylistTab(self
)
663 self
.bookmarks_button
.connect('clicked',self
.playlist_tab
.add_bookmark
)
664 self
.notebook
.append_page(self
.playlist_tab
, gtk
.Label(_('Playlist')))
665 self
.notebook
.set_tab_label_packing(
666 self
.playlist_tab
, True, True, gtk
.PACK_START
)
669 self
.notebook
.set_current_page(0)
671 if util
.platform
== util
.MAEMO
:
672 self
.volume
= hildon
.VVolumebar()
673 self
.volume
.set_property('can-focus', False)
674 self
.volume
.connect('level_changed', self
.volume_changed_hildon
)
675 self
.volume
.connect('mute_toggled', self
.mute_toggled
)
676 window
.connect('key-press-event', self
.on_key_press
)
677 main_hbox
.pack_start(self
.volume
, False, True)
679 # Add a button to pop out the volume bar
680 self
.volume_button
= gtk
.ToggleButton('')
681 image(self
.volume_button
, 'media-speaker.png')
682 self
.volume_button
.connect('clicked', self
.toggle_volumebar
)
684 'show', lambda x
: self
.volume_button
.set_active(True))
686 'hide', lambda x
: self
.volume_button
.set_active(False))
687 buttonbox
.add(self
.volume_button
)
688 self
.volume_button
.show()
690 # Disable focus for all widgets, so we can use the cursor
691 # keys + enter to directly control our media player, which
692 # is handled by "key-press-event"
694 self
.rrewind_button
, self
.rewind_button
,
695 self
.play_pause_button
, self
.forward_button
,
696 self
.fforward_button
, self
.progress
,
697 self
.bookmarks_button
, self
.volume_button
, ):
698 w
.unset_flags(gtk
.CAN_FOCUS
)
700 self
.volume
= gtk
.VolumeButton()
701 self
.volume
.connect('value-changed', self
.volume_changed_gtk
)
702 buttonbox
.add(self
.volume
)
705 self
.set_volume(settings
.volume
)
707 def create_menu(self
):
711 menu_open
= gtk
.ImageMenuItem(_('Open playlist'))
713 gtk
.image_new_from_stock(gtk
.STOCK_OPEN
, gtk
.ICON_SIZE_MENU
))
714 menu_open
.connect("activate", self
.open_file_callback
)
715 menu
.append(menu_open
)
717 # the recent files menu
718 self
.menu_recent
= gtk
.MenuItem(_('Open recent playlist'))
719 menu
.append(self
.menu_recent
)
720 self
.create_recent_files_menu()
722 menu
.append(gtk
.SeparatorMenuItem())
724 menu_save
= gtk
.ImageMenuItem(_('Save current playlist'))
726 gtk
.image_new_from_stock(gtk
.STOCK_SAVE_AS
, gtk
.ICON_SIZE_MENU
))
727 menu_save
.connect("activate", self
.save_to_playlist_callback
)
728 menu
.append(menu_save
)
730 menu
.append(gtk
.SeparatorMenuItem())
732 # the settings sub-menu
733 menu_settings
= gtk
.MenuItem(_('Settings'))
734 menu
.append(menu_settings
)
736 menu_settings_sub
= gtk
.Menu()
737 menu_settings
.set_submenu(menu_settings_sub
)
739 menu_settings_disable_skip
= gtk
.CheckMenuItem(
740 _('Disable Delayed Track Skipping') )
741 settings
.attach_checkbutton(
742 menu_settings_disable_skip
, 'disable_delayed_skip' )
743 menu_settings_sub
.append(menu_settings_disable_skip
)
745 menu_settings_lock_progress
= gtk
.CheckMenuItem(_('Lock Progress Bar'))
746 settings
.attach_checkbutton(
747 menu_settings_lock_progress
, 'progress_locked' )
748 menu_settings_sub
.append(menu_settings_lock_progress
)
750 menu_about
= gtk
.ImageMenuItem(gtk
.STOCK_ABOUT
)
751 menu_about
.connect("activate", self
.show_about
, self
.main_window
)
752 menu
.append(menu_about
)
754 menu
.append(gtk
.SeparatorMenuItem())
756 menu_quit
= gtk
.ImageMenuItem(gtk
.STOCK_QUIT
)
757 menu_quit
.connect("activate", self
.destroy
)
758 menu
.append(menu_quit
)
762 def create_recent_files_menu( self
):
763 max_files
= settings
.max_recent_files
764 self
.recent_files
= player
.playlist
.get_recent_files(max_files
)
765 menu_recent_sub
= gtk
.Menu()
767 temp_playlist
= os
.path
.expanduser(settings
.temp_playlist
)
769 if len(self
.recent_files
) > 0:
770 for f
in self
.recent_files
:
771 # don't include the temporary playlist in the file list
772 if f
== temp_playlist
: continue
773 filename
, extension
= os
.path
.splitext(os
.path
.basename(f
))
774 menu_item
= gtk
.MenuItem( filename
.replace('_', ' '))
775 menu_item
.connect('activate', self
.on_recent_file_activate
, f
)
776 menu_recent_sub
.append(menu_item
)
778 menu_item
= gtk
.MenuItem(_('No recent files available.'))
779 menu_item
.set_sensitive(False)
780 menu_recent_sub
.append(menu_item
)
782 self
.menu_recent
.set_submenu(menu_recent_sub
)
784 def __get_fullscreen(self
):
785 return self
.__window
_fullscreen
787 def __set_fullscreen(self
, value
):
788 if value
and not self
.__window
_fullscreen
:
789 self
.main_window
.fullscreen()
790 elif not value
and self
.__window
_fullscreen
:
791 self
.main_window
.unfullscreen()
793 self
.__window
_fullscreen
= value
795 fullscreen
= property( __get_fullscreen
, __set_fullscreen
)
797 def on_recent_file_activate(self
, widget
, filepath
):
798 self
.play_file(filepath
)
800 def show_about(self
, w
, win
):
801 dialog
= gtk
.AboutDialog()
802 dialog
.set_website(about_website
)
803 dialog
.set_website_label(about_website
)
804 dialog
.set_name(about_name
)
805 dialog
.set_authors(about_authors
)
806 dialog
.set_comments(about_text
)
807 dialog
.set_version(app_version
)
811 def destroy(self
, widget
):
815 def handle_headset_button(self
, event
, button
):
816 if event
== 'ButtonPressed' and button
== 'phone':
817 self
.on_btn_play_pause_clicked()
819 def check_queue(self
):
820 """ Makes sure the queue is saved if it has been modified
821 True means a new file can be opened
822 False means the user does not want to continue """
824 if player
.playlist
.queue_modified
:
826 self
.main_window
, _('Save current playlist'),
827 _('Current playlist has been modified'),
828 _('Opening a new file will replace the current playlist. ') +
829 _('Do you want to save it before creating a new one?'),
830 affirmative_button
=gtk
.STOCK_SAVE
,
831 negative_button
=_('Discard changes'))
833 self
.__log
.debug('Response to "Save Queue?": %s', response
)
838 return self
.save_to_playlist_callback()
846 def open_file_callback(self
, widget
=None):
847 if self
.check_queue():
848 filename
= get_file_from_filechooser(self
.main_window
)
849 if filename
is not None:
850 self
._play
_file
(filename
)
852 def save_to_playlist_callback(self
, widget
=None):
853 filename
= get_file_from_filechooser(
854 self
.main_window
, save_file
=True, save_to
='playlist.m3u' )
859 if os
.path
.isfile(filename
):
860 response
= dialog( self
.main_window
, _('File already exists'),
861 _('File already exists'),
862 _('The file %s already exists. You can choose another name or '
863 'overwrite the existing file.') % os
.path
.basename(filename
),
864 affirmative_button
=gtk
.STOCK_SAVE
,
865 negative_button
=_('Rename file'))
873 return self
.save_to_playlist_callback()
875 ext
= util
.detect_filetype(filename
)
876 if not player
.playlist
.save_to_new_playlist(filename
, ext
):
877 util
.notify(_('Error saving playlist...'))
882 def set_controls_sensitivity(self
, sensitive
):
883 self
.forward_button
.set_sensitive(sensitive
)
884 self
.rewind_button
.set_sensitive(sensitive
)
885 self
.fforward_button
.set_sensitive(sensitive
)
886 self
.rrewind_button
.set_sensitive(sensitive
)
888 def on_key_press(self
, widget
, event
):
889 # this stuff is only maemo-related
890 if util
.platform
== util
.MAEMO
:
891 if event
.keyval
== gtk
.keysyms
.F7
: #plus
892 self
.set_volume( min( 1, self
.get_volume() + 0.10 ))
893 elif event
.keyval
== gtk
.keysyms
.F8
: #minus
894 self
.set_volume( max( 0, self
.get_volume() - 0.10 ))
895 elif event
.keyval
== gtk
.keysyms
.Left
: # seek back
896 self
.rewind_callback(self
.rewind_button
)
897 elif event
.keyval
== gtk
.keysyms
.Right
: # seek forward
898 self
.forward_callback(self
.forward_button
)
899 elif event
.keyval
== gtk
.keysyms
.Return
: # play/pause
900 self
.on_btn_play_pause_clicked()
901 elif event
.keyval
== gtk
.keysyms
.F6
:
902 self
.fullscreen
= not self
.fullscreen
904 # The following two functions get and set the
905 # volume from the volume control widgets.
906 def get_volume(self
):
907 if util
.platform
== util
.MAEMO
:
908 return self
.volume
.get_level()/100.0
910 return self
.volume
.get_value()
912 def set_volume(self
, vol
):
913 """ vol is a float from 0 to 1 """
915 if util
.platform
== util
.MAEMO
:
916 self
.volume
.set_level(vol
*100.0)
918 self
.volume
.set_value(vol
)
920 def __set_volume_hide_timer(self
, timeout
, force_show
=False):
921 if force_show
or self
.volume_button
.get_active():
923 if self
.volume_timer_id
is not None:
924 gobject
.source_remove(self
.volume_timer_id
)
925 self
.volume_timer_id
= None
927 self
.volume_timer_id
= gobject
.timeout_add(
928 1000 * timeout
, self
.__volume
_hide
_callback
)
930 def __volume_hide_callback(self
):
931 self
.volume_timer_id
= None
935 def __set_anti_blank_timer(self
, allow_blanking
):
936 if util
.platform
== util
.MAEMO
:
937 if allow_blanking
and self
.anti_blank_timer
is not None:
938 self
.__log
.info('Screen blanking enabled.')
939 gobject
.source_remove(self
.anti_blank_timer
)
940 self
.anti_blank_timer
= None
941 elif not allow_blanking
and self
.anti_blank_timer
is None:
942 self
.__log
.info('Attempting to disable screen blanking.')
943 self
.anti_blank_timer
= gobject
.timeout_add(
944 1000 * 59, util
.poke_backlight
)
946 self
.__log
.info('Blanking controls are for Maemo only.')
948 def toggle_volumebar(self
, widget
=None):
949 if self
.volume_timer_id
is None:
950 self
.__set
_volume
_hide
_timer
(5)
952 self
.__volume
_hide
_callback
()
954 def volume_changed_gtk(self
, widget
, new_value
=0.5):
955 settings
.volume
= new_value
957 def volume_changed_hildon(self
, widget
):
958 self
.__set
_volume
_hide
_timer
( 4, force_show
=True )
959 settings
.volume
= widget
.get_level()/100.0
961 def mute_toggled(self
, widget
):
962 if widget
.get_mute():
965 settings
.volume
= widget
.get_level()/100.0
967 def show_main_window(self
):
968 self
.main_window
.present()
970 def play_file(self
, filename
):
971 if self
.check_queue():
972 self
._play
_file
(filename
)
974 def _play_file(self
, filename
, pause_on_load
=False):
977 player
.playlist
.load( os
.path
.abspath(filename
) )
978 if player
.playlist
.is_empty
:
983 def on_player_stopped(self
):
984 self
.stop_progress_timer()
985 self
.title_label
.set_size_request(-1,-1)
986 self
.reset_progress()
987 self
.set_controls_sensitivity(False)
989 def on_player_playing(self
):
990 self
.start_progress_timer()
991 image(self
.play_pause_button
, 'media-playback-pause.png')
992 self
.set_controls_sensitivity(True)
994 def on_player_new_track(self
, metadata
):
995 image(self
.play_pause_button
, 'media-playback-start.png')
996 self
.play_pause_button
.disconnect(self
.button_handler_id
)
997 self
.button_handler_id
= self
.play_pause_button
.connect(
998 'clicked', self
.on_btn_play_pause_clicked
)
1000 for widget
in [self
.title_label
,self
.artist_label
,self
.album_label
]:
1004 self
.cover_art
.hide()
1005 self
.has_coverart
= False
1006 self
.set_metadata(metadata
)
1008 text
, position
= player
.get_formatted_position()
1009 estimated_length
= metadata
.get('length', 0)
1010 self
.set_progress_callback( position
, estimated_length
)
1012 def on_player_paused(self
):
1013 self
.stop_progress_timer() # This should save some power
1014 image(self
.play_pause_button
, 'media-playback-start.png')
1016 def on_player_end_of_playlist(self
):
1017 self
.play_pause_button
.disconnect(self
.button_handler_id
)
1018 self
.button_handler_id
= self
.play_pause_button
.connect(
1019 'clicked', self
.open_file_callback
)
1020 image(self
.play_pause_button
, gtk
.STOCK_OPEN
, True)
1022 def on_file_queued(self
, filepath
, success
, notify
):
1024 filename
= os
.path
.basename(filepath
)
1027 util
.notify( '%s added successfully.' % filename
))
1030 util
.notify( 'Error adding %s to the queue.' % filename
))
1032 def reset_progress(self
):
1033 self
.progress
.set_fraction(0)
1034 self
.set_progress_callback(0,0)
1036 def set_progress_callback(self
, time_elapsed
, total_time
):
1037 """ times must be in nanoseconds """
1038 time_string
= "%s / %s" % ( util
.convert_ns(time_elapsed
),
1039 util
.convert_ns(total_time
) )
1040 self
.progress
.set_text( time_string
)
1041 fraction
= float(time_elapsed
) / float(total_time
) if total_time
else 0
1042 self
.progress
.set_fraction( fraction
)
1044 def on_progressbar_changed(self
, widget
, event
):
1045 if ( not settings
.progress_locked
and
1046 event
.type == gtk
.gdk
.BUTTON_PRESS
and event
.button
== 1 ):
1047 new_fraction
= event
.x
/float(widget
.get_allocation().width
)
1048 resp
= player
.do_seek(percent
=new_fraction
)
1050 # Preemptively update the progressbar to make seeking smoother
1051 self
.set_progress_callback( *resp
)
1053 def on_btn_play_pause_clicked(self
, widget
=None):
1054 player
.play_pause_toggle()
1056 def progress_timer_callback( self
):
1057 if player
.playing
and not player
.seeking
:
1058 pos_int
, dur_int
= player
.get_position_duration()
1059 # This prevents bogus values from being set while seeking
1060 if ( pos_int
> 10**9 ) and ( dur_int
> 10**9 ):
1061 self
.set_progress_callback( pos_int
, dur_int
)
1064 def start_progress_timer( self
):
1065 if self
.progress_timer_id
is not None:
1066 self
.stop_progress_timer()
1068 self
.progress_timer_id
= gobject
.timeout_add(
1069 1000, self
.progress_timer_callback
)
1071 def stop_progress_timer( self
):
1072 if self
.progress_timer_id
is not None:
1073 gobject
.source_remove( self
.progress_timer_id
)
1074 self
.progress_timer_id
= None
1076 def set_coverart( self
, pixbuf
):
1077 self
.cover_art
.set_from_pixbuf(pixbuf
)
1078 self
.cover_art
.show()
1079 self
.has_coverart
= True
1081 def set_metadata( self
, tag_message
):
1082 tags
= { 'title': self
.title_label
, 'artist': self
.artist_label
,
1083 'album': self
.album_label
}
1085 if tag_message
.has_key('image') and tag_message
['image'] is not None:
1086 value
= tag_message
['image']
1088 pbl
= gtk
.gdk
.PixbufLoader()
1092 pixbuf
= pbl
.get_pixbuf().scale_simple(
1093 coverart_size
[0], coverart_size
[1], gtk
.gdk
.INTERP_BILINEAR
)
1094 self
.set_coverart(pixbuf
)
1095 except Exception, e
:
1096 self
.__log
.exception('Error setting coverart...')
1098 tag_vals
= dict([ (i
,'') for i
in tags
.keys()])
1099 for tag
,value
in tag_message
.iteritems():
1100 if tags
.has_key(tag
) and value
is not None and value
.strip():
1101 tags
[tag
].set_markup('<big>'+value
+'</big>')
1102 tag_vals
[tag
] = value
1103 tags
[tag
].set_alignment( 0.5*int(not self
.has_coverart
), 0.5)
1106 if util
.platform
== util
.MAEMO
:
1107 self
.main_window
.set_title(value
)
1108 # oh man this is hacky :(
1109 if self
.has_coverart
:
1110 tags
[tag
].set_size_request(420,-1)
1111 if len(value
) >= 80: value
= value
[:80] + '...'
1113 self
.main_window
.set_title(value
+ ' - Panucci')
1115 tags
[tag
].set_markup('<b><big>'+value
+'</big></b>')
1117 def on_seekbutton_pressed(self
, widget
):
1118 self
.last_seekbutton_pressed
= widget
1119 self
.last_seekbutton_pressed_time
= time
.time()
1120 self
.__log
.debug( 'Seekbutton %s pressed at %f',
1121 hash(widget
), self
.last_seekbutton_pressed_time
)
1123 def on_seekbutton_clicked(self
, widget
, seek_amount
, long_seek
):
1124 time_delta
= time
.time() - self
.last_seekbutton_pressed_time
1125 self
.__log
.debug('Seekbutton %s released, delta t = %f',
1126 hash(widget
), time_delta
)
1128 if ( long_seek
and not settings
.disable_delayed_skip
and
1129 widget
== self
.last_seekbutton_pressed
and
1130 time_delta
> settings
.skip_delay
):
1133 player
.playlist
.next()
1135 player
.playlist
.prev()
1137 resp
= player
.do_seek(from_current
=seek_amount
*10**9)
1139 # Preemptively update the progressbar to make seeking smoother
1140 self
.set_progress_callback( *resp
)
1142 def pickle_file_conversion(self
):
1143 pickle_file
= os
.path
.expanduser('~/.rmp-bookmarks')
1144 if os
.path
.isfile(pickle_file
):
1145 import pickle_converter
1148 util
.notify( _('Converting old pickle format to SQLite.') ))
1149 self
.__log
.info( util
.notify( _('This may take a while...') ))
1151 if pickle_converter
.load_pickle_file(pickle_file
):
1153 util
.notify( _('Pickle file converted successfully.') ))
1155 self
.__log
.error( util
.notify(
1156 _('Error converting pickle file, check your log...') ))
1158 def run(filename
=None):
1159 GTK_Main( filename
)
1162 if __name__
== '__main__':
1163 log
.error( 'WARNING: Use the "panucci" executable to run this program.' )
1164 log
.error( 'Exiting...' )