Only hide playlist when closing it
[panucci.git] / src / panucci / main.py
blobc16915aaee5b462e233239a7bb3c504c809d3ae3
1 #!/usr/bin/env python
3 # This file is part of Panucci.
4 # Copyright (c) 2008-2010 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)
25 from __future__ import absolute_import
28 import logging
29 import sys
30 import os, os.path
31 import time
33 import gtk
34 import gobject
35 import pango
37 from panucci import widgets
38 from panucci import util
39 from panucci import ossohelper
41 log = logging.getLogger('panucci.panucci')
44 try:
45 import hildon
46 except:
47 if util.platform.MAEMO:
48 log.critical( 'Using GTK widgets, install "python2.5-hildon" '
49 'for this to work properly.' )
51 from panucci.simplegconf import gconf
52 from panucci.settings import settings
53 from panucci.player import player
54 from panucci.dbusinterface import interface
55 from panucci.services import ObservableService
57 about_name = 'Panucci'
58 about_text = _('Resuming audiobook and podcast player')
59 about_authors = ['Thomas Perl', 'Nick (nikosapi)', 'Matthew Taylor']
60 about_website = 'http://panucci.garage.maemo.org/'
61 app_version = ''
63 coverart_sizes = {
64 'normal' : 110,
65 'maemo' : 200,
66 'maemo fullscreen' : 275,
69 gtk.about_dialog_set_url_hook(util.open_link, None)
70 gtk.icon_size_register('panucci-button', 32, 32)
72 def generate_image(filename, is_stock=False):
73 image = None
74 if is_stock:
75 image = gtk.image_new_from_stock(
76 filename, gtk.icon_size_from_name('panucci-button') )
77 else:
78 filename = util.find_image(filename)
79 if filename is not None:
80 image = gtk.image_new_from_file(filename)
81 if image is not None:
82 if util.platform.MAEMO:
83 image.set_padding(20, 20)
84 else:
85 image.set_padding(5, 5)
86 image.show()
87 return image
89 def image(widget, filename, is_stock=False):
90 child = widget.get_child()
91 if child is not None:
92 widget.remove(child)
93 image = generate_image(filename, is_stock)
94 if image is not None:
95 widget.add(image)
97 def dialog( toplevel_window, title, question, description,
98 affirmative_button=gtk.STOCK_YES, negative_button=gtk.STOCK_NO,
99 abortion_button=gtk.STOCK_CANCEL ):
101 """Present the user with a yes/no/cancel dialog.
102 The return value is either True, False or None, depending on which
103 button has been pressed in the dialog:
105 affirmative button (default: Yes) => True
106 negative button (defaut: No) => False
107 abortion button (default: Cancel) => None
109 When the dialog is closed with the "X" button in the window manager
110 decoration, the return value is always None (same as abortion button).
112 You can set any of the affirmative_button, negative_button or
113 abortion_button values to "None" to hide the corresponding action.
115 dlg = gtk.MessageDialog( toplevel_window, gtk.DIALOG_MODAL,
116 gtk.MESSAGE_QUESTION, message_format=question )
118 dlg.set_title(title)
120 if abortion_button is not None:
121 dlg.add_button(abortion_button, gtk.RESPONSE_CANCEL)
122 if negative_button is not None:
123 dlg.add_button(negative_button, gtk.RESPONSE_NO)
124 if affirmative_button is not None:
125 dlg.add_button(affirmative_button, gtk.RESPONSE_YES)
127 dlg.format_secondary_text(description)
129 response = dlg.run()
130 dlg.destroy()
132 if response == gtk.RESPONSE_YES:
133 return True
134 elif response == gtk.RESPONSE_NO:
135 return False
136 elif response in [gtk.RESPONSE_CANCEL, gtk.RESPONSE_DELETE_EVENT]:
137 return None
139 def get_file_from_filechooser(
140 toplevel_window, folder=False, save_file=False, save_to=None):
142 if folder:
143 open_action = gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER
144 else:
145 open_action = gtk.FILE_CHOOSER_ACTION_OPEN
147 if util.platform.MAEMO:
148 if save_file:
149 args = ( toplevel_window, gtk.FILE_CHOOSER_ACTION_SAVE )
150 else:
151 args = ( toplevel_window, open_action )
153 dlg = hildon.FileChooserDialog( *args )
154 else:
155 if save_file:
156 args = ( _('Select file to save playlist to'), None,
157 gtk.FILE_CHOOSER_ACTION_SAVE,
158 (( gtk.STOCK_CANCEL, gtk.RESPONSE_REJECT,
159 gtk.STOCK_SAVE, gtk.RESPONSE_OK )) )
160 else:
161 args = ( _('Select podcast or audiobook'), None, open_action,
162 (( gtk.STOCK_CANCEL, gtk.RESPONSE_REJECT,
163 gtk.STOCK_OPEN, gtk.RESPONSE_OK )) )
165 dlg = gtk.FileChooserDialog(*args)
167 current_folder = os.path.expanduser(settings.last_folder)
169 if current_folder is not None and os.path.isdir(current_folder):
170 dlg.set_current_folder(current_folder)
172 if save_file and save_to is not None:
173 dlg.set_current_name(save_to)
175 if dlg.run() == gtk.RESPONSE_OK:
176 filename = dlg.get_filename()
177 settings.last_folder = dlg.get_current_folder()
178 else:
179 filename = None
181 dlg.destroy()
182 return filename
184 def set_stock_button_text( button, text ):
185 alignment = button.get_child()
186 hbox = alignment.get_child()
187 image, label = hbox.get_children()
188 label.set_text(text)
190 ##################################################
191 # PanucciGUI
192 ##################################################
193 class PanucciGUI(object):
194 """ The object that holds the entire panucci gui """
196 def __init__(self, filename=None):
197 self.__log = logging.getLogger('panucci.panucci.PanucciGUI')
198 interface.register_gui(self)
200 # Build the base ui (window and menubar)
201 if util.platform.MAEMO:
202 self.app = hildon.Program()
203 window = hildon.Window()
204 self.app.add_window(window)
205 else:
206 window = gtk.Window(gtk.WINDOW_TOPLEVEL)
208 self.main_window = window
209 window.set_title('Panucci')
210 self.window_icon = util.find_image('panucci.png')
211 if self.window_icon is not None:
212 window.set_icon_from_file( self.window_icon )
213 window.set_default_size(400, -1)
214 window.set_border_width(0)
215 window.connect("destroy", self.destroy)
217 # Add the tabs (they are private to prevent us from trying to do
218 # something like gui_root.player_tab.some_function() from inside
219 # playlist_tab or vice-versa)
220 self.__player_tab = PlayerTab(self)
221 self.__playlist_tab = PlaylistTab(self)
223 self.playlist_window = gtk.Window(gtk.WINDOW_TOPLEVEL)
224 self.playlist_window.connect('delete-event', gtk.Widget.hide_on_delete)
225 self.playlist_window.set_title(_('Panucci playlist'))
226 self.playlist_window.set_transient_for(self.main_window)
227 self.playlist_window.add(self.__playlist_tab)
229 if util.platform.MAEMO:
230 window.set_menu(self.create_menu())
231 window.add(self.__player_tab)
232 else:
233 menu_vbox = gtk.VBox()
234 menu_vbox.set_spacing(0)
235 window.add(menu_vbox)
236 menu_bar = gtk.MenuBar()
237 root_menu = gtk.MenuItem('Panucci')
238 root_menu.set_submenu(self.create_menu())
239 menu_bar.append(root_menu)
240 menu_vbox.pack_start(menu_bar, False, False, 0)
241 menu_bar.show()
242 menu_vbox.pack_end(self.__player_tab, True, True, 6)
244 # Tie it all together!
245 self.__ignore_queue_check = False
246 self.__window_fullscreen = False
248 if util.platform.MAEMO and interface.headset_device is not None:
249 # Enable play/pause with headset button
250 interface.headset_device.connect_to_signal(
251 'Condition', self.handle_headset_button )
253 self.main_window.connect('key-press-event', self.on_key_press)
254 player.playlist.register( 'file_queued', self.on_file_queued )
256 self.__anti_blank_timer = None
257 settings.register('allow_blanking_changed',self.__set_anti_blank_timer)
258 self.__set_anti_blank_timer( settings.allow_blanking )
260 player.playlist.register( 'playlist-to-be-overwritten',
261 self.check_queue )
262 self.__player_tab.register( 'select-current-item-request',
263 self.__select_current_item )
265 self.main_window.show_all()
267 # this should be done when the gui is ready
268 self.pickle_file_conversion()
269 player.init(filepath=filename)
271 def create_menu(self):
272 # the main menu
273 menu = gtk.Menu()
275 menu_open = gtk.ImageMenuItem(_('Open playlist'))
276 menu_open.set_image(
277 gtk.image_new_from_stock(gtk.STOCK_OPEN, gtk.ICON_SIZE_MENU))
278 menu_open.connect("activate", self.open_file_callback)
279 menu.append(menu_open)
281 # the recent files menu
282 self.menu_recent = gtk.MenuItem(_('Open recent playlist'))
283 menu.append(self.menu_recent)
284 self.create_recent_files_menu()
286 menu.append(gtk.SeparatorMenuItem())
288 menu_save = gtk.ImageMenuItem(_('Save current playlist'))
289 menu_save.set_image(
290 gtk.image_new_from_stock(gtk.STOCK_SAVE_AS, gtk.ICON_SIZE_MENU))
291 menu_save.connect("activate", self.save_to_playlist_callback)
292 menu.append(menu_save)
294 menu.append(gtk.SeparatorMenuItem())
296 # the settings sub-menu
297 menu_settings = gtk.MenuItem(_('Settings'))
298 menu.append(menu_settings)
300 menu_settings_sub = gtk.Menu()
301 menu_settings.set_submenu(menu_settings_sub)
303 menu_settings_enable_dual_action = gtk.CheckMenuItem(
304 _('Enable dual-action buttons') )
305 settings.attach_checkbutton( menu_settings_enable_dual_action,
306 'enable_dual_action_btn' )
307 menu_settings_sub.append(menu_settings_enable_dual_action)
309 if util.platform.MAEMO:
310 menu_settings_enable_hw_decoding = gtk.CheckMenuItem(
311 _('Enable hardware decoding') )
312 settings.attach_checkbutton( menu_settings_enable_hw_decoding,
313 'enable_hardware_decoding' )
314 menu_settings_sub.append(menu_settings_enable_hw_decoding)
316 menu_settings_lock_progress = gtk.CheckMenuItem(_('Lock Progress Bar'))
317 settings.attach_checkbutton( menu_settings_lock_progress,
318 'progress_locked' )
319 menu_settings_sub.append(menu_settings_lock_progress)
321 menu_about = gtk.ImageMenuItem(gtk.STOCK_ABOUT)
322 menu_about.connect("activate", self.show_about, self.main_window)
323 menu.append(menu_about)
325 menu.append(gtk.SeparatorMenuItem())
327 menu_quit = gtk.ImageMenuItem(gtk.STOCK_QUIT)
328 menu_quit.connect("activate", self.destroy)
329 menu.append(menu_quit)
331 return menu
333 def create_recent_files_menu( self ):
334 max_files = settings.max_recent_files
335 self.recent_files = player.playlist.get_recent_files(max_files)
336 menu_recent_sub = gtk.Menu()
338 temp_playlist = os.path.expanduser(settings.temp_playlist)
340 if len(self.recent_files) > 0:
341 for f in self.recent_files:
342 # don't include the temporary playlist in the file list
343 if f == temp_playlist: continue
344 # don't include non-existant files
345 if not os.path.exists( f ): continue
346 filename, extension = os.path.splitext(os.path.basename(f))
347 menu_item = gtk.MenuItem( filename.replace('_', ' '))
348 menu_item.connect('activate', self.on_recent_file_activate, f)
349 menu_recent_sub.append(menu_item)
350 else:
351 menu_item = gtk.MenuItem(_('No recent files available.'))
352 menu_item.set_sensitive(False)
353 menu_recent_sub.append(menu_item)
355 self.menu_recent.set_submenu(menu_recent_sub)
357 def destroy(self, widget):
358 player.quit()
359 gtk.main_quit()
361 def show_main_window(self):
362 self.main_window.present()
364 def check_queue(self):
365 """ Makes sure the queue is saved if it has been modified
366 True means a new file can be opened
367 False means the user does not want to continue """
369 if not self.__ignore_queue_check and player.playlist.queue_modified:
370 response = dialog(
371 self.main_window, _('Save current playlist'),
372 _('Current playlist has been modified'),
373 _('Opening a new file will replace the current playlist. ') +
374 _('Do you want to save it before creating a new one?'),
375 affirmative_button=gtk.STOCK_SAVE,
376 negative_button=_('Discard changes'))
378 self.__log.debug('Response to "Save Queue?": %s', response)
380 if response is None:
381 return False
382 elif response:
383 return self.save_to_playlist_callback()
384 elif not response:
385 return True
386 else:
387 return False
388 else:
389 return True
391 def open_file_callback(self, widget=None):
392 if self.check_queue():
393 # set __ingnore__queue_check because we already did the check
394 self.__ignore_queue_check = True
395 filename = get_file_from_filechooser(self.main_window)
396 if filename is not None:
397 self._play_file(filename)
399 self.__ignore_queue_check = False
401 def save_to_playlist_callback(self, widget=None):
402 filename = get_file_from_filechooser(
403 self.main_window, save_file=True, save_to='playlist.m3u' )
405 if filename is None:
406 return False
408 if os.path.isfile(filename):
409 response = dialog( self.main_window, _('File already exists'),
410 _('File already exists'),
411 _('The file %s already exists. You can choose another name or '
412 'overwrite the existing file.') % os.path.basename(filename),
413 affirmative_button=gtk.STOCK_SAVE,
414 negative_button=_('Rename file'))
416 if response is None:
417 return None
419 elif response:
420 pass
421 elif not response:
422 return self.save_to_playlist_callback()
424 ext = util.detect_filetype(filename)
425 if not player.playlist.save_to_new_playlist(filename, ext):
426 util.notify(_('Error saving playlist...'))
427 return False
429 return True
431 def __get_fullscreen(self):
432 return self.__window_fullscreen
434 def __set_fullscreen(self, value):
435 if value != self.__window_fullscreen:
436 if value:
437 self.main_window.fullscreen()
438 else:
439 self.main_window.unfullscreen()
441 self.__window_fullscreen = value
442 player.playlist.send_metadata()
444 fullscreen = property( __get_fullscreen, __set_fullscreen )
446 def on_key_press(self, widget, event):
447 if util.platform.MAEMO:
448 if event.keyval == gtk.keysyms.F6:
449 self.fullscreen = not self.fullscreen
451 def on_recent_file_activate(self, widget, filepath):
452 self._play_file(filepath)
454 def on_file_queued(self, filepath, success, notify):
455 if notify:
456 filename = os.path.basename(filepath)
457 if success:
458 self.__log.info(
459 util.notify( '%s added successfully.' % filename ))
460 else:
461 self.__log.error(
462 util.notify( 'Error adding %s to the queue.' % filename))
464 def show_about(self, w, win):
465 dialog = gtk.AboutDialog()
466 dialog.set_website(about_website)
467 dialog.set_website_label(about_website)
468 dialog.set_name(about_name)
469 dialog.set_authors(about_authors)
470 dialog.set_comments(about_text)
471 dialog.set_version(app_version)
472 dialog.run()
473 dialog.destroy()
475 def _play_file(self, filename, pause_on_load=False):
476 player.playlist.load( os.path.abspath(filename) )
478 if player.playlist.is_empty:
479 return False
481 def handle_headset_button(self, event, button):
482 if event == 'ButtonPressed' and button == 'phone':
483 player.play_pause_toggle()
485 def __set_anti_blank_timer(self, allow_blanking):
486 if util.platform.MAEMO:
487 if allow_blanking and self.__anti_blank_timer is not None:
488 self.__log.info('Screen blanking enabled.')
489 gobject.source_remove(self.__anti_blank_timer)
490 self.__anti_blank_timer = None
491 elif not allow_blanking and self.__anti_blank_timer is None:
492 self.__log.info('Attempting to disable screen blanking.')
493 self.__anti_blank_timer = gobject.timeout_add(
494 1000 * 59, util.poke_backlight )
495 else:
496 self.__log.info('Blanking controls are for Maemo only.')
498 def __select_current_item( self ):
499 # Select the currently playing track in the playlist tab
500 # and switch to it (so we can edit bookmarks, etc.. there)
501 self.__playlist_tab.select_current_item()
502 self.playlist_window.show()
504 def pickle_file_conversion(self):
505 pickle_file = os.path.expanduser('~/.rmp-bookmarks')
506 if os.path.isfile(pickle_file):
507 import pickle_converter
509 self.__log.info(
510 util.notify( _('Converting old pickle format to SQLite.') ))
511 self.__log.info( util.notify( _('This may take a while...') ))
513 if pickle_converter.load_pickle_file(pickle_file):
514 self.__log.info(
515 util.notify( _('Pickle file converted successfully.') ))
516 else:
517 self.__log.error( util.notify(
518 _('Error converting pickle file, check your log...') ))
520 ##################################################
521 # PlayerTab
522 ##################################################
523 class PlayerTab(ObservableService, gtk.HBox):
524 """ The tab that holds the player elements """
526 signals = [ 'select-current-item-request', ]
528 def __init__(self, gui_root):
529 self.__log = logging.getLogger('panucci.panucci.PlayerTab')
530 self.__gui_root = gui_root
532 gtk.HBox.__init__(self)
533 ObservableService.__init__(self, self.signals, self.__log)
535 # Timers
536 self.progress_timer_id = None
537 self.volume_timer_id = None
539 self.recent_files = []
540 self.make_player_tab()
541 self.has_coverart = False
542 self.set_volume(settings.volume)
544 settings.register( 'enable_dual_action_btn_changed',
545 self.on_dual_action_setting_changed )
546 settings.register( 'dual_action_button_delay_changed',
547 self.on_dual_action_setting_changed )
548 settings.register( 'volume_changed', self.set_volume )
549 settings.register( 'scrolling_labels_changed', lambda v:
550 setattr( self.title_label, 'scrolling', v ) )
552 player.register( 'stopped', self.on_player_stopped )
553 player.register( 'playing', self.on_player_playing )
554 player.register( 'paused', self.on_player_paused )
555 player.playlist.register( 'end-of-playlist',
556 self.on_player_end_of_playlist )
557 player.playlist.register( 'new-track-loaded',
558 self.on_player_new_track )
559 player.playlist.register( 'new-metadata-available',
560 self.on_player_new_metadata )
562 def make_player_tab(self):
563 main_vbox = gtk.VBox()
564 main_vbox.set_spacing(6)
565 # add a vbox to self
566 self.pack_start(main_vbox, True, True)
568 # a hbox to hold the cover art and metadata vbox
569 metadata_hbox = gtk.HBox()
570 metadata_hbox.set_spacing(6)
571 main_vbox.pack_start(metadata_hbox, True, False)
573 self.cover_art = gtk.Image()
574 metadata_hbox.pack_start( self.cover_art, False, False )
576 # vbox to hold metadata
577 metadata_vbox = gtk.VBox()
578 metadata_vbox.set_spacing(8)
579 empty_label = gtk.Label()
580 metadata_vbox.pack_start(empty_label, True, True)
581 self.artist_label = gtk.Label('')
582 self.artist_label.set_ellipsize(pango.ELLIPSIZE_END)
583 metadata_vbox.pack_start(self.artist_label, False, False)
584 self.album_label = gtk.Label('')
585 self.album_label.set_ellipsize(pango.ELLIPSIZE_END)
586 metadata_vbox.pack_start(self.album_label, False, False)
587 self.title_label = widgets.ScrollingLabel( '',
588 update_interval=200,
589 pixel_jump=5,
590 delay_btwn_scrolls=5000,
591 delay_halfway=3000 )
592 self.title_label.scrolling = settings.scrolling_labels
593 metadata_vbox.pack_start(self.title_label, False, False)
594 empty_label = gtk.Label()
595 metadata_vbox.pack_start(empty_label, True, True)
596 metadata_hbox.pack_start( metadata_vbox, True, True )
598 progress_eventbox = gtk.EventBox()
599 progress_eventbox.set_events(gtk.gdk.BUTTON_PRESS_MASK)
600 progress_eventbox.connect(
601 'button-press-event', self.on_progressbar_changed )
602 self.progress = gtk.ProgressBar()
603 # make the progress bar more "finger-friendly"
604 if util.platform.MAEMO:
605 self.progress.set_size_request( -1, 50 )
606 progress_eventbox.add(self.progress)
607 main_vbox.pack_start( progress_eventbox, False, False )
609 # make the button box
610 buttonbox = gtk.HBox()
612 # A wrapper to help create DualActionButtons with the right settings
613 create_da = lambda a, b, c=None, d=None: widgets.DualActionButton(
614 a, b, c, d, settings.dual_action_button_delay,
615 settings.enable_dual_action_btn )
617 self.rrewind_button = create_da(
618 generate_image('media-skip-backward.png'),
619 lambda: self.do_seek(-1*settings.seek_long),
620 generate_image(gtk.STOCK_GOTO_FIRST, True),
621 player.playlist.prev)
622 buttonbox.add(self.rrewind_button)
624 self.rewind_button = create_da(
625 generate_image('media-seek-backward.png'),
626 lambda: self.do_seek(-1*settings.seek_short))
627 buttonbox.add(self.rewind_button)
629 self.play_pause_button = gtk.Button('')
630 image(self.play_pause_button, 'media-playback-start.png')
631 self.play_pause_button.connect( 'clicked',
632 self.on_btn_play_pause_clicked )
633 self.play_pause_button.set_sensitive(False)
634 buttonbox.add(self.play_pause_button)
636 self.forward_button = create_da(
637 generate_image('media-seek-forward.png'),
638 lambda: self.do_seek(settings.seek_short))
639 buttonbox.add(self.forward_button)
641 self.fforward_button = create_da(
642 generate_image('media-skip-forward.png'),
643 lambda: self.do_seek(settings.seek_long),
644 generate_image(gtk.STOCK_GOTO_LAST, True),
645 player.playlist.next)
646 buttonbox.add(self.fforward_button)
648 self.bookmarks_button = create_da(
649 generate_image('bookmark-new.png'),
650 player.add_bookmark_at_current_position,
651 generate_image(gtk.STOCK_JUMP_TO, True),
652 lambda *args: self.notify('select-current-item-request'))
653 buttonbox.add(self.bookmarks_button)
654 self.set_controls_sensitivity(False)
655 main_vbox.pack_start(buttonbox, False, False)
657 if util.platform.MAEMO:
658 self.volume = hildon.VVolumebar()
659 self.volume.set_property('can-focus', False)
660 self.volume.connect('level_changed', self.volume_changed_hildon)
661 self.volume.connect('mute_toggled', self.mute_toggled)
662 self.__gui_root.main_window.connect( 'key-press-event',
663 self.on_key_press )
664 self.pack_start(self.volume, False, True)
666 # Add a button to pop out the volume bar
667 self.volume_button = gtk.ToggleButton('')
668 image(self.volume_button, 'media-speaker.png')
669 self.volume_button.connect('clicked', self.toggle_volumebar)
670 self.volume.connect(
671 'show', lambda x: self.volume_button.set_active(True))
672 self.volume.connect(
673 'hide', lambda x: self.volume_button.set_active(False))
674 buttonbox.add(self.volume_button)
675 self.volume_button.show()
677 # Disable focus for all widgets, so we can use the cursor
678 # keys + enter to directly control our media player, which
679 # is handled by "key-press-event"
680 for w in (
681 self.rrewind_button, self.rewind_button,
682 self.play_pause_button, self.forward_button,
683 self.fforward_button, self.progress,
684 self.bookmarks_button, self.volume_button, ):
685 w.unset_flags(gtk.CAN_FOCUS)
686 else:
687 self.volume = gtk.VolumeButton()
688 self.volume.connect('value-changed', self.volume_changed_gtk)
689 buttonbox.add(self.volume)
690 self.volume.show()
692 self.set_volume(settings.volume)
694 def set_controls_sensitivity(self, sensitive):
695 for button in self.forward_button, self.rewind_button, \
696 self.fforward_button, self.rrewind_button:
698 button.set_sensitive(sensitive)
700 # the play/pause button should always be available except
701 # for when the player starts without a file
702 self.play_pause_button.set_sensitive(True)
704 def on_dual_action_setting_changed( self, *args ):
705 for button in self.forward_button, self.rewind_button, \
706 self.fforward_button, self.rrewind_button, \
707 self.bookmarks_button:
709 button.set_longpress_enabled( settings.enable_dual_action_btn )
710 button.set_duration( settings.dual_action_button_delay )
712 def on_key_press(self, widget, event):
713 if util.platform.MAEMO:
714 if event.keyval == gtk.keysyms.F7: #plus
715 self.set_volume( min( 1, self.get_volume() + 0.10 ))
716 elif event.keyval == gtk.keysyms.F8: #minus
717 self.set_volume( max( 0, self.get_volume() - 0.10 ))
718 elif event.keyval == gtk.keysyms.Left: # seek back
719 self.do_seek( -1 * settings.seek_long )
720 elif event.keyval == gtk.keysyms.Right: # seek forward
721 self.do_seek( settings.seek_long )
722 elif event.keyval == gtk.keysyms.Return: # play/pause
723 self.on_btn_play_pause_clicked()
725 # The following two functions get and set the
726 # volume from the volume control widgets.
727 def get_volume(self):
728 if util.platform.MAEMO:
729 return self.volume.get_level()/100.0
730 else:
731 return self.volume.get_value()
733 def set_volume(self, vol):
734 """ vol is a float from 0 to 1 """
735 assert 0 <= vol <= 1
736 if util.platform.MAEMO:
737 self.volume.set_level(vol*100.0)
738 else:
739 self.volume.set_value(vol)
741 def __set_volume_hide_timer(self, timeout, force_show=False):
742 if force_show or self.volume_button.get_active():
743 self.volume.show()
744 if self.volume_timer_id is not None:
745 gobject.source_remove(self.volume_timer_id)
746 self.volume_timer_id = None
748 self.volume_timer_id = gobject.timeout_add(
749 1000 * timeout, self.__volume_hide_callback )
751 def __volume_hide_callback(self):
752 self.volume_timer_id = None
753 self.volume.hide()
754 return False
756 def toggle_volumebar(self, widget=None):
757 if self.volume_timer_id is None:
758 self.__set_volume_hide_timer(5)
759 else:
760 self.__volume_hide_callback()
762 def volume_changed_gtk(self, widget, new_value=0.5):
763 settings.volume = new_value
765 def volume_changed_hildon(self, widget):
766 self.__set_volume_hide_timer( 4, force_show=True )
767 settings.volume = widget.get_level()/100.0
769 def mute_toggled(self, widget):
770 if widget.get_mute():
771 settings.volume = 0
772 else:
773 settings.volume = widget.get_level()/100.0
775 def on_player_stopped(self):
776 self.stop_progress_timer()
777 self.set_controls_sensitivity(False)
778 image(self.play_pause_button, 'media-playback-start.png')
780 def on_player_playing(self):
781 self.start_progress_timer()
782 image(self.play_pause_button, 'media-playback-pause.png')
783 self.set_controls_sensitivity(True)
785 def on_player_new_track(self):
786 for widget in [self.title_label,self.artist_label,self.album_label]:
787 widget.set_markup('')
788 widget.hide()
790 self.cover_art.hide()
791 self.has_coverart = False
793 def on_player_new_metadata(self):
794 metadata = player.playlist.get_file_metadata()
795 self.set_metadata(metadata)
797 if not player.playing:
798 position = player.playlist.get_current_position()
799 estimated_length = metadata.get('length', 0)
800 self.set_progress_callback( position, estimated_length )
802 def on_player_paused( self, position, duration ):
803 self.stop_progress_timer() # This should save some power
804 self.set_progress_callback( position, duration )
805 image(self.play_pause_button, 'media-playback-start.png')
807 def on_player_end_of_playlist(self, loop):
808 pass
810 def reset_progress(self):
811 self.progress.set_fraction(0)
812 self.set_progress_callback(0,0)
814 def set_progress_callback(self, time_elapsed, total_time):
815 """ times must be in nanoseconds """
816 time_string = "%s / %s" % ( util.convert_ns(time_elapsed),
817 util.convert_ns(total_time) )
818 self.progress.set_text( time_string )
819 fraction = float(time_elapsed) / float(total_time) if total_time else 0
820 self.progress.set_fraction( fraction )
822 def on_progressbar_changed(self, widget, event):
823 if ( not settings.progress_locked and
824 event.type == gtk.gdk.BUTTON_PRESS and event.button == 1 ):
825 new_fraction = event.x/float(widget.get_allocation().width)
826 resp = player.do_seek(percent=new_fraction)
827 if resp:
828 # Preemptively update the progressbar to make seeking smoother
829 self.set_progress_callback( *resp )
831 def on_btn_play_pause_clicked(self, widget=None):
832 player.play_pause_toggle()
834 def progress_timer_callback( self ):
835 if player.playing and not player.seeking:
836 pos_int, dur_int = player.get_position_duration()
837 # This prevents bogus values from being set while seeking
838 if ( pos_int > 10**9 ) and ( dur_int > 10**9 ):
839 self.set_progress_callback( pos_int, dur_int )
840 return True
842 def start_progress_timer( self ):
843 if self.progress_timer_id is not None:
844 self.stop_progress_timer()
846 self.progress_timer_id = gobject.timeout_add(
847 1000, self.progress_timer_callback )
849 def stop_progress_timer( self ):
850 if self.progress_timer_id is not None:
851 gobject.source_remove( self.progress_timer_id )
852 self.progress_timer_id = None
854 def get_coverart_size( self ):
855 if util.platform.MAEMO:
856 if self.__gui_root.fullscreen:
857 size = coverart_sizes['maemo fullscreen']
858 else:
859 size = coverart_sizes['maemo']
860 else:
861 size = coverart_sizes['normal']
863 return size, size
865 def set_coverart( self, pixbuf ):
866 self.cover_art.set_from_pixbuf(pixbuf)
867 self.cover_art.show()
868 self.has_coverart = True
870 def set_metadata( self, tag_message ):
871 tags = { 'title': self.title_label, 'artist': self.artist_label,
872 'album': self.album_label }
874 # set the coverart
875 if tag_message.has_key('image') and tag_message['image'] is not None:
876 value = tag_message['image']
878 pbl = gtk.gdk.PixbufLoader()
879 try:
880 pbl.write(value)
881 pbl.close()
883 x, y = self.get_coverart_size()
884 pixbuf = pbl.get_pixbuf()
885 pixbuf = pixbuf.scale_simple( x, y, gtk.gdk.INTERP_BILINEAR )
886 self.set_coverart(pixbuf)
887 except Exception, e:
888 self.__log.exception('Error setting coverart...')
890 # set the text metadata
891 for tag,value in tag_message.iteritems():
892 if tags.has_key(tag) and value is not None and value.strip():
893 tags[tag].set_markup('<big>'+value+'</big>')
894 tags[tag].set_alignment( 0.5*int(not self.has_coverart), 0.5)
895 tags[tag].show()
897 if tag == 'title':
898 # make the title bold
899 tags[tag].set_markup('<b><big>'+value+'</big></b>')
901 if not util.platform.MAEMO:
902 value += ' - Panucci'
904 self.__gui_root.main_window.set_title( value )
906 def do_seek(self, seek_amount):
907 resp = player.do_seek(from_current=seek_amount*10**9)
908 if resp:
909 # Preemptively update the progressbar to make seeking smoother
910 self.set_progress_callback( *resp )
914 ##################################################
915 # PlaylistTab
916 ##################################################
917 class PlaylistTab(gtk.VBox):
918 def __init__(self, main_window):
919 gtk.VBox.__init__(self)
920 self.__log = logging.getLogger('panucci.panucci.BookmarksWindow')
921 self.main = main_window
923 self.__model = gtk.TreeStore(
924 # uid, name, position
925 gobject.TYPE_STRING, gobject.TYPE_STRING, gobject.TYPE_STRING )
927 self.set_spacing(5)
928 self.treeview = gtk.TreeView()
929 self.treeview.set_model(self.__model)
930 self.treeview.set_headers_visible(True)
931 tree_selection = self.treeview.get_selection()
932 # This breaks drag and drop, only use single selection for now
933 # tree_selection.set_mode(gtk.SELECTION_MULTIPLE)
934 tree_selection.connect('changed', self.tree_selection_changed)
936 # The tree lines look nasty on maemo
937 if util.platform.DESKTOP:
938 self.treeview.set_enable_tree_lines(True)
939 self.update_model()
941 ncol = gtk.TreeViewColumn(_('Name'))
942 ncell = gtk.CellRendererText()
943 ncell.set_property('ellipsize', pango.ELLIPSIZE_END)
944 ncell.set_property('editable', True)
945 ncell.connect('edited', self.label_edited)
946 ncol.set_expand(True)
947 ncol.pack_start(ncell)
948 ncol.add_attribute(ncell, 'text', 1)
950 tcol = gtk.TreeViewColumn(_('Position'))
951 tcell = gtk.CellRendererText()
952 tcol.pack_start(tcell)
953 tcol.add_attribute(tcell, 'text', 2)
955 self.treeview.append_column(ncol)
956 self.treeview.append_column(tcol)
957 self.treeview.connect('drag-data-received', self.drag_data_recieved)
958 self.treeview.connect('drag_data_get', self.drag_data_get_data)
960 treeview_targets = [
961 ( 'playlist_row_data', gtk.TARGET_SAME_WIDGET, 0 ) ]
963 self.treeview.enable_model_drag_source(
964 gtk.gdk.BUTTON1_MASK, treeview_targets, gtk.gdk.ACTION_COPY )
966 self.treeview.enable_model_drag_dest(
967 treeview_targets, gtk.gdk.ACTION_COPY )
969 sw = gtk.ScrolledWindow()
970 sw.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
971 sw.set_shadow_type(gtk.SHADOW_IN)
972 sw.add(self.treeview)
973 self.add(sw)
975 self.hbox = gtk.HBox()
977 self.add_button = gtk.Button(gtk.STOCK_NEW)
978 self.add_button.set_use_stock(True)
979 set_stock_button_text( self.add_button, _('Add File') )
980 self.add_button.connect('clicked', self.add_file)
981 self.hbox.pack_start(self.add_button, True, True)
983 self.dir_button = gtk.Button(gtk.STOCK_OPEN)
984 self.dir_button.set_use_stock(True)
985 set_stock_button_text( self.dir_button, _('Add Directory') )
986 self.dir_button.connect('clicked', self.add_directory)
987 self.hbox.pack_start(self.dir_button, True, True)
989 self.remove_button = widgets.DualActionButton(
990 generate_image(gtk.STOCK_REMOVE, True),
991 self.remove_bookmark,
992 generate_image(gtk.STOCK_CANCEL, True),
993 lambda *a: player.playlist.reset_playlist() )
994 #self.remove_button.set_use_stock(True)
995 #self.remove_button.connect('clicked', self.remove_bookmark)
996 self.hbox.pack_start(self.remove_button, True, True)
998 self.jump_button = gtk.Button(gtk.STOCK_JUMP_TO)
999 self.jump_button.set_use_stock(True)
1000 self.jump_button.connect('clicked', self.jump_bookmark)
1001 self.hbox.pack_start(self.jump_button, True, True)
1003 self.info_button = gtk.Button()
1004 self.info_button.add(
1005 gtk.image_new_from_stock(gtk.STOCK_INFO, gtk.ICON_SIZE_BUTTON))
1006 self.info_button.connect('clicked', self.show_playlist_item_details)
1007 self.hbox.pack_start(self.info_button, True, True)
1009 self.pack_start(self.hbox, False, True)
1011 player.playlist.register( 'file_queued',
1012 lambda x,y,z: self.update_model() )
1013 player.playlist.register( 'bookmark_added', self.on_bookmark_added )
1015 self.show_all()
1017 def tree_selection_changed(self, treeselection):
1018 count = treeselection.count_selected_rows()
1019 self.remove_button.set_sensitive(count > 0)
1020 self.jump_button.set_sensitive(count == 1)
1021 self.info_button.set_sensitive(count == 1)
1023 def drag_data_get_data(
1024 self, treeview, context, selection, target_id, timestamp):
1026 treeselection = treeview.get_selection()
1027 model, iter = treeselection.get_selected()
1028 # only allow moving around top-level parents
1029 if model.iter_parent(iter) is None:
1030 # send the path of the selected row
1031 data = model.get_string_from_iter(iter)
1032 selection.set(selection.target, 8, data)
1033 else:
1034 self.__log.debug("Can't move children...")
1036 def drag_data_recieved(
1037 self, treeview, context, x, y, selection, info, timestamp):
1039 drop_info = treeview.get_dest_row_at_pos(x, y)
1041 # TODO: If user drags the row past the last row, drop_info is None
1042 # I'm not sure if it's safe to simply assume that None is
1043 # euqivalent to the last row...
1044 if None not in [ drop_info and selection.data ]:
1045 model = treeview.get_model()
1046 path, position = drop_info
1048 from_iter = model.get_iter_from_string(selection.data)
1050 # make sure the to_iter doesn't have a parent
1051 to_iter = model.get_iter(path)
1052 if model.iter_parent(to_iter) is not None:
1053 to_iter = model.iter_parent(to_iter)
1055 from_row = model.get_path(from_iter)[0]
1056 to_row = path[0]
1058 if ( position == gtk.TREE_VIEW_DROP_BEFORE or
1059 position == gtk.TREE_VIEW_DROP_INTO_OR_BEFORE ):
1060 model.move_before( from_iter, to_iter )
1061 to_row = to_row - 1 if from_row < to_row else to_row
1062 elif ( position == gtk.TREE_VIEW_DROP_AFTER or
1063 position == gtk.TREE_VIEW_DROP_INTO_OR_AFTER ):
1064 model.move_after( from_iter, to_iter )
1065 to_row = to_row + 1 if from_row > to_row else to_row
1066 else:
1067 self.__log.debug('Drop not supported: %s', position)
1069 # don't do anything if we're not actually moving rows around
1070 if from_row != to_row:
1071 player.playlist.move_item( from_row, to_row )
1073 else:
1074 self.__log.debug('No drop_data or selection.data available')
1076 def update_model(self):
1077 plist = player.playlist
1078 path_info = self.treeview.get_path_at_pos(0,0)
1079 path = path_info[0] if path_info is not None else None
1081 self.__model.clear()
1083 # build the tree
1084 for item, data in plist.get_playlist_item_ids():
1085 parent = self.__model.append(None, (item, data.get('title'), None))
1087 for bid, bname, bpos in plist.get_bookmarks_from_item_id( item ):
1088 nice_bpos = util.convert_ns(bpos)
1089 self.__model.append( parent, (bid, bname, nice_bpos) )
1091 self.treeview.expand_all()
1093 if path is not None:
1094 self.treeview.scroll_to_cell(path)
1096 def label_edited(self, cellrenderer, path, new_text):
1097 iter = self.__model.get_iter(path)
1098 old_text = self.__model.get_value(iter, 1)
1100 if new_text.strip() and old_text != new_text:
1101 # this loop will only run once, because only one cell can be
1102 # edited at a time, we use it to get the item and bookmark ids
1103 for m, bkmk_id, biter, item_id, iiter in self.__cur_selection():
1104 self.__model.set_value(iter, 1, new_text)
1105 player.playlist.update_bookmark(
1106 item_id, bkmk_id, name=new_text )
1107 else:
1108 self.__model.set_value(iter, 1, old_text)
1110 def on_bookmark_added(self, parent_id, bookmark_name, position):
1111 util.notify(_('Bookmark added: %s') % bookmark_name)
1112 self.update_model()
1114 def add_file(self, widget):
1115 filename = get_file_from_filechooser(self.main.main_window)
1116 if filename is not None:
1117 player.playlist.append(filename)
1119 def add_directory(self, widget):
1120 directory = get_file_from_filechooser(
1121 self.main.main_window, folder=True )
1122 if directory is not None:
1123 player.playlist.load_directory(directory, append=True)
1125 def __cur_selection(self):
1126 selection = self.treeview.get_selection()
1127 model, bookmark_paths = selection.get_selected_rows()
1129 # Convert the paths to gtk.TreeRowReference objects, because we
1130 # might modify the model while this generator is running
1131 bookmark_refs = [gtk.TreeRowReference(model, p) for p in bookmark_paths]
1133 for reference in bookmark_refs:
1134 bookmark_iter = model.get_iter(reference.get_path())
1135 item_iter = model.iter_parent(bookmark_iter)
1137 # bookmark_iter is actually an item_iter
1138 if item_iter is None:
1139 item_iter = bookmark_iter
1140 item_id = model.get_value(item_iter, 0)
1141 bookmark_id, bookmark_iter = None, None
1142 else:
1143 bookmark_id = model.get_value(bookmark_iter, 0)
1144 item_id = model.get_value(item_iter, 0)
1146 yield model, bookmark_id, bookmark_iter, item_id, item_iter
1148 def remove_bookmark(self, w=None):
1149 for model, bkmk_id, bkmk_iter, item_id, item_iter in self.__cur_selection():
1150 player.playlist.remove_bookmark( item_id, bkmk_id )
1151 if bkmk_iter is not None:
1152 model.remove(bkmk_iter)
1153 elif item_iter is not None:
1154 model.remove(item_iter)
1156 def select_current_item(self):
1157 model = self.treeview.get_model()
1158 selection = self.treeview.get_selection()
1159 current_item_id = str(player.playlist.get_current_item())
1160 for row in iter(model):
1161 if model.get_value(row.iter, 0) == current_item_id:
1162 selection.unselect_all()
1163 self.treeview.set_cursor(row.path)
1164 self.treeview.scroll_to_cell(row.path, use_align=True)
1165 break
1167 def show_playlist_item_details(self, w):
1168 selection = self.treeview.get_selection()
1169 if selection.count_selected_rows() == 1:
1170 selected = self.__cur_selection().next()
1171 model, bkmk_id, bkmk_iter, item_id, item_iter = selected
1172 playlist_item = player.playlist.get_item_by_id(item_id)
1173 PlaylistItemDetails(self.main, playlist_item)
1175 def jump_bookmark(self, w):
1176 selected = list(self.__cur_selection())
1177 if len(selected) == 1:
1178 # It should be guranteed by the fact that we only enable the
1179 # "Jump to" button when the selection count equals 1.
1180 model, bkmk_id, bkmk_iter, item_id, item_iter = selected.pop(0)
1181 player.playlist.load_from_bookmark_id(item_id, bkmk_id)
1183 # FIXME: The player/playlist should be able to take care of this
1184 if not player.playing:
1185 player.play()
1188 ##################################################
1189 # PlaylistItemDetails
1190 ##################################################
1191 class PlaylistItemDetails(gtk.Dialog):
1192 def __init__(self, main, playlist_item):
1193 gtk.Dialog.__init__( self, _('Playlist item details'),
1194 main.main_window, gtk.DIALOG_MODAL,
1195 (gtk.STOCK_CLOSE, gtk.RESPONSE_OK))
1197 self.main = main
1198 self.fill(playlist_item)
1199 self.set_has_separator(False)
1200 self.set_resizable(False)
1201 self.show_all()
1202 self.run()
1203 self.destroy()
1205 def fill(self, playlist_item):
1206 t = gtk.Table(10, 2)
1207 self.vbox.pack_start(t, expand=False)
1209 metadata = playlist_item.metadata
1211 t.attach(gtk.Label(_('Custom title:')), 0, 1, 0, 1)
1212 t.attach(gtk.Label(_('ID:')), 0, 1, 1, 2)
1213 t.attach(gtk.Label(_('Playlist ID:')), 0, 1, 2, 3)
1214 t.attach(gtk.Label(_('Filepath:')), 0, 1, 3, 4)
1216 row_num = 4
1217 for key in metadata:
1218 if metadata[key] is not None:
1219 t.attach( gtk.Label(key.capitalize()+':'),
1220 0, 1, row_num, row_num+1 )
1221 row_num += 1
1223 t.foreach(lambda x, y: x.set_alignment(1, 0.5), None)
1224 t.foreach(lambda x, y: x.set_markup('<b>%s</b>' % x.get_label()), None)
1226 t.attach(gtk.Label(playlist_item.title or _('<not modified>')),1,2,0,1)
1227 t.attach(gtk.Label(str(playlist_item)), 1, 2, 1, 2)
1228 t.attach(gtk.Label(playlist_item.playlist_id), 1, 2, 2, 3)
1229 t.attach(gtk.Label(playlist_item.filepath), 1, 2, 3, 4)
1231 row_num = 4
1232 for key in metadata:
1233 value = metadata[key]
1234 if key == 'length':
1235 value = util.convert_ns(value)
1236 if metadata[key] is not None:
1237 t.attach( gtk.Label( str(value) or _('<not set>')),
1238 1, 2, row_num, row_num+1)
1239 row_num += 1
1241 t.foreach(lambda x, y: x.get_alignment() == (0.5, 0.5) and \
1242 x.set_alignment(0, 0.5), None)
1244 t.set_border_width(8)
1245 t.set_row_spacings(4)
1246 t.set_col_spacings(8)
1248 l = gtk.ListStore(str, str)
1249 t = gtk.TreeView(l)
1250 cr = gtk.CellRendererText()
1251 cr.set_property('ellipsize', pango.ELLIPSIZE_END)
1252 c = gtk.TreeViewColumn(_('Title'), cr, text=0)
1253 c.set_expand(True)
1254 t.append_column(c)
1255 c = gtk.TreeViewColumn(_('Time'), gtk.CellRendererText(), text=1)
1256 t.append_column(c)
1257 playlist_item.load_bookmarks()
1258 for bookmark in playlist_item.bookmarks:
1259 l.append([bookmark.bookmark_name, \
1260 util.convert_ns(bookmark.seek_position)])
1262 sw = gtk.ScrolledWindow()
1263 sw.set_shadow_type(gtk.SHADOW_IN)
1264 sw.add(t)
1265 sw.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
1266 e = gtk.Expander(_('Bookmarks'))
1267 e.add(sw)
1268 self.vbox.pack_start(e)
1271 def run(filename=None):
1272 ossohelper.application_init('org.panucci', '0.4')
1273 PanucciGUI( filename )
1274 gtk.main()
1275 ossohelper.application_exit()
1277 if __name__ == '__main__':
1278 log.error( 'Use the "panucci" executable to run this program.' )
1279 log.error( 'Exiting...' )
1280 sys.exit(1)