Details dialog for PlaylistItem objects
[panucci.git] / src / panucci / panucci.py
blob54b0e9bd75457c85206eeb4495be3381b4f58b30
1 #!/usr/bin/env python
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)
26 import logging
27 import sys
28 import os, os.path
29 import time
31 import gtk
32 import gobject
33 import pango
35 # At the moment, we don't have gettext support, so
36 # make a dummy "_" function to passthrough the string
37 _ = lambda s: s
39 log = logging.getLogger('panucci.panucci')
41 import util
43 try:
44 import hildon
45 except:
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/'
59 app_version = ''
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())
71 image = None
72 if is_stock:
73 image = gtk.image_new_from_stock(
74 filename, gtk.icon_size_from_name('panucci-button') )
75 else:
76 filename = util.find_image(filename)
77 if filename is not None:
78 image = gtk.image_new_from_file(filename)
80 if image is not None:
81 if util.platform == util.MAEMO:
82 image.set_padding(20, 20)
83 else:
84 image.set_padding(5, 5)
85 widget.add(image)
86 image.show()
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 )
109 dlg.set_title(title)
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)
120 response = dlg.run()
121 dlg.destroy()
123 if response == gtk.RESPONSE_YES:
124 return True
125 elif response == gtk.RESPONSE_NO:
126 return False
127 elif response in [gtk.RESPONSE_CANCEL, gtk.RESPONSE_DELETE_EVENT]:
128 return None
130 def get_file_from_filechooser(
131 toplevel_window, folder=False, save_file=False, save_to=None):
133 if folder:
134 open_action = gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER
135 else:
136 open_action = gtk.FILE_CHOOSER_ACTION_OPEN
138 if util.platform == util.MAEMO:
139 if save_file:
140 args = ( toplevel_window, gtk.FILE_CHOOSER_ACTION_SAVE )
141 else:
142 args = ( toplevel_window, open_action )
144 dlg = hildon.FileChooserDialog( *args )
145 else:
146 if save_file:
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 )) )
151 else:
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()
169 else:
170 filename = None
172 dlg.destroy()
173 return filename
175 def set_stock_button_text( button, text ):
176 alignment = button.get_child()
177 hbox = alignment.get_child()
178 image, label = hbox.get_children()
179 label.set_text(text)
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))
184 self.main = main
185 self.fill(playlist_item)
186 self.set_has_separator(False)
187 self.set_resizable(False)
188 self.show_all()
189 self.run()
190 self.destroy()
192 def fill(self, playlist_item):
193 t = gtk.Table(10, 2)
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)
203 row_num = 4
204 for key in metadata:
205 if metadata[key] is not None:
206 t.attach(gtk.Label(key.capitalize()+':'), 0, 1, row_num, row_num+1)
207 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)
217 row_num = 4
218 for key in metadata:
219 value = metadata[key]
220 if key == 'length':
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)
224 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)
234 t = gtk.TreeView(l)
235 cr = gtk.CellRendererText()
236 cr.set_property('ellipsize', pango.ELLIPSIZE_END)
237 c = gtk.TreeViewColumn(_('Title'), cr, text=0)
238 c.set_expand(True)
239 t.append_column(c)
240 c = gtk.TreeViewColumn(_('Time'), gtk.CellRendererText(), text=1)
241 t.append_column(c)
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)
249 sw.add(t)
250 sw.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
251 e = gtk.Expander(_('Bookmarks'))
252 e.add(sw)
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
261 self.set_spacing(5)
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)
271 self.update_model()
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)
292 treeview_targets = [
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)
305 self.add(sw)
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() )
341 self.show_all()
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)
359 else:
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]
382 to_row = path[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
392 else:
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 )
399 else:
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()
410 if path is not None:
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)
417 if new_text.strip():
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)
422 else:
423 self.model.set_value(iter, 1, new_text)
424 player.playlist.update_bookmark(
425 item_id, bkmk_id, name=new_text )
426 else:
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)
435 self.update_model()
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
465 else:
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:
497 player.play()
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()
506 # Timers
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)
541 else:
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())
555 else:
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)
564 menu_bar.show()
566 self.notebook = gtk.Notebook()
568 if util.platform == util.MAEMO:
569 window.add(self.notebook)
570 else:
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 )
668 window.show_all()
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)
683 self.volume.connect(
684 'show', lambda x: self.volume_button.set_active(True))
685 self.volume.connect(
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"
693 for w in (
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)
699 else:
700 self.volume = gtk.VolumeButton()
701 self.volume.connect('value-changed', self.volume_changed_gtk)
702 buttonbox.add(self.volume)
703 self.volume.show()
705 self.set_volume(settings.volume)
707 def create_menu(self):
708 # the main menu
709 menu = gtk.Menu()
711 menu_open = gtk.ImageMenuItem(_('Open playlist'))
712 menu_open.set_image(
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'))
725 menu_save.set_image(
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)
760 return menu
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)
777 else:
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)
808 dialog.run()
809 dialog.destroy()
811 def destroy(self, widget):
812 player.quit()
813 gtk.main_quit()
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:
825 response = dialog(
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)
835 if response is None:
836 return False
837 elif response:
838 return self.save_to_playlist_callback()
839 elif not response:
840 return True
841 else:
842 return False
843 else:
844 return True
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' )
856 if filename is None:
857 return False
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'))
867 if response is None:
868 return None
870 elif response:
871 pass
872 elif not response:
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...'))
878 return False
880 return True
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
909 else:
910 return self.volume.get_value()
912 def set_volume(self, vol):
913 """ vol is a float from 0 to 1 """
914 assert 0 <= vol <= 1
915 if util.platform == util.MAEMO:
916 self.volume.set_level(vol*100.0)
917 else:
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():
922 self.volume.show()
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
932 self.volume.hide()
933 return False
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 )
945 else:
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)
951 else:
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():
963 settings.volume = 0
964 else:
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):
975 player.stop()
977 player.playlist.load( os.path.abspath(filename) )
978 if player.playlist.is_empty:
979 return False
981 player.play()
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]:
1001 widget.set_text('')
1002 widget.hide()
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):
1023 if notify:
1024 filename = os.path.basename(filepath)
1025 if success:
1026 self.__log.info(
1027 util.notify( '%s added successfully.' % filename ))
1028 else:
1029 self.__log.error(
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)
1049 if resp:
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 )
1062 return True
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()
1089 try:
1090 pbl.write(value)
1091 pbl.close()
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)
1104 tags[tag].show()
1105 if tag == 'title':
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] + '...'
1112 else:
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 ):
1132 if seek_amount > 0:
1133 player.playlist.next()
1134 else:
1135 player.playlist.prev()
1136 else:
1137 resp = player.do_seek(from_current=seek_amount*10**9)
1138 if resp:
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
1147 self.__log.info(
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):
1152 self.__log.info(
1153 util.notify( _('Pickle file converted successfully.') ))
1154 else:
1155 self.__log.error( util.notify(
1156 _('Error converting pickle file, check your log...') ))
1158 def run(filename=None):
1159 GTK_Main( filename )
1160 gtk.main()
1162 if __name__ == '__main__':
1163 log.error( 'WARNING: Use the "panucci" executable to run this program.' )
1164 log.error( 'Exiting...' )
1165 sys.exit(1)